Repository navigation
fix(runtime): every flow start door judges a start input against the object the flow's definition reads it from - #29365
Conversation
…ds them from A start input that a flow's own definition reads as the id of a record of a fixed object (a CRUD node's filter.id, $eq or $in, a screen edit form's recordId, handed on through subflow and map inputs) is now loaded in the caller's own scope from that object at every door that starts the flow by name, and must be one id, or a list of ids where it is read as a list. A value the caller cannot read there is refused 404 RECORD_NOT_FOUND, and any other shape 400 VALIDATION_FAILED naming the input, before the run exists. The REST action route also refuses a body recordId that is not one id with 400 naming recordId, before the param contract, the load and dispatch. Claude-Session: https://claude.ai/code/session_01ADHkmRp2YLTpRc6Vfw6TD3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 47 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 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 730933e93d1120dfae25395b901b70730d7249ef && git checkout 730933e93d1120dfae25395b901b70730d7249ef
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57cde8cb3da5970f2c3f6512569cacdb9b2b06a8 1ffeb77cc18ed85fa628ece0052e2fdfd6258ae8 && git checkout -B drift-repro 57cde8cb3da5970f2c3f6512569cacdb9b2b06a8 && git merge --no-ff 1ffeb77cc18ed85fa628ece0052e2fdfd6258ae8
node scripts/docs-audit/affected-docs.mjs --json 57cde8cb3da5970f2c3f6512569cacdb9b2b06a8
|
The flow trigger route's 404 RECORD_NOT_FOUND row now says that a start input the flow's definition reads as a record id of a fixed object is loaded in the caller's scope from that object, and must be one id or a list of ids. The action page says a body recordId must be one id. Claude-Session: https://claude.ai/code/session_01ADHkmRp2YLTpRc6Vfw6TD3 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 维护者速读
席位意见:与 diff 相符。只判断 flow 定义里写明的 id 读取,不加新键、不改 spec;#29227 的检查原样先跑。正文与 changeset 只写规则(规则 C)。 席位核对ACCEPT ·
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Requested by Steve · project thread
Refs #29050
Clause-②: no
Metadata-protocol: none
维护者速读
filter.id/$eq/$in、screen 编辑表单的recordId,以及经subflow/map传下去的输入),所有按名字启动 flow 的入口在启动前,都按调用者自己的权限从「flow 读的那个对象」加载这个输入;读不到回404 RECORD_NOT_FOUND,值不是 id(或不是 id 列表)回400 VALIDATION_FAILED并点名该输入,不产生运行记录。REST action 入口的 bodyrecordId不是单个 id 时回 400 点名recordId。id的recordIdField都不变。新增拒绝只针对定义里声明的 id 读取。回滚即撤掉本 PR。packages/runtime/src/flow-start-admission.ts的flowDeclaredRecordReads;packages/runtime/src/action-execution.ts的refuseUnreadableFlowStartRecords与flowActionStartInputs;domains/actions.ts的recordId形状检查。pnpm --filter @objectstack/runtime exec vitest run src/flow-start-declared-record-reads.test.ts,应看到 48 条全过。What changes
Flow start admission's third question (#29227,
refuseUnreadableFlowStartRecords) now also asks it of the object the flow reads, wherever the flow's own definition declares that read.The rule. Where a flow's definition reads a start input as the id of a record of a fixed object, every door that starts the flow by name loads that input in the caller's own scope from that object before the run exists:
404 RECORD_NOT_FOUND(one envelope for both);400 VALIDATION_FAILEDnaming the input, and nothing is loaded;Where a definition declares such a read (
flowDeclaredRecordReads,flow-start-admission.ts), across every region of the flow (collectFlowGraphs):get_record,update_recordordelete_recordnode with a literalobjectNamewhosefilter.idorfilter.id.$eqis the input (one id), or whosefilter.id.$inis (a list);screennode with a literalobjectNamewhoserecordIdis the input;subflowormapnode with a literalflowNamethat hands the input (or, for amapiterating it, each item of it) to a callee input the callee reads so, followed up to 8 levels and never re-entering a flow already on the path."Is the input" means a CEL value whose whole source is the input's name, bare or through
vars.. A computed value is not a declared read, and a name aloopormapnode binds as its item or index variable is not read as a start input.Which values each door judges.
type: 'flow'endpoint: every key of the builtparamsbag (flowStartRecordKeysnow also returns it asinputs).flowActionStartInputs): each key of the caller's ownparams, and the routed record id under the row slots. The routed record keeps its subject load against the action's object, and is now also judged against the object the flow reads. Left out: the subject record's own fields, and the row slots of an action whoserecordIdFieldnames a non-idfield.The REST action route's routed record is one id (
domains/actions.ts). A bodyrecordIdthat is not a string or a number is refused400 VALIDATION_FAILEDnamingrecordId, before the declared param contract, the record load and dispatch, for every action type. Absent,nulland''stay the record-less call.Unchanged. A start whose caller can read the record in the object the flow reads it from; an input the definition does not read as an id; the system principal; the #29227 checks on the start's named object, which run first as before.
Changeset:
.changeset/29050-flow-declared-record-reads.md, apatchfor@objectstack/runtime, rule-only.Docs, in the changeset's words:
content/docs/automation/flows.mdx: the trigger route's404 RECORD_NOT_FOUNDrow also states that a start input the flow's definition reads as the id of a record of a fixed object is loaded in the caller's own scope from that object, and must be one id or a list of ids (400naming the input otherwise).content/docs/ui/actions.mdx: a bodyrecordIdnames one record by its id, a string or a number, or the route answers400 VALIDATION_FAILEDnamingrecordId.Tests
packages/runtime/src/flow-start-declared-record-reads.test.ts(48 tests), over a row-scoped data double with two objects:vars.spelling, nodes inside a loop body and a try region, subflow and map hand-offs, a self-calling flow, and what declares nothing (a non-input, a computed value, a non-id filter key, a non-literal object, a shadowed loop item, a service with nogetFlow);recordIdthat is an object, a list or a boolean is 400 namingrecordIdfor a flow and a script action, with nothing loaded or run; a string and a number pass the shape check;Acceptance notes
packages/specchange and no new keys. A read the definition does not state (a computed id, a non-id filter key, a node that is not one of the above) is not judged; those are left for a decision that needs the spec.run_actiontool already accepts only a stringrecordId.execute(). Every refusal is asserted withexecutenever called.action-execution.tsanddomains/actions.ts, and feat(automation): an oauth2 connector with clientSource: 'installer' connects with the administrator's OAuth client #29097 touchesdomains/automation.ts; none of their hunks overlap this diff.Verification
Head
1ffeb77c: the two docs pages on top of5f06db07(the merge oforigin/mainat5780fa49ontoe92495c0). The code readings were taken on5f06db07;1ffeb77cchanges no code.Runtime package: typecheck, tests, lint
pnpm --filter @objectstack/runtime testthroughos-verify-lock.shon head5f06db07: Test Files 517 passed (517), Tests 8419 passed, 19 skipped (8438). VERDICT command-exit 0.pnpm typecheckinpackages/runtime(includescheck:test-typecheck): exit 0; the test-typecheck debt ledger held unchanged.vitest run src/flow-start-declared-record-reads.test.ts: Test Files 1 passed (1), Tests 48 passed (48).origin/main, the 50 existing runtime test files that drive the trigger routes, the endpoint and the action door (every test file naminghandleActions,dispatchFlowAction,invokeBusinessAction,flowStartRecordKeysorrefuseUnreadableFlowStartRecords): Test Files 50 passed (50), Tests 1326 passed (1326), with no edit.eslint packages/runtime --no-inline-config(the rootpnpm lintcommand, scoped): exit 0, no output.Each pin goes red with its check removed (seven ablations, each restored to the HEAD blob).
Dispatch gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon head1ffeb77cderived 92 commands (the 62 of5f06db07plus 30 docs families). Each ran with< /dev/null: 92 exit 0.dispatch-gates.mjs --repo objectstack-ai/objectstack --ran <file>: 92 derived famil(ies) accounted for, 92 run, 0 NOT-MEASURED (a DERIVED zero). Changed lines 967 (+937 / −30) against the 3000 threshold.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmRp2YLTpRc6Vfw6TD3