Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): the enablement door refuses in its own words (#11666) - #12139
Conversation
A caller without `manage_metadata` that hit `POST /automation/:name/toggle` was answered with the refusal the three definition writes share — "Authoring automation flows requires the `manage_metadata` capability." They were disabling a flow, not authoring one: accurate about the policy #10243 ruled, and naming a verb the caller did not use. Adds a second refusal constant for the enablement arm, shaped on this file's own precedent (`SCREEN_READ_DENY_MESSAGE` beside `RUN_READ_DENY_MESSAGE`, #7968): a second constant for a second question, rather than a reworded shared one. The question "is this request the enablement door?" is extracted into `isFlowEnablementWrite` so the gate and the sentence read the SAME answer — this file's own rule that a question spelled at two call sites is two questions that happen to agree today. Copy only. The accept set is bit-identical, `PERMISSION_DENIED` / 403 is unchanged on every arm, and #10243's policy classification is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbG3rGVLjZStHQxHDtzJdJ
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f366a1cee15d37cf11ce30b2cf540279758c3c14 && git checkout f366a1cee15d37cf11ce30b2cf540279758c3c14
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e91e62acba1fc0d165031a4e561a39dc20634155 2969bb035f62dd7f8b326df2bbb5aaf8c4350a74 && git checkout -B drift-repro e91e62acba1fc0d165031a4e561a39dc20634155 && git merge --no-ff 2969bb035f62dd7f8b326df2bbb5aaf8c4350a74
node scripts/docs-audit/affected-docs.mjs --json e91e62acba1fc0d165031a4e561a39dc20634155
|
Pure move, no text changed. `isFlowEnablementWrite` and its `[#11666]` docblock were inserted BETWEEN the `[#10145]` docblock and `isFlowAuthoringWrite`, which left two doc comments stacked before one declaration: the nearer one wins, so the helper took the adjacent block and `isFlowAuthoringWrite` was left with none — while the `[#10145]` route-coverage table, where #10243's toggle arm and the `trigger` exclusion are justified, sat above a function it does not describe. That table is exactly what the next author reads before touching the gate. The helper now sits above the `[#10145]` block, so each docblock is adjacent to its own subject and the helper is still declared before its callers. Verified as a pure relocation: the file's multiset of lines is unchanged, and the `[#10145]` block is byte-identical before and after (md5 244ddce7…). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbG3rGVLjZStHQxHDtzJdJ
Uh oh!
There was an error while loading. Please reload this page.
Resolves the two additive conflicts in packages/runtime/src/domains/automation.ts against #12190 (#12156, the flow clone door), keeping BOTH sides: * isFlowAuthoringWrite — this branch's delegated toggle arm (`return isFlowEnablementWrite(parts, method)`) AND main's [#12156] clone comment block plus `if (method === 'POST' && parts[1] === 'clone')`. * the route docblock — this branch's `⚑ refused with its OWN sentence (#11666)` sub-line under the toggle row AND main's `POST /:name/clone` row, each sub-line under its own row, in table order. Adds the pin the merge owes: an unentitled `POST /automation/:name/clone` is refused with the AUTHORING sentence and not the enablement one. The clone door is a two-segment POST whose verb lives in `parts[1]`, so a future loosening of isFlowEnablementWrite would hand it this card's own defect on the arm that arrived after the card. Also corrects one enumeration this merge falsified: the FLOW_ENABLEMENT_DENY_MESSAGE docblock and the pin file's header both said the shared sentence reads correctly for "the three definition writes". Clone is a fourth. Part of #11666 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbG3rGVLjZStHQxHDtzJdJ
os-trump
commented
Aug 26, 2026
Round 3 — landing repair ( |
Fixes#11666
A caller without
manage_metadatathat hitPOST /api/v1/automation/:name/togglewasanswered with the refusal the three definition writes share. They were disabling a
flow, not authoring one.
This is option B from the card's table, as ruled at triage: a second refusal constant
for the enable/disable arm, naming the operation actually attempted. Options A and C were
closed at grading and are not reopened here — in particular the shared sentence is not
reworded, because it reads correctly today for
POST /,PUT /:nameandDELETE /:nameand widening it would degrade all three to fix one arm.
Copy only — nothing about the policy moved
codeandstatusare untouched.PERMISSION_DENIED/ 403 on every arm, asfix(runtime): POST /automation/:name/toggle joins the manage_metadata write set #11660's pins and the ADR-0112 vocabulary assert. Both constants share
FLOW_WRITE_DENY_STATUSandFLOW_WRITE_DENY_CODE; only the message varies.callers are refused on the same four routes,
isSystemstill bypasses, the anonymousfloor still answers first, and the legacy execution door
POST /automation/trigger/:nameis still excluded — including for a flow literally named
toggle.How the arm is identified
refuseUngrantedFlowWritenow receivesparts/methodand picks the sentence fromisFlowEnablementWrite— the same helperisFlowAuthoringWrite's toggle arm now calls.The question "is this request the enablement door?" is therefore spelled once, which is
this file's own stated rule (a policy spelled at three call sites is three policies that
happen to agree today). Under that arm's guard the helper reduces to exactly the
parts[0] !== 'trigger'it replaces, so the predicate's truth table is unchanged.One deviation from the precedent, reported rather than forced
The card pointed at
SCREEN_READ_DENY_MESSAGEbesideRUN_READ_DENY_MESSAGE(#7968). Itsshape transferred — a second constant, same
codeandstatus, a different sentencefor a different question — but its mechanism did not: that precedent is a second
constant read at a second call site (
refuseUnrelatedScreenRead, its own gate function).refuseUngrantedFlowWriteis one gate behind one predicate, so a second call site wouldhave required splitting
isFlowAuthoringWrite— i.e. moving the policy, which the cardfences. The selection therefore happens inside the existing refusal, off the existing
predicate.
Review round 2 — docblock adjacency (
0d73d8dc)Review caught that the new helper had been inserted between the
[#10145]docblock andisFlowAuthoringWrite, stacking two doc comments before one declaration. The nearer onewins, so the helper took the adjacent block and the gate predicate was left with none —
while the
[#10145]route-coverage table (where #10243's toggle arm and thetriggerexclusion are justified, i.e. what the next author reads before touching the gate) sat above
a function it does not describe.
Fixed as a pure move: the helper and its docblock now sit above the
[#10145]block, soeach docblock is adjacent to its own subject and the helper is still declared before its
callers. Verified as a relocation rather than a rewrite — the file's multiset of lines is
unchanged, and the
[#10145]block is byte-identical before and after (md5244ddce71dfd4c7245faa8c53ee23316). Its text was not edited.Verification
Union run at
0d73d8dc, on a clean tree.The pins assert what the refusal SAYS, on the arm that was refused. A pin asserting only
403 cannot see this defect — the defect ships a 403 today, and
automation-write-capability-gate.test.tspasses over it in both the before and the afterstate. New file
packages/runtime/src/domains/automation-toggle-deny-message.test.ts(15tests) drives
dispatcher.dispatch('POST', '/automation/…')— the registered domainroute
createAutomationDomaininstalls, not thehandleAutomationshortcut — and assertsboth directions: the toggle arm gets the new sentence and does not contain
Authoring;each definition write keeps the original and does not contain
Enabling or disabling.Reverse verification (run at
6df0268e; round 2 changed no executable text, only wherethe helper sits). Source reverted to
2ba4329e, pins kept, tree confirmed on disk in bothdirections before the run (
Enabling or disabling…absent: 0 hits;function isFlowEnablementWriteabsent: 0 hits;return parts[0] !== 'trigger';present: 1 hit;Authoring automation flows requirespresent: 1 hit). Result — 4 failed | 37 passed:automation-write-capability-gate.test.tsstayed green under the mutation — named hereas a regression guard, ⛔ never counted as red-before evidence. That it cannot see this
defect is the reason the new file exists. The restore leg was re-verified on disk
(new sentence present: 1; helper present: 1; old inline arm absent: 0) and the tree is clean.
Suites (5 files, 85 tests, all passing — re-run on
0d73d8dc): the new pin file plusautomation-write-capability-gate,automation-screen-read-gate,automation-run-read-permission-gate,automation-toggle-unknown-flow. Packagetypecheckexit 0 on the same tree.Gates, each read from its own verdict line (exit code captured before any pipe):
check:route-envelope·check:cross-package-test-inputs(both thepnpmandscripts/forms) ·check:published-files·check:slot-lookup·check:test-source-alias·
check:type-source-resolution·check:changeset-gate-self-tests·check:objectui-changeset·check:query-options-erasure·check:engine-double-contract·check:where-matcher·check:type-check-coverage·check:nul-bytes·check-ci-filter-parity·check-plugin-teardown-shape·check-adr-0087-registration·check-changeset-no-major·check-empty-changeset·release-rehearsal-clone --self-test·docs-audit/check-affected-docs·docs-audit/check-drift-comment. All exit 0. Theratchet families were re-run on the new head
0d73d8dcafter the round-2 push and aregreen there.
pnpm lintfull-repo, not narrowed —eslint . --no-inline-configover the whole tree,exit 0, re-run on
0d73d8dc.One declared narrowing.
check:type-check-debt(--re-measure) re-runs tsc for everyledger entry and needs the entire workspace built. Instead the
@objectstack/runtimeslice — the only entry this diff can reach — was measured directly, twice, over the same
program with the test exclusion lifted: 227 errors with the new pin file, 227 without,
identical to the ledger's recorded
errors: 227, with 0 diagnostics naming either changedfile. The ratchet does not move. (A first attempt at this measurement reported a false
0from a tsconfig placed outside the repo —TS2688: Cannot find type definition file for 'node'— and was discarded as not measured, not as zero.)Changeset
@objectstack/runtime: patch. A client branching on the human-readable prose of a 403rather than on
error.codeis the only thing that can notice.Generated by Claude Code