Observation from #10145, filed rather than taken: two hand-maintained ledgers describe the /automation domain's authorization posture in prose, and both now under-describe it.
Nothing is broken and no gate reds — the enforcement exists and is pinned in packages/runtime/src/domains/automation-write-capability-gate.test.ts. What is stale is the description, and both ledgers exist precisely so the next author can find the decision they are standing on.
1. packages/runtime/src/route-ledger.ts — the three write rows carry no note
The resume row already documents its gate in exactly this field:
note: "generic, so the SUSPENDED NODE gates it (#3801) …"
By that convention the three rows #10145 gated could each name the capability they now require:
- line 319 ·
POST /automation → automation.create - line 331 ·
PUT /automation/:name → automation.update - line 332 ·
DELETE /automation/:name → automation.delete
⛔ Why #10145 did not do it: that file was under a hard serial with in-flight PR #10177. The dispatch's instruction was to stop and report if the fix needed a ledger row change; it did not (the row interface carries no capability field, so the gate is fully expressible without touching the file), so the note is a follow-up to sequence behind #10177 rather than a conflict to take.
2. packages/qa/dogfood/test/authz-conformance.matrix.ts — the anonymous-deny-automation row
The /packages sibling row names its per-route capability gates inside the same enforcement field:
… per-route capability predicates run after this floor — manage_metadata for every state-changing route …
The anonymous-deny-automation row still describes only the anonymous floor, so the domain's second gate is invisible to a reader of the ledger.
This moves no ratchet. The discover() probe for this file matches shouldDenyAnonymous( only, so the new gate adds no discovered key, cannot go UNCLASSIFIED, and the completeness check is unaffected — which is exactly why this is a finding and not a defect. Per the file's own #8711 note, "one row per primitive, each in EXACTLY ONE honest state" is a hand-maintained invariant, not a checked one; a primitive whose description drifts is precisely what that note says the ratchet cannot see.
Also worth a look while in there
The manage_metadata capability check is now spelled at four sites, each with its own docblock and each identical in mechanism:
Each new one copied the last deliberately rather than inventing, so they agree today. Whether that should become one shared predicate is a judgement call — the repo's own standing note on this shape is that "a policy spelled out at three call sites is three policies that happen to agree today" — but the refusal messages are legitimately per-domain, so a naive extraction would lose information. Related: #7020.
Observation from #10145, filed rather than taken: two hand-maintained ledgers describe the
/automationdomain's authorization posture in prose, and both now under-describe it.Nothing is broken and no gate reds — the enforcement exists and is pinned in
packages/runtime/src/domains/automation-write-capability-gate.test.ts. What is stale is the description, and both ledgers exist precisely so the next author can find the decision they are standing on.1.
packages/runtime/src/route-ledger.ts— the three write rows carry nonoteThe
resumerow already documents its gate in exactly this field:By that convention the three rows #10145 gated could each name the capability they now require:
POST /automation→automation.createPUT /automation/:name→automation.updateDELETE /automation/:name→automation.delete⛔ Why #10145 did not do it: that file was under a hard serial with in-flight PR #10177. The dispatch's instruction was to stop and report if the fix needed a ledger row change; it did not (the row interface carries no capability field, so the gate is fully expressible without touching the file), so the note is a follow-up to sequence behind #10177 rather than a conflict to take.
2.
packages/qa/dogfood/test/authz-conformance.matrix.ts— theanonymous-deny-automationrowThe
/packagessibling row names its per-route capability gates inside the sameenforcementfield:The
anonymous-deny-automationrow still describes only the anonymous floor, so the domain's second gate is invisible to a reader of the ledger.This moves no ratchet. The
discover()probe for this file matchesshouldDenyAnonymous(only, so the new gate adds no discovered key, cannot go UNCLASSIFIED, and the completeness check is unaffected — which is exactly why this is afindingand not a defect. Per the file's own #8711 note, "one row per primitive, each in EXACTLY ONE honest state" is a hand-maintained invariant, not a checked one; a primitive whose description drifts is precisely what that note says the ratchet cannot see.Also worth a look while in there
The
manage_metadatacapability check is now spelled at four sites, each with its own docblock and each identical in mechanism:packages/runtime/src/domains/meta.ts(twice — the PUT gate and_migrate-stored)packages/runtime/src/domains/packages.ts(requireManageMetadata, module-private)packages/runtime/src/domains/automation.ts(refuseUngrantedFlowWrite, added by Automation flow write routes (POST/PUT/DELETE /api/v1/automation) lack the manage_metadata gate — a plain tenant edits/deletes flows for every organization on a walled shared-database deployment #10145)Each new one copied the last deliberately rather than inventing, so they agree today. Whether that should become one shared predicate is a judgement call — the repo's own standing note on this shape is that "a policy spelled out at three call sites is three policies that happen to agree today" — but the refusal messages are legitimately per-domain, so a naive extraction would lose information. Related: #7020.