Repository navigation
feat(spec)!: refuse at save a system hook or script action body that writes an identity-assignment object - #26430
Conversation
…writes an identity-assignment object Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzcHS6dL9h6WLdsGCH
…gnment sentence Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzcHS6dL9h6WLdsGCH
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 146 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 b416147940989130809f4adc6c66c52b032f00e0 && git checkout b416147940989130809f4adc6c66c52b032f00e0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 75afeffdf564ae0bd4c0baa18791f765d10a91a8 1234236ab8fe2060cd98c1558a3570839786eee4 && git checkout -B drift-repro 75afeffdf564ae0bd4c0baa18791f765d10a91a8 && git merge --no-ff 1234236ab8fe2060cd98c1558a3570839786eee4
node scripts/docs-audit/affected-docs.mjs --json 75afeffdf564ae0bd4c0baa18791f765d10a91a8
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: Rendered by an isolated CONTRACT_REVIEW_TIER subagent of seat session_01WGufzfhobWEgUUrjz4rrbh ( VERDICT: PASS |
席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #25653
Clause-②: no (narrowing)
Metadata-protocol: narrowed
维护者速读
HookSchema与ActionSchema在保存时拒收「以 system 提权运行、且沙箱 JS 正文写四个身份分配对象(岗位持有、权限集授予、邀请、成员)」的钩子/动作;Hook.runAs说明补上「提权不触及身份分配」;新增一条 ADR-0087 D3 条目。packages/spec/src/identity/identity-assignment-body-writes.ts(判定与拒收文案)、packages/spec/src/data/hook.zod.ts的refuseElevatedIdentityAssignmentBodyWrite、packages/spec/src/ui/action.zod.ts的refuseIdentityAssignmentBodyWrite。pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/identity/identity-assignment-body-writes.test.ts,预期 28 passed。What changes
App-authored automation does not use elevation to write
sys_user_position,sys_user_permission_set,sys_invitationorsys_member(ruling record 6384657105, letter A). The run-time half landed in #25954 and #25982: the data middleware refuses that write withPERMISSION_DENIED. This PR is the save-time half that triage routed todomain:spec(6431012701).packages/spec/src/identity/identity-assignment-body-writes.ts(import-free, not exported from the barrel). It holds the four-object set, the reading of a sandboxed body's source (a write methodinsert,update,delete,updateMany,deleteManyorupsertonctx.api.object('OBJECT'), called on the handle or on a name the body bound it to), and the refusal text with its prescription per source.HookSchemagains a second object-level check,refuseElevatedIdentityAssignmentBodyWrite. It refuses a hook that declaresrunAs: 'system'and carries alanguage: 'js'body whose source writes one of the four objects. There is onecustomissue per object, atbody.source. The JSON-stage hook instack.zod.tskeeps the check throughsafeExtend.ActionSchemagainsrefuseIdentityAssignmentBodyWrite: the same check for atype: 'script'action, whose body always runs as system.Hook.runAs: the JSDoc and the.describe()now say that elevation does not reach identity assignment, asFlowSchema.runAsdoes. The hook reference page was regenerated (gen:docs).hook-action-body-system-identity-assignment-write-refusedsits beside18.flow-run-as-system-identity-assignment-write-refused. It adds no tombstone and no D2 conversion.@objectstack/specminor, BREAKING,adr-0087: registered.Tests
packages/spec/src/identity/identity-assignment-body-writes.test.ts, 28 cases:HookSchema, the registeredhooktype schema,ActionSchemaand the registeredactiontype schema each refuse each object atbody.source, with the right prescription.defineStackrefuses with422 STACK_SCHEMA_INVALIDathooks.N.body.source. It refuses the action too. An artifact's parse refuses the hook.runAs: 'user', an expression body and a code handler.Acceptance notes
origin/maine931621a: no hook or action body in this repository callsctx.api.object(...)on one of the four objects (git grep, 0 hits; the control pattern over everysys_*object hits 6 files). So no shipped body starts being refused. hotcrm was not re-measured from this session. The run-time slices' census found none there.FLOW_SYSTEM_IDENTITY_WRITE_OBJECTS. The claim's file surface is spec-only, so plugin-security does not import the spec set yet. That would also make the leaf a public export.FlowSchemahas no save-time twin of this check. ArunAs: 'system'flow's write node aimed at one of the four objects is refused only at run time, by the earlier change. This is noted here, not filed.Verification
All readings below are at head
1234236abunless noted.Spec package
pnpm --filter @objectstack/spec testunderos-verify-lock: VERDICT command-exit 0 · Test Files 728 passed (728) · Tests 22037 passed | 1 todo. The pre-merge head2a0debc6read 727 files / 22012 passed.pnpm --filter @objectstack/spec typecheck: exit 0 (check:test-typecheckOK).eslint --no-inline-configon the 5 changed.tsfiles: exit 0.pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date after the merge oforigin/main. The only regenerated artifact iscontent/docs/references/data/hook.mdx.Ablation (one behaviour: the body-source reader)
node scripts/ablation-replace.mjsreplacedreturn [...named];withreturn [];in the leaf. The anchor went 1 → 0 and the blob changed. With the mutation, the test file read 20 failed | 8 passed. The 20 were exactly the refusal rows. The 8 that stayed green were the 6 controls and the 2 ledger pins. On restore, the blob equalled HEAD andgit diff HEADwas empty. The spec suite imports fromsrc/, so no rebuild was involved.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon head1234236ab: 114 families. All 114 exited 0 on the first pass.dispatch-gates --ranwith every exit code recorded: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN.2a0debc6the same derivation (114 families, two different) also ended 114/114 exit 0. Eight of them first answered exit 3 (a prerequisite: an unbuilt workspace or the shallow clone), and they exited 0 after a full build and fetching the pinned fixture commit.node scripts/check-dev-prereqs.mjs(a local-only family): exit 0.Dogfood (full battery, final head)
pnpm --filter @objectstack/dogfood testunderos-verify-lockat1234236ab, after a full workspace build (118/118): VERDICT command-exit 1 · Test Files 2 failed | 298 passed | 3 skipped (303) · Tests 10 failed | 2522 passed | 18 skipped (2550).The 10 failures are all in
test/authz-conformance.test.ts(the #17111 docblock-figure pins: "docblock says 80 rows, the table holds 81" forpackages/rest/src/rest-route-ledger.ts) andtest/authz-probe-blind-spot.test.ts(thethis.routeManager.register(control inpackages/rest/src/rest-server.tsreads 63, pinned at 62). They are not caused by this diff. It touches no file underpackages/restorpackages/qa:git diff e677770ae 1234236ab -- packages/rest packages/qais empty. The row came from6a2866af7(#26361), which is already an ancestor of this branch's basee931621a(git merge-base --is-ancestorexit 0). Somain's own full battery carries the same reds.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses (eslint on the changed files locally; the repo-wide run is CI's).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzecHS6dL9h6WLdsGCH
Generated by Claude Code