Repository navigation
Security: a runAs system hook or an action script body reaches the four identity-assignment objects past the delegated-administration gate (the #25251 refusal requires flowRunId) #25653
Description
Filing gate: a maintainer ruling to act (Sales 「25251 同意」 = follow-up question 2, letter A, recorded on #25251). Filed by domain:services seat 1 (seat post #6021) · os-sales · session session_016g7nYP9wdNx1GDa5XDr7Uy. ⛔ Not a claim. Under the lane rule of 2026-10-24, this seat claims no new server cards, so the card waits in pm:queue for the next domain:server seat.
Reader: the domain:server lane: packages/objectql (declarative hook runAs derivation), packages/runtime (action script execution context) and packages/plugins/plugin-security (the #25251 refusal). The spec half goes to the domain:spec lane through a pointer on #6017.
Dedupe (open AND closed): "hook runAs system identity assignment", "action script body elevated write sys_member", and "without flowRunId". No hit. Parent: #25251 (closed; PR1 #25483, PR2 #25564, ADR amendment #25587). Precedent shape: #21520 → #21649 (run-time) + #21687 (save-time, hook half in hook.zod.ts).
Disclosure: mechanism only. ⛔ No reproduction steps go on this card or its PRs.
type: Bug
What is wrong
#25251's refusal (ruling 6354966423) stops a runAs: 'system' flow from writing sys_user_position, sys_user_permission_set, sys_invitation and sys_member. Its predicate is context.isSystem && context.flowRunId. Two other app-authored elevation paths carry isSystem: true and no flowRunId, so the predicate does not see them. Both reach the four objects through the security middleware's isSystem short-circuit. They are found by reading main, not run:
- A declarative hook with
runAs: 'system'fired by an ordinary user write.withRunAs('system')→sudo()={ ...triggering context, isSystem: true }(packages/objectql/src/engine.ts, thewithRunAsderivation). A hook fired under a system flow inherits that flow'sflowRunId, and PR1 already refuses it. The gap is the user-triggered one. - An action script body (
type: 'script', L1 expression or L2 sandbox). It runs underbuildActionExecutionContext(ec)={ ...caller context, isSystem: true }(packages/runtime/src/action-execution.ts). An L2 body also needs a declared write capability (packages/runtime/src/sandbox/quickjs-runner.ts).
Both are writable only with manage_metadata, the same reach as flows. Each such write lands with no granted_by, and the delegated-administration and invitation gates are skipped. Those are the same defects #25251 closed for flows.
Expected (ruled: the #25251 principle applies to the whole family)
App-authored automation does not use elevation to write the four governed identity objects, whether it is a flow, a hook or an action script.
- Run-time: the same
PERMISSION_DENIEDrefusal and prescription that PR1 gives: userunAs: 'user', or the invitation / membership doors. - Save-time: a hook or action whose body literally names one of the four objects with system elevation is refused at registration. This mirrors feat(spec)!: FlowSchema refuses a create_record, update_record or delete_record node whose static objectName is a stored-metadata table, with the runtime's prescription (#21654) #21687's hook half and is spec lane work.
Conditions on the executor (from the ruling)
- Measure first. Before any code, show on a local stack that each path above actually writes each of the four objects today. A path that does not write is reported as a fork on this card, not fixed.
- The marker goes only where app-authored metadata is derived. The provenance mark that lets the refusal see these writes is set in two places: the declarative hook's
runAsderivation and the action body's execution-context builder. ⛔ It is never set onsudo()itself, because platform code uses the same function (better-auth sync, approval mirrors, seeding). A pin shows a platformsudo()write to the four objects still succeeds. - One predicate. Extend
flow-system-identity-write-refusal.ts's predicate to recognise the new mark. Do not write a second refusal module. Each newisSystemread site gets its row oncontent/docs/permissions/system-context.mdx(check-system-context-census). - Pins:
- each path is refused on each of the four objects, with the code and the prescription;
- the same path under
runAs: 'user'takes the ordinary gates; - a system flow is still refused exactly as PR1 does;
- platform
sudo()writes still succeed.
- Spec half. The save-time refusal and its ADR-0087 D3 semantic entry belong to
domain:spec: leave a pointer on [PM seat] domain:spec — 🟢 os-tesla · session_013odhA26dvzy7pQmevdTTvy #6017. The run-time PR ships first, or in the same release. - A changeset is owed: a narrowing, with the
Clause-②arm read against AGENTS.md at claim time.
Notes for the dispatcher
- p1 · security, sized M–L: one run-time PR (objectql / runtime / plugin-security) plus one spec PR.
- Census at claim time: no hook or action body on
mainwrites the four objects (reading of the decision card). Re-run it, and name any shipped body that would start being refused. - Security: a runAs system flow writes identity-assignment rows (sys_user_position, sys_invitation) past the delegated-administration gate #25251 stays closed (ruling: it is not reopened).
Generated by Claude Code
Activity