Repository navigation
feat(automation): flow-level requiredPermissions, checked at every flow start door - #25643
Conversation
…w start door Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
…s anchor Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
…ion face row Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
…fusal Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
…ow-required-permissions
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
…iredPermissions row Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
📓 Docs Drift CheckThis PR changes 5 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 151 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 c7aa0a4ca6de3d5be3e2d884aaa7558047155215 && git checkout c7aa0a4ca6de3d5be3e2d884aaa7558047155215
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin baae6ed14e90e357b6e437e62aed01f0f1be9649 df8bbf94e36be6aa48e2eced4cedb9ee67a8a3c5 && git checkout -B drift-repro baae6ed14e90e357b6e437e62aed01f0f1be9649 && git merge --no-ff df8bbf94e36be6aa48e2eced4cedb9ee67a8a3c5
node scripts/docs-audit/affected-docs.mjs --json baae6ed14e90e357b6e437e62aed01f0f1be9649
|
席位核对ACCEPT · #25580 ·
Generated by Claude Code |
…uiredPermissions Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
席位核对 (round 2 addendum)ACCEPT stands · #25580 ·
Generated by Claude Code |
|
Claim: cross-account contract review of this PR at head ⛔ Not a claim on the card and no label change now. Under the cross-account review patrol ( Generated by Claude Code |
Contract reviewServed-tier: Cross-account review: the PR was implemented under Check-runs on ① Derived judgments
② Semver level
③ Boundary flagsPR author
Flags the lane seat raised for this review (
Implemented-by: VERDICT: PASS Generated by Claude Code |
…ing the official upgrade checklists of 18.0 and 18.1 (#110) Pin @objectstack/* to 18.1.0, move position-to-set bindings into the position definitions, and rewrite flow value slots as CEL envelopes (ADR-0032), per the 18.0 and 18.1 upgrade checklists. executed_upload answers 403 until the release carrying objectstack-ai/objectstack#25643. Part of #82 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
Fixes #25580
Ruling-ref: 6377848497
Clause-②: yes (widening)
Metadata-protocol: added
What
A flow can now declare who may START it:
FlowSchema.requiredPermissions: string[], the same value shape as an action'srequiredPermissions(ADR-0066 D4). Ruling A's order holds inside this one PR (retriage answer 6382852102): the spec adds the key, and the server checks it in the one shared check every start door asks.The shared check —
packages/runtime/src/flow-start-admission.ts#flowStartRefusalThe module already existed as "the ONE definition asked by every door that starts a flow by name" (the elevated self-triggered rule). Its door-facing predicate is replaced by one function,
flowStartRefusal(automation, flowName, executionContext), which returns the ADR-0112 refusal ornull:runAs: 'system'flow of typeautolaunched/record_change/schedule. Answers first, because no capability opens it.requiredPermissions(new): a non-system caller must hold every named capability, or403 PERMISSION_DENIED, nothing dispatched. The capability arithmetic ispackages/runtime/src/required-capabilities.ts#missingCapabilities, which the action gateactionPermissionErrornow asks too: one spelling of "holds X" for both keys.The system principal (
isSystem) is admitted, the action gate's bypass; the census row oncontent/docs/permissions/system-context.mdxis re-anchored to the new symbol.The doors (each one call to the check, nothing else)
POST /automation/:name/trigger(and the legacy spelling)packages/runtime/src/domains/automation.ts#respondToFlowTriggertype: 'flow': RESTPOST /actions/:object/:actionand MCPrun_actionpackages/runtime/src/action-execution.ts#dispatchFlowAction(both entrances reach it)type: 'flow'packages/runtime/src/endpoint-executor.ts#executeFlowpackages/mcpneeded no change:run_actionreachesdispatchFlowActionthrough the runtime bridge.Decisions recorded
dispatchFlowAction.subflownode starts the child throughengine.execute, which never reads the key. Pinned on the real engine.type: 'flow'endpoints as a fourth door that starts a flow by name, so leaving it out would leave a bypass.runAs: 'user'flows are gated too. The key gates the start, not the elevation.Authoring surfaces
os validate:capability-reference-unknownnow judgesflows[i].requiredPermissionsthrough the same code that judges actions (packages/lint/src/validate-capability-references.ts), andos explainnames the position.string-tags, as on the action form). The translation bundles are regenerated; zh-CN, ja-JP and es-ES are translated by hand.live) and state counts.Tests
packages/runtime/src/flow-start-required-permissions.test.tscovers all four doors (trigger, REST/actions, MCPrun_action, endpoint). Each case is pinned oncode+status+ never dispatched:403 PERMISSION_DENIED; with X → startspackages/services/service-automation/src/subflow-required-permissions.test.ts: a gated child runs from its parent for a member holding nothing, on the real engine.packages/spec/src/automation/flow-required-permissions.test.ts: full parse green and kept on the parsed definition; wrong value shapes refused at the same paths the action key refuses.packages/lint/src/validate-capability-references.test.ts: a typo atflows[1].requiredPermissionswarns; a granted one passes.Readings are on head
3ddf0535(and as noted per line) (local subset; CI runs the farm):@objectstack/runtimefull suite (pre-merge heada2fd696f; the merge brought no runtime change): Test Files 441 passed (441), Tests 7080 passed / 19 skipped. On the merged head86714f38: the three door pin files, 97 passed.@objectstack/specfull suite on86714f38: Test Files 716 passed (716), Tests 21712 passed / 1 todo. The first run, pre-merge, was 2 failed (the translation-face and form-reconciliation pins named the new key); both were fixed in this PR by offering the key in the flow form and adding a translation-face row.@objectstack/lintfull suite on86714f38: Test Files 159 passed (159), Tests 7245 passed.@objectstack/service-automationfull suite (pre-merge): Test Files 200 passed (200), Tests 2630 passed. The sub-flow pin on86714f38: 2 passed.@objectstack/platform-objectsfull suite on3ddf0535: Test Files 94 passed (94), Tests 1343 passed. The catalog count pin moved 678 to 679 for the new form row.Done, exit 0.Ablation (one-off, no permanent file). Mutating
if (missing.length > 0) return requiredPermissionsRefusal(toif (false) …throughscripts/ablation-replace.mjsredsflow-start-required-permissions.test.ts: 21 failed / 24 passed of 45. The file was restored byte-identical (blob9f81b3b1570bbefore = after;git diff HEADempty). The runtime tests importsrcrelatively, so nodist/leg was needed.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, which gives 122 commands.86714f38(after theorigin/mainmerge), each exit code captured before any pipe: 122 exit 0.check-plugin-teardown-shape --self-testfirst answered 3 (shallow clone). After fetching its pinned fixture commit it answered 0.3ddf0535) only touches a platform-objects test file. It re-derives the same 122, and the ratchet families on that head answered 0: test-source-alias, cross-package-test-inputs, i18n, i18n-stale-fill, type-check-debt, type-check-coverage.--ranreconciliation: 122 derived, 122 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.check-system-context-censusOK;check-platform-checklistOK, and--changed-from origin/mainOK with 0 anchors lost;check:dev-prereqsOK;check-nul-bytesOK; speccheck:generated"All 14 generated artifacts are up to date" after the merge.!in the title and no(narrowing)arm. CI runs the farm.Producers
Measured at
7265b33e77: 8 flows inexamples/**declarerunAs: 'system'(showcase 6, todo 2). 2 of them arescreen/api:showcase_inquiry_purge(screen) andshowcase_inbound_task_webhook(api). None declaresrequiredPermissions, so none changes behaviour.packages/**holds no flow definition that declaresrunAs: 'system'outside tests.Acceptance notes
requiredPermissions: MCPlist_actions(packages/runtime/src/domains/mcp.ts, theactionPermissionErrorfilter) and the/metaaction strip. So a member can still see an action whose target flow refuses them, and running it answers 403. These are UI conveniences, not access boundaries, and the doors enforce. Not filed: it is an observation, not one of the three filing classes.resume_run/ the screen-resume route continue a run that a door already admitted, so they are not checked again.requiredPermissionsrefusal is thrown as a bare sentence with nocode/status, as the existing elevated-door pins already record. The new flow-level refusal carriesPERMISSION_DENIED/403 at both entrances.runtime/src/domains/automation.ts(the trigger door lives there, not inaction-execution.ts) andruntime/src/endpoint-executor.ts.runtime/src/required-capabilities.tsand the door pin.service-automationsub-flow pin;spec/src/automation/flow.form.tsandspec/src/system/flows-translation-face.test.ts(form reconciliation and translation-face pins);platform-objectstranslation bundles;spec/liveness/flow.json;content/docs/permissions/system-context.mdx(census);docs/qa/platform-checklist/areas/automation.json(the anchor followed the renamed symbol).rest/src/import-prepare.tsandspec/src/api/endpoint-publish-gate.ts.current_user.positionsgate insrm_bank_verifyonce this ships. The four-eyes half (the creator may not verify) is out of scope per the ruling.Session:
session_01HEB7eZUjj5begLB9coGhFs🤖 Generated with Claude Code
https://claude.ai/code/session_01HEB7eZUjj5begLB9coGhFs
Generated by Claude Code