Skip to content

Commit c9c1c6d

Browse files
committed
fix(runtime): keep tracker ids out of ledger prose; record the new elevation read
`check:doc-authoring` refuses tracker ids inside runtime prose strings — a route ledger `note` reaches operators and generated surfaces, none of whom can resolve `#NNNN`. The ids move to adjacent `//` comments, where the reader who can resolve them already is. `check:check-system-context-census` requires every `isSystem` read to carry an anchor on the census page. The new gate's `isSystem` bypass is anchored on the automation row and the page's seven census-derived counts move with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
1 parent cca312a commit c9c1c6d

2 files changed

Lines changed: 24 additions & 11 deletions

File tree

content/docs/permissions/system-context.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ the seed loader replaying package fixtures, a plugin's boot reconciler, a
99
service self-write, a migration.
1010

1111
This page is **the authority** for what that flag actually does. It exists
12-
because the flag is not one concept: it is a single boolean read at **106
12+
because the flag is not one concept: it is a single boolean read at **107
1313
distinct sites across 20 packages**, and knowing three of those behaviours gives
14-
no hint that the other hundred-and-three exist. Every documented app-side bug
14+
no hint that the other hundred-and-four exist. Every documented app-side bug
1515
traced to `isSystem` had the same shape — the metadata was complete and correct,
1616
and the gap was observable only by querying the resulting rows.
1717

@@ -131,7 +131,7 @@ that silently does not happen.
131131

132132
### 3. Sharing (`plugin-sharing`)
133133

134-
The largest single consumer — **17 of the 106 sites**.
134+
The largest single consumer — **17 of the 107 sites**.
135135

136136
| # | Behaviour when `isSystem` | What you get / what you lose | Anchor |
137137
|:--|:---|:---|:---|
@@ -172,7 +172,7 @@ The largest single consumer — **17 of the 106 sites**.
172172
| 54 | Package REST route capability gate bypassed | rest | Get: package read/write over REST without `manage_metadata` / `studio.access` / `setup.access` | `packages/rest/src/package-routes.ts#refusePackageRequest` |
173173
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `packages/runtime/src/domains/packages.ts#requireManageMetadata`, `#requireReadCapability` |
174174
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite`, `#refuseUngrantedActivationAuthoring` |
175-
| 57 | Automation run-state read, flow-authoring write and unrelated-screen read all pass | runtime | Get: run state, flow writes and screen reads with no grant | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#refuseUnrelatedScreenRead` |
175+
| 57 | Automation run-state read, flow-authoring write, unrelated-screen read and the two operator run-lifecycle writes all pass | runtime | Get: run state, flow writes, screen reads with no grant — and cancelling or restoring a suspension without the platform-operator rung. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#refuseUnrelatedScreenRead`, `#refuseUngrantedRunLifecycleWrite` |
176176
| 58 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertTenantAdmin` |
177177
| 59 | Email-template / webhook provenance stamps skipped | plugin-email, plugin-webhooks | Lose: the row is not marked as an admin customization | `packages/plugins/plugin-email/src/email-template-provenance.ts#bindEmailTemplateProvenanceStamp`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` |
178178
| 60 | **Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win | `packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner`, called from `packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes` |
@@ -277,7 +277,7 @@ Ownership injection, `readonly` bypass and sharing materialisation are
277277
independent decisions, and a seed loader plausibly wants the first two but not
278278
the third. The concept is nevertheless **staying as one boolean**:
279279

280-
- **Shipped semantics.** `isSystem` is a published contract with 106 read sites
280+
- **Shipped semantics.** `isSystem` is a published contract with 107 read sites
281281
in 20 packages. Splitting it is a breaking contract change across all of them.
282282
(The ruling was taken when the census read 80 sites in 18 packages; the count
283283
has grown, which strengthens rather than weakens the argument.)
@@ -334,16 +334,16 @@ still holds equal to the census on every pull request:
334334
| Appearances of the bare identifier `isSystem` in non-test sources | 813 ||
335335
| — parsed as a declaration | 22 ||
336336
| — parsed as an object-literal / type key (producers and option objects) | 310 ||
337-
| — parsed as a property **read** | 112 ||
337+
| — parsed as a property **read** | 113 ||
338338
| — parsed in some other syntactic position (a local, a cast, a conditional) | 9 ||
339339
| — the remainder: text inside comments and string literals | 358 ||
340340
| Of those reads: reads of one of the unrelated metadata fields | 6 ||
341-
| Of those reads: reads of `ExecutionContext.isSystem` | **106** ||
342-
| — behaviour-bearing (rows 1–62 above) | 102 ||
341+
| Of those reads: reads of `ExecutionContext.isSystem` | **107** ||
342+
| — behaviour-bearing (rows 1–62 above) | 103 ||
343343
| — carry the flag onward only (rows 63–66 above) | 4 ||
344344
| Packages containing at least one elevation read | **20** ||
345345
| Files containing at least one elevation read | 45 ||
346-
| — the distinct symbols those reads live in — what this page anchors | 89 ||
346+
| — the distinct symbols those reads live in — what this page anchors | 90 ||
347347
| — of those files, the ones holding more than one read in one symbol | 9 ||
348348

349349
The six rows marked — are a **dated decomposition, not a live claim**: they were

packages/runtime/src/route-ledger.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,10 +384,23 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [
384384
{ route: 'POST /automation/:name/runs/:runId/resume', domain: '/automation', disposition: 'sdk', client: 'automation.resume',
385385
note: "generic, so the SUSPENDED NODE gates it (#3801): a pause whose descriptor declares resumeAuthority:'service' — today `approval` / `approval_revise` — answers 403 here and continues only through its owning service (ApprovalService.decide), which authorizes and records the decision first. A node type that declares NO resumeAuthority answers 403 too, fail-closed since #5561: this door is an opt-in a descriptor states with 'any'. Screen/wait pauses are unaffected because they declare it; this route is the screen-flow runner's door" },
386386
{ route: 'GET /automation/:name/runs/:runId/screen', domain: '/automation', disposition: 'sdk', client: 'automation.getScreen' },
387+
// [#13953] Cancel a suspended run (ADR-0044) — the maintainer ruling of 2026-09-05
388+
// (option A) on the two operator run-lifecycle verbs. The engine has carried both for
389+
// as long as either has existed with no way for an operator to reach them: no REST
390+
// route, no CLI command, and until the contract half landed (PR #16563, card #16495)
391+
// not on `IAutomationService` either. #15981 is the correction this gate is built on
392+
// at birth: the platform-operator test is the ADR-0095 rung, never the `positions[]`
393+
// name. #13909 is the parent card the repair verb belongs to; #10243 / #12156 are the
394+
// toggle and clone arms whose `trigger` exclusion this predicate copies; #5519 is the
395+
// anonymous floor that answers first.
387396
{ route: 'POST /automation/:name/runs/:runId/cancel', domain: '/automation', disposition: 'server-only',
388-
note: "[#13953] Cancel a suspended run (ADR-0044) — the maintainer ruling of 2026-09-05 (option A) gave the engine's two operator run-lifecycle verbs a door, because until #16563 neither was reachable by an operator at all: no REST route, no CLI command, and not on `IAutomationService`. Body is the closed `{ reason? }` envelope, relayed VERBATIM to the engine, which lands it on the terminal `cancelled` log's `error`. ⚑ ONE authority tier and it is the strictest one this domain has: the ADR-0095 D2/D3 posture RUNG (`posture === 'PLATFORM_ADMIN'`, #15981 — NEVER `positions.includes('platform_admin')`, which `sys_user_position` lets a tenant mint), required UNCONDITIONALLY. ⛔ Not posture-conditional like the ADR-0126 §5 activation gate: that one falls open under `single` because `manage_metadata` still gates it there, and this door has no capability tier in front of it, so the same conditionality would open an operator verb to any authenticated caller on every single-organization deployment — looser than `resume`, whose `resumeAuthority` gate is fail-closed on every deployment. Fail-closed by construction: an absent executionContext, an absent posture or any other rung all reach the refusal, 403 `PERMISSION_DENIED` (ADR-0112); only `isSystem` bypasses, which is how plugin-approvals' in-process revise-window recall keeps working. The #5519 anonymous floor answers an unidentified caller 401 first. WHICH routes is one predicate, `isRunLifecycleWrite` in `domains/automation.ts`, read by the gate AND by both route arms so they cannot drift; it excludes `parts[0] === 'trigger'` so a flow literally NAMED `runs` keeps its legacy execution door. Answers 200 both ways — `cancelled: false` is idempotent success per the contract — but ⛔ never a BARE success: `false` is also what an UNREADABLE durable store answers, so the response carries a `notice` naming both readings (#13909's posture: never a door that returns success while hiding the condition). The `true` notice states that `true` is NOT exclusive — the engine has no cancel-side compare-and-set, so overlapping cancels each answer `true` and each record the terminal log; this door keys no once-only side effect off it and says so on the wire. A service not declaring `cancelRun` (an OPTIONAL member) answers 501 `NOT_IMPLEMENTED`, ⛔ never a 200 and ⛔ never `{ handled: false }`. NOT JS-SDK surface on this leg, and that is stated rather than left as an open gap: the ruling charters a REST door for a platform operator holding only HTTP and explicitly declines a CLI command for want of pull, so this card declares no client method and implies none; adding one reclassifies this row to `sdk`. Pinned in `domains/automation-run-lifecycle-door.test.ts`" },
397+
note: "Cancel a suspended run (ADR-0044) — the maintainer ruling of 2026-09-05 (option A) gave the engine's two operator run-lifecycle verbs a door, because until the contract half landed neither was reachable by an operator at all: no REST route, no CLI command, and not on `IAutomationService`. Body is the closed `{ reason? }` envelope, relayed VERBATIM to the engine, which lands it on the terminal `cancelled` log's `error`. ⚑ ONE authority tier and it is the strictest one this domain has: the ADR-0095 D2/D3 posture RUNG (`posture === 'PLATFORM_ADMIN'` — NEVER `positions.includes('platform_admin')`, which `sys_user_position` lets a tenant mint), required UNCONDITIONALLY. ⛔ Not posture-conditional like the ADR-0126 §5 activation gate: that one falls open under `single` because `manage_metadata` still gates it there, and this door has no capability tier in front of it, so the same conditionality would open an operator verb to any authenticated caller on every single-organization deployment — looser than `resume`, whose `resumeAuthority` gate is fail-closed on every deployment. Fail-closed by construction: an absent executionContext, an absent posture or any other rung all reach the refusal, 403 `PERMISSION_DENIED` (ADR-0112); only `isSystem` bypasses, which is how plugin-approvals' in-process revise-window recall keeps working. The anonymous floor answers an unidentified caller 401 first. WHICH routes is one predicate, `isRunLifecycleWrite` in `domains/automation.ts`, read by the gate AND by both route arms so they cannot drift; it excludes `parts[0] === 'trigger'` so a flow literally NAMED `runs` keeps its legacy execution door. Answers 200 both ways — `cancelled: false` is idempotent success per the contract — but ⛔ never a BARE success: `false` is also what an UNREADABLE durable store answers, so the response carries a `notice` naming both readings (never a door that returns success while hiding the condition). The `true` notice states that `true` is NOT exclusive — the engine has no cancel-side compare-and-set, so overlapping cancels each answer `true` and each record the terminal log; this door keys no once-only side effect off it and says so on the wire. A service not declaring `cancelRun` (an OPTIONAL member) answers 501 `NOT_IMPLEMENTED`, ⛔ never a 200 and ⛔ never `{ handled: false }`. NOT JS-SDK surface on this leg, and that is stated rather than left as an open gap: the ruling charters a REST door for a platform operator holding only HTTP and explicitly declines a CLI command for want of pull, so this card declares no client method and implies none; adding one reclassifies this row to `sdk`. Pinned in `domains/automation-run-lifecycle-door.test.ts`" },
398+
// [#13953] The repair verb's door — the exit from the `'stranded'` state #13937
399+
// shape 4 named and #13909 exists to measure. Its result is the inline structural
400+
// type PR #16563 landed in spec (card #16495 route (i)), whose `refusal?: string` is
401+
// the covariant widening the non-exhaustive status switch is a consequence of.
389402
{ route: 'POST /automation/:name/runs/:runId/restore-suspension', domain: '/automation', disposition: 'server-only',
390-
note: "[#13953] Put back the suspension a failed resume consumed (#13909) — the repair verb for `AutomationResult.status: 'stranded'`, re-arming a run the platform recorded as terminally failed. Same gate, same predicate and same fail-closed absent-member 501 as the cancel row above; see it for the authority and why the rung is unconditional. The card's own reason this needed a permission model rather than a line of routing: a repair verb re-arms a terminally-failed run, so 'who may do this' is a real question and NOT the same answer as 'who may resume'. Body is the closed `{ reason? }` envelope; ⭐ `requestedBy` is filled from the AUTHENTICATED CALLER and is refused BY NAME in the body, so no operator can write another's name into the trace that records who re-armed the run. Refusals are answered as refusals — `RUN_NOT_FOUND` 404, `STORE_UNAVAILABLE` 503, and the five run-state conflicts (`RESUME_IN_PROGRESS`, `RESTORE_IN_PROGRESS`, `RUN_SUSPENDED`, `RUN_COMPLETED`, `RUN_CANCELLED`, `NO_CONSUMED_SUSPENSION`) 409 — matching the statuses this same door already answers those conditions with on `resume`, ⛔ never a 200 carrying `restored: false`. ⚠️ The contract types the refusal as `refusal?: string`, a covariant widening of the engine's closed eight-member union (#16495 route (i)), so that mapping is a NON-EXHAUSTIVE string switch by construction: an unrecognised code — or a `restored: false` carrying none — answers 500, ⛔ deliberately not one of the 409s, which would claim a diagnosis this door did not make. ⛔ The vocabulary is neither narrowed nor extended at this call site; closing it is a `packages/spec` card. The engine's code rides `details.refusal`, ⛔ never `details.code`, which would promote an unregistered member into the ADR-0112-closed `error.code`. NOT JS-SDK surface on this leg, for the cancel row's reason; adding a client method reclassifies this row to `sdk`. Pinned in `domains/automation-run-lifecycle-door.test.ts`" },
403+
note: "Put back the suspension a failed resume consumed — the repair verb for `AutomationResult.status: 'stranded'`, re-arming a run the platform recorded as terminally failed. Same gate, same predicate and same fail-closed absent-member 501 as the cancel row above; see it for the authority and why the rung is unconditional. The card's own reason this needed a permission model rather than a line of routing: a repair verb re-arms a terminally-failed run, so 'who may do this' is a real question and NOT the same answer as 'who may resume'. Body is the closed `{ reason? }` envelope; ⭐ `requestedBy` is filled from the AUTHENTICATED CALLER and is refused BY NAME in the body, so no operator can write another's name into the trace that records who re-armed the run. Refusals are answered as refusals — `RUN_NOT_FOUND` 404, `STORE_UNAVAILABLE` 503, and the run-state conflicts (`RESUME_IN_PROGRESS`, `RESTORE_IN_PROGRESS`, `RUN_SUSPENDED`, `RUN_COMPLETED`, `RUN_CANCELLED`, `NO_CONSUMED_SUSPENSION`) 409 — matching the statuses this same door already answers those conditions with on `resume`, ⛔ never a 200 carrying `restored: false`. ⚠️ The contract types the refusal as `refusal?: string`, a covariant widening of the engine's closed eight-member union, so that mapping is a NON-EXHAUSTIVE string switch by construction: an unrecognised code — or a `restored: false` carrying none — answers 500, ⛔ deliberately not one of the 409s, which would claim a diagnosis this door did not make. ⛔ The vocabulary is neither narrowed nor extended at this call site; closing it is a `packages/spec` card. The engine's code rides `details.refusal`, ⛔ never `details.code`, which would promote an unregistered member into the ADR-0112-closed `error.code`. NOT JS-SDK surface on this leg, for the cancel row's reason; adding a client method reclassifies this row to `sdk`. Pinned in `domains/automation-run-lifecycle-door.test.ts`" },
391404
{ route: 'GET /automation/:name/runs/:runId', domain: '/automation', disposition: 'sdk', client: 'automation.getRun' },
392405
{ route: 'GET /automation/:name/runs', domain: '/automation', disposition: 'sdk', client: 'automation.listRuns' },
393406
{ route: 'GET /automation/:name', domain: '/automation', disposition: 'sdk', client: 'automation.get' },

0 commit comments

Comments
 (0)