Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): route the flow param seeder through the single object-less action-key predicate - #15018
Conversation
…104 D2 param gate Both functions were ablated repo-wide first. `seedFlowActionParams` turned out to be pinned already — indirectly, through the REST route — but only on its object-BOUND leg; `enforceActionParams` had no pin at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ess predicate Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ergence Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 24 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 6a3d903a77f11d69f7779211eacbb68884e98254 && git checkout 6a3d903a77f11d69f7779211eacbb68884e98254
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fddfc8db062d61ca68ba482531f5368326109554 ae512c7e73df6f32d45393355e0fea50048fbb7e && git checkout -B drift-repro fddfc8db062d61ca68ba482531f5368326109554 && git merge --no-ff ae512c7e73df6f32d45393355e0fea50048fbb7e
node scripts/docs-audit/affected-docs.mjs --json fddfc8db062d61ca68ba482531f5368326109554 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14864
File face — declared, not discovered
This PR touches 5 paths, all in
packages/runtimeand.changeset:packages/runtime/src/action-execution.tspackages/runtime/src/action-owner-key-single-source.test.tspackages/runtime/src/action-object-less-key-agreement.test.ts(new)packages/runtime/src/action-params-enforcement.test.ts(new).changeset/object-less-action-key-one-predicate.md(new)Card #13906 (p1 · security ·
computeExecCtxseams) was flagged as a possiblecollision. It is not one: its committed face is
packages/rest/src/rest-api-plugin.ts,packages/rest/src/rest-server.ts, twopackages/resttests, apackages/qatest and anADR — zero files under
packages/runtime. Read from its worktree's own diff againstthe merge base, not inferred. No file is shared.
packages/specwas read, never edited.packages/rest/src/error-response.tsandpackages/cli/src/commands/generate.tswere not touched.The headline claim on the card is FALSE, and the ablation is how I know
The card and its triage both say "neither
seedFlowActionParamsnorenforceActionParamsis named by any test", and both correctly refused to let anyone build on it. Ablated
repo-wide against
packages/runtime(baseline 217 files / 3143 tests, all green), eachmutation proven on disk by blob hash and anchor counts, each restore proven by blob-hash
equality against the HEAD blob, under a
trap … EXIT INT TERMon absolute paths:seedFlowActionParamsgutted toreturn { ...params }http-dispatcher.actions-type-dispatch.test.tsenforceActionParamsreplaced with an unconditionalreturn nullSo the two functions are in opposite states, and the card's single sentence was wrong
about one of them and right about the other:
seedFlowActionParamswas pinned all along — indirectly, through the REST route,in a file that never names it. Exactly the "pinned somewhere I did not look" case the
method warning predicts. But every case in that file routes at a real object
(
/crm_lead/...), so its object-LESS leg — where the two predicates actually disagree —was the unpinned part. That is a sharper finding than "unpinned", and it is why the
divergence survived: the ladder was pinned, the leg was not.
enforceActionParamsis genuinely unpinned. The ADR-0104 D2 gate could stopexisting entirely and nothing in the repo would notice. Its validator
(
validateActionParams) is thoroughly pinned in@objectstack/spec; the runtime gatewrapped around it was not, and a green validator says nothing about whether anything
still calls it. Its only other mention outside the source is a manually-run
platform-checklist clause. Confirmed by a repo-wide search for its one observable string
Invalid action params— zero automated assertions, with a positive control that fired.Is
'*'dead, or merely unused today? — merely unusedThis is the measurement the card said to make first, and it decides the direction.
'*', so the divergence is not producing wrongresults in this repo today.
actionHandlerObjectKeysdeliberately probes'*'last, with a docblock sayingwhy: "kept last so a handler that user code registered directly against the wildcard
still resolves."
registerAction(objectName, actionName, handler)is public enginesurface — user code can and does pass its own key.
objectNameon this path is a raw URL path segment(
domains/actions.ts), unvalidated, soPOST /actions/*/theAction/theIdresolves aglobal flow action through
ownsRouteand lands in the divergence withobjectNameequal to
'*'.⇒
'*'is a live, deliberately-honoured legacy read path that happens to have nofirst-party writer. Retiring it is a compatibility decision about someone else's package,
with a retirement question attached (#3913). That is not a tidy-up this p3 is entitled to
make.
Direction taken: widen, and the divergence is narrower than the card says
seedFlowActionParamsnow asksisObjectLessActionKey(objectName)— the same predicatedispatchFlowActionasks three lines away before deciding whether to send anobjectatall. One predicate, one answer.
'*'and
''" as object-less, implying two divergent inputs. Measured againstorigin/main,it is exactly one. The old guard was
objectName && objectName !== GLOBAL_ACTION_OBJECT_KEY,and its
objectName &&truthiness leg already covered''(andundefined):isObjectLessActionKey'crm_lead''global''''*''*Id'The observable defect is a params bag carrying a nonsense key spelled
'*Id'besiderecordId, for a route the envelope on the previous line already called object-less.The pin, red-first, naming which arm fired
action-object-less-key-agreement.test.tsrun against unmodifiedorigin/mainsource:The arm that fired is the negative arm, not a control. Specifically:
an object-BOUND route still seeds its alias key) passed —the harness runs and the seeding path is live, so the negatives are not passing vacuously;
'global'and''rows passed — which is the table above, measured rather thanargued: only
'*'diverged;'*'row failed with the message the pin was written to emit.For the second pin,
enforceActionParams, the red-first proof is the ablation itself:re-running the A2 mutation with the new pin present turns 3 of its 5 tests red where
the whole 3143-test suite had stayed green — and the two that stay green are the
anti-vacuity control and the param-less pass-through, which that mutation genuinely does
not affect. Right arms, both directions.
Verification
All at final HEAD
ae512c7e73, heavy runs throughscripts/pm/os-verify-lock.sh, exitscaptured before any pipe.
pnpm --filter @objectstack/runtime exec vitest run— 219 files / 3154 passed(baseline on
origin/mainwas 217 / 3143; the delta is exactly the two new pin files).pnpm --filter @objectstack/runtime typecheck(run by name, chainscheck:test-typecheck) — gate's own verdict:check:test-typecheck: OK — @objectstack/runtime's test layer compiles under packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned signature(s) held in test-typecheck-debt.json.⭐
test-typecheck-debt.jsonis untouched — the new test files compile clean; nothingwas added to the shrink-only ledger, no
any, nots-expect-error, no weakened assertion.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackat this HEAD (not from a hand-written diff): 28 green, 3 NOT MEASURED — each
an exit 3, a gate's own PREREQUISITE NOT MET, which is not a red:
check:test-completeness— grades a savedturbo run testlog; CI tees one, there isnone locally, and the gate's own text says to record it as NOT MEASURED.
check:dual-build-cjs-loads— needs a whole-repopnpm buildfirst; that is CI's run.scripts/pm/check-half-states.mjs— repo-scoped GitHub reads are 403 on this session(
GET /rate_limitanswers 200 with 15000 left;GET /repos/...answers 403). Itsself-test half,
pnpm check:pm-half-states, passed with 2062 cases.pnpm lint— narrowed, and the narrowing is a measurement, declared as such. Therepo-wide
eslint . --no-inline-configis CI's run. Locally I linted the 4 source paths andprove the narrowing excluded nothing:
eslint.config.mjs, and its own header states it "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file, test or not"(
eslint.config.mjs:325-335);--format json: 4 files linted, 0 errors, 0 warnings;can move the verdict of a file it does not contain.
Scope
The
enforceActionParamspin is a second file beyond the strict divergence fix, named herebecause a coverage addition should never arrive unnamed. It is in scope: the card names that
function as half of its primary deliverable, the ablation proved it genuinely unpinned, no
other claim holds the file, and it adds no new gate surface. It changes no behaviour — it
pins the behaviour that is already there.
Changeset: patch on
@objectstack/runtime— a behaviour change in a released package,which is the fork
dispatch-gates.mjsprints. Notskip-changeset: this publishes from areleased package, so that label would be wrong here.
Kept out of scope, deliberately: retiring
'*', which needs the compatibility decisionabove; and
packages/spec, read but never edited.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code