Repository navigation
fix(runtime): every door that starts a flow by name refuses a record key the caller cannot read before the run exists - #29227
Conversation
…run in the caller's scope before the run exists WIP: the shared check (refuseUnreadableFlowStartRecords) beside the action door's subject load, called by the trigger routes, a declared flow endpoint and the action door's flow branch. Claude-Session: https://claude.ai/code/session_01ADHkmT7iCiGQkgXGQAo4Fo Co-authored-by: Claude <noreply@anthropic.com>
… a flow endpoint and the action door Claude-Session: https://claude.ai/code/session_01ADHkmT7iCiGQkgXGQAo4Fo Co-authored-by: Claude <noreply@anthropic.com>
…aunch on Eight files started a flow on a record against a harness with no data read for that record (or, at the action door, with a caller param naming a second row the harness did not serve). Each now serves the row to the caller, so the assertions they pin are unchanged and reached as before. Claude-Session: https://claude.ai/code/session_01ADHkmT7iCiGQkgXGQAo4Fo Co-authored-by: Claude <noreply@anthropic.com>
…d on the system-context page The flow start check that loads a start's record keys in the caller's scope skips the system principal, as the two admission rules already do. Row 57 now names that bypass and its symbol, and the site register is regenerated. Claude-Session: https://claude.ai/code/session_01ADHkmT7iCiGQkgXGQAo4Fo Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 28 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 af4d16b511f6487fb132b6c2e274108b5b02233e && git checkout af4d16b511f6487fb132b6c2e274108b5b02233e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0c0d3b68e161afadb2b5bc9ce47ad53c4d15de16 0258ffd81f0ee054709dc272919c75c2db3daf40 && git checkout -B drift-repro 0c0d3b68e161afadb2b5bc9ce47ad53c4d15de16 && git merge --no-ff 0258ffd81f0ee054709dc272919c75c2db3daf40
node scripts/docs-audit/affected-docs.mjs --json 0c0d3b68e161afadb2b5bc9ce47ad53c4d15de16
|
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 维护者速读
席位意见:与 diff 相符。三个开放问题:用户身份 flow 也检查(取 A,入口一致是本卡要点,action 入口本就对每个 flow 做这次读取); 席位核对ACCEPT ·
|
…ecord refusal The trigger route now answers 404 RECORD_NOT_FOUND for a start whose record the caller cannot read (and 400 VALIDATION_FAILED for a record key that is not one id, or an elevated start with no object), before anything is dispatched. The table, its outcome count and the never-dispatched count say so. Claude-Session: https://claude.ai/code/session_01ADHkmT7iCiGQkgXGQAo4Fo Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 维护者速读
席位意见:文档行只写规则,不含复现。PR 正文仍引 席位核对ACCEPT ·
|
Requested by Steve · project thread
Fixes #29050
Clause-②: no
Metadata-protocol: none
维护者速读
type: 'flow'endpoint、flow 类 action 的 REST 与 MCP 入口)在启动前,都用调用者自己的权限读一次这次启动交给运行的记录;读不到就回 action 入口同一个404 RECORD_NOT_FOUND,不产生运行记录。runAs: 'system'运行的 flow 会越过调用者自己的读权限。本 PR 让三个入口问同一个问题、给同一个答复。packages/runtime/src/action-execution.ts的refuseUnreadableFlowStartRecords和flowActionCallerRecordKeys;domains/automation.ts的respondToFlowTrigger与flowStartRecordKeys;endpoint-executor.ts的executeFlow。pnpm --filter @objectstack/runtime exec vitest run src/flow-start-record-admission.test.ts,应看到 32 条全过。What changes
Every door that starts a flow by name now asks a third admission question after
flowStartRefusaland beforeexecute(): the row this start addresses. One function,refuseUnreadableFlowStartRecords(packages/runtime/src/action-execution.ts, beside the action door's subject load that it reuses), is called by:POST /api/v1/automation/:name/triggerand the legacyPOST /api/v1/automation/trigger/:name(respondToFlowTrigger,domains/automation.ts);type: 'flow'(executeFlow,endpoint-executor.ts);type: 'flow'at the REST action route and the MCPrun_actiontool (dispatchFlowAction), for the row slots the caller's ownparamsfill. The routed record keeps the subject load and refusal it already had, unchanged.The rule it applies:
recordId, the{objectName}Idalias, an action's declaredrecordIdParam), with the value the run will receive, a caller's ownparamsvalue included.callData('get', …), the read the action door's subject load makes), vialoadActionSubjectRecord.refuseDeniedSubjectLoad: the action door's own404 RECORD_NOT_FOUND(ADR-0112 envelope). An unreadable record and an id that names nothing get one answer. Nothing runs and no run is persisted.objectName(orobject) in a trigger or endpoint body, the action's own object at the action door. A start that names a record key but no object cannot be judged: a flow declaredrunAs: 'system'refuses it400 VALIDATION_FAILEDnamingobjectName; a flow that runs as the user starts as before.400 VALIDATION_FAILEDnaming the key, and is never loaded.recordIdFieldnames a field other thanid(its row slots carry that field's value, not an id).Supporting changes:
flow-start-admission.ts: the module docblock states the third question, andflowRunsElevatedis the one reading ofrunAsit needs. The module still imports nothing fromaction-execution.ts.domains/automation.ts:recordIdAliasis now the one spelling of the{objectName}Idalias, read bybuildAutomationContextto seed it and by the newflowStartRecordKeysto judge it. The trigger routes answer the refusal throughdeps.errorFromThrown, the exit the action door serves the same refusal through, so both doors answer one envelope.content/docs/permissions/system-context.mdx: row 57 (the flow start admission bypass of the system principal) now names this check and its symbol, and the site register is regenerated withcheck-system-context-census.mjs --write. The new function readsisSystemonce, socheck:system-context-censusrequired a row to own it..changeset/29050-flow-start-record-admission.md, apatchfor@objectstack/runtime, rule-only.Why
#29050: the doors that start a flow by name did not all ask whether the caller can read the record the start hands the run, so the three doors disagreed.
flow-start-admission.tsalready says a door left out of a start rule is the defect. This PR states the rule once and has every door ask it.Tests
packages/runtime/src/flow-start-record-admission.test.ts(32 tests), over a row-scoped data double:404 RECORD_NOT_FOUND, and no run is created;objectName, and nothing is loaded;flow-run-locale.test.ts,flow-run-posture.test.ts,flow-caller-param-keys.test.ts: the trigger-route cases' kernel also serves a data read for the launched record.domains/automation-trigger-params-shape.test.ts: the dispatcher double also serves the launched record (thea record launchcase).domain-handler-registry.test.ts: the both-routes identity case serves its launched record.endpoint-executor.test.ts: the flow delegation case'scallDatadouble returns the launched record.dispatcher-plugin.endpoint-fallback.integration.test.ts: the fake engine serves the row the flow endpoint is launched on.http-dispatcher.actions-type-dispatch.test.ts: inlets an explicit param win over every seeded key, the caller can also read the record its own param names.Acceptance notes
IAutomationService.executeby flow name from a caller: the two trigger spellings, a declaredtype: 'flow'endpoint, anddispatchFlowAction(shared by the REST action route and the MCPrun_actiontool). All four now ask the question. A parent flow'ssubflownode starts its child through the engine, not a door, asflow-start-admission.tsalready notes.objectName/object, and the action door has its action's object. A record key with no object is refused for an elevated flow and passes for a user flow, as described above. Nopackages/specchange.executenever called.Verification
Head
2020b6e5. Code readings were taken on5fda11a5(the merge oforigin/mainat4550091a);2020b6e5adds only the docs row above. Heavy runs went throughos-verify-lock.sh.Runtime package: typecheck, full test, lint
pnpm --workspace-concurrency=2 --filter '@objectstack/runtime^...' buildafter the merge: VERDICT command-exit 0.pnpm --filter @objectstack/runtime typecheck(includescheck:test-typecheck): VERDICT command-exit 0.pnpm --filter @objectstack/runtime test: Test Files 515 passed (515), Tests 8350 passed, 19 skipped (8369). VERDICT command-exit 0.expected 404 to be 200, anexecutemock called 0 times, or (the action door case) a run that never started.vitest runover the 8 edited files plus the new file: Test Files 9 passed (9), Tests 250 passed (250).eslint packages/runtime --no-inline-config(the rootpnpm lintcommand, scoped): exit 0, no output.Ablations: every new refusal goes red when the check is taken out
Each leg used
scripts/ablation-replace.mjson one anchor, ranvitest run src/flow-start-record-admission.test.tsthrough the lock, and restored.git diff HEADwas 0 bytes after every leg.expected 200 to be 404,expected 200 to be 400, and the parity caseexpected { status: 200, … } to deeply equal { status: 404, … }.a record key the caller cannot read: 404 RECORD_NOT_FOUND, no run):expected 200 to be 404.expected 200 to be 404, andpromise resolved "{ ok: true, … }" instead of rejecting.expected 200 to be 400.expected 404 to be 400.{object}Idslot (dropped from the keys): 2 failed:expected 200 to be 404.dispatch-gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon2020b6e5: 93 commands. Each ran with stdin closed after a fullturbo run build --filter='./packages/**'(Tasks 78 successful, 78 total; VERDICT command-exit 0). All 93 exited 0,check:nul-bytesincluded.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE: "93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED (a DERIVED zero — all 93 recorded an exit code and none of them is 3)". Exit 0.5fda11a5),check:system-context-censuswas red: the new function readsisSystemand no row oncontent/docs/permissions/system-context.mdxowned it. Row 57 now does (commit2020b6e5). Two prerequisite exits on that pass were cleared and pass on the final run: the teardown-shape self-test's pinned fixture commit was fetched, and the CJS-load gate ran after the full build.--commandslists them: the families whose argv takes a value only a CI run has (check-issue-citations.mjs --census, the shard attestation emitters,check-test-completeness.mjs), and the CI-scheduled jobs (Build Core, Build Docs, the type-check lanes, and themain-only dogfood and temporal batteries). Those are CI's readings.Human Minimum
Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code