Skip to content

feat(spec)!: refuse at save a system hook or script action body that writes an identity-assignment object - #26430

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-zz446j
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-zz446j

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread
Fixes #25653

Clause-②: no (narrowing)
Metadata-protocol: narrowed

维护者速读

  • 改了什么:HookSchema 与 ActionSchema 在保存时拒收「以 system 提权运行、且沙箱 JS 正文写四个身份分配对象(岗位持有、权限集授予、邀请、成员)」的钩子/动作;Hook.runAs 说明补上「提权不触及身份分配」;新增一条 ADR-0087 D3 条目。
  • 为什么改:运行时早已拒绝这类写入(上两片已合入),但保存门仍然接受,作者只能在运行失败时才发现;裁决 6384657105 字母 A 要求整个家族在保存时同样拒收。
  • 风险与回滚:只收窄 spec 的接受集,运行时不变;仓内普查为零命中,不会有已发布的钩子或动作开始被拒;回滚即撤回本 PR。
  • 先看: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_invitation or sys_member (ruling record 6384657105, letter A). The run-time half landed in #25954 and #25982: the data middleware refuses that write with PERMISSION_DENIED. This PR is the save-time half that triage routed to domain:spec (6431012701).

  • New leaf 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 method insert, update, delete, updateMany, deleteMany or upsert on ctx.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.
  • HookSchema gains a second object-level check, refuseElevatedIdentityAssignmentBodyWrite. It refuses a hook that declares runAs: 'system' and carries a language: 'js' body whose source writes one of the four objects. There is one custom issue per object, at body.source. The JSON-stage hook in stack.zod.ts keeps the check through safeExtend.
  • ActionSchema gains refuseIdentityAssignmentBodyWrite: the same check for a type: 'script' action, whose body always runs as system.
  • Hook.runAs: the JSDoc and the .describe() now say that elevation does not reach identity assignment, as FlowSchema.runAs does. The hook reference page was regenerated (gen:docs).
  • ADR-0087 D3 entry hook-action-body-system-identity-assignment-write-refused sits beside 18.flow-run-as-system-identity-assignment-write-refused. It adds no tombstone and no D2 conversion.
  • Changeset: @objectstack/spec minor, BREAKING, adr-0087: registered.

Tests

packages/spec/src/identity/identity-assignment-body-writes.test.ts, 28 cases:

  • The reader names each of the four objects for every write method and every quote form. It also names a write on a bound handle, and each object once.
  • HookSchema, the registered hook type schema, ActionSchema and the registered action type schema each refuse each object at body.source, with the right prescription.
  • defineStack refuses with 422 STACK_SCHEMA_INVALID at hooks.N.body.source. It refuses the action too. An artifact's parse refuses the hook.
  • The D3 entry is registered at 18 with no conversion and no tombstone.
  • Controls (what the run-time refusal leaves legal too): reads of the four objects, writes to other objects, a hook under runAs: 'user', an expression body and a code handler.

Acceptance notes

  • Census at origin/main e931621a: no hook or action body in this repository calls ctx.api.object(...) on one of the four objects (git grep, 0 hits; the control pattern over every sys_* 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.
  • The four-object set and the prescription text now live in two places: this spec leaf and plugin-security's 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.
  • FlowSchema has no save-time twin of this check. A runAs: '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 1234236ab unless noted.

Spec package
  • pnpm --filter @objectstack/spec test under os-verify-lock: VERDICT command-exit 0 · Test Files 728 passed (728) · Tests 22037 passed | 1 todo. The pre-merge head 2a0debc6 read 727 files / 22012 passed.
  • pnpm --filter @objectstack/spec typecheck: exit 0 (check:test-typecheck OK).
  • eslint --no-inline-config on the 5 changed .ts files: exit 0.
  • pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date after the merge of origin/main. The only regenerated artifact is content/docs/references/data/hook.mdx.
Ablation (one behaviour: the body-source reader)

node scripts/ablation-replace.mjs replaced return [...named]; with return []; 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 and git diff HEAD was empty. The spec suite imports from src/, so no rebuild was involved.

Gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on head 1234236ab: 114 families. All 114 exited 0 on the first pass.
  • dispatch-gates --ran with every exit code recorded: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN.
  • On the pre-merge head 2a0debc6 the 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 test under os-verify-lock at 1234236ab, 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" for packages/rest/src/rest-route-ledger.ts) and test/authz-probe-blind-spot.test.ts (the this.routeManager.register( control in packages/rest/src/rest-server.ts reads 63, pinned at 62). They are not caused by this diff. It touches no file under packages/rest or packages/qa: git diff e677770ae 1234236ab -- packages/rest packages/qa is empty. The row came from 6a2866af7 (#26361), which is already an ancestor of this branch's base e931621a (git merge-base --is-ancestor exit 0). So main's own full battery carries the same reds.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (eslint on the changed files locally; the repo-wide run is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzecHS6dL9h6WLdsGCH


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 31 documentable anchor(s).

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 75afeffdf564ae0bd4c0baa18791f765d10a91a8.

⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 146 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 75afeffdf564ae0bd4c0baa18791f765d10a91a8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b416147940989130809f4adc6c66c52b032f00e0 — the merge of head 1234236ab8fe2060cd98c1558a3570839786eee4 into base 75afeffdf564ae0bd4c0baa18791f765d10a91a8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 75afeffdf564ae0bd4c0baa18791f765d10a91a8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1234236ab8fe2060cd98c1558a3570839786eee4
Local-runs: none
Scope: packages/spec/src/data/hook.zod.ts, packages/spec/src/identity/identity-assignment-body-writes.ts, packages/spec/src/migrations/entries/semantic/18.hook-action-body-system-identity-assignment-write-refused.ts, packages/spec/src/ui/action.zod.ts, packages/spec/src/identity/identity-assignment-body-writes.test.ts, .changeset/25653-hook-action-body-identity-assignment-write-refused.md + packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts, packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts, packages/plugins/plugin-security/src/delegated-admin-gate.ts, packages/runtime/src/sandbox/quickjs-runner.ts, packages/objectql/src/engine.ts (ObjectRepository), packages/objectql/src/hook-binder.ts, packages/runtime/src/app-automation-identity-write-refusal.integration.test.ts, packages/spec/src/data/hook-body.zod.ts, packages/spec/src/automation/flow.zod.ts, packages/spec/src/migrations/entries/semantic/18.flow-run-as-system-identity-assignment-write-refused.ts, packages/spec/scripts/build-migration-registry.ts, content/docs/references/data/hook.mdx

① Derived judgments

  1. The run-time gate today, and what this PR adds. packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts refuses a governed write (GOVERNED_OPERATIONS: insert, update, delete, transfer, restore, purge) to the four objects when the context carries isSystem AND (flowRunId OR the @objectstack/core app-automation elevation mark). The middleware calls it ahead of its isSystem bypass (security-plugin-authz-middleware.ts, the block before line 320). After fix(security)!: a hook that runs as system and an action body write no identity assignment #25954 and fix(security)!: an app-authored hook's inherited elevation, its sudo() and an action it reaches by execute() write no identity assignment #25982 the mark covers a runAs: 'system' hook, an app-authored hook's inherited system context and its sudo(), an action body, and ObjectRepository.execute(). So the card's two run-time paths are closed on main, and the card's remaining ask (its "Save-time" line, retriage 6431012701: the save-time refusal, the Hook.runAs sentence, the D3 entry) is exactly this diff. Fixes #25653 is right. Correct.
  2. Evasion of the save-time check. The reader is a literal match: .object( with a single-quoted, double-quoted or backtick literal of one of the four names, followed by one of six write verbs, on the handle or on a const/let/var the body bound it to. A dynamic name (ctx.api.object(name)), concatenation ('sys_' + 'member'), a destructured object, bracket access, a reassignment without a declaration, or a handle returned by a function all pass the parse. None of them passes at run time: the write arrives at the middleware as an operation on the real object name under a marked context and is refused there. This is the shape the card ruled ("a hook or action whose body literally names one of the four objects"), the 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 precedent, and the D3 entry says "named with one of the four as a literal". The run-time refusal stays the enforcement; the parse is the authoring door. Correct, with the limit stated in the entry.
  3. Verb set. The six verbs (insert, update, delete, updateMany, deleteMany, upsert) are byte-for-byte the WRITE list the sandbox installs on ctx.api.object(name) (quickjs-runner.ts line 686); the sandbox hands them to ObjectRepository, which carries insert, update, delete and no upsert/updateMany/deleteMany (those three fault "not implemented" in the sandbox), so judging them is conservative and harmless. The reads (find, findOne, count, aggregate) are not judged, as at run time. ctx.api.sudo() does not exist in the sandbox, so it needs no reading. A write inside ctx.api.transaction(fn) whose callback does api.object('sys_member').insert(…) is caught: the handle pattern takes any prefix before .object(. Correct.
  4. What is now refused, and who carries it. Only a hook with runAs: 'system' and a language: 'js' body, or a type: 'script' action with such a body. A hook under 'user' or 'inherit', an expression body (no IO), a code handler, and every other object stay accepted; pinned by the controls. An 'inherit' hook that a platform system write elevates cannot be judged by a parse and is left to the run-time mark, which is right. Census on the tree: .object( with any of the four names in any quote form hits only packages/objectql/src/hook-run-as.test.ts (in-process handler code, not a body string) and two prose pages under content/docs/permissions/; nothing in examples/, showcase apps, templates or fixtures. The run-time pins in packages/runtime/src/app-automation-identity-write-refusal.integration.test.ts build exactly the refused shape, but through bindHooksToEngine, which does not parse with HookSchema (hook-binder.ts line 379), so they survive. hotcrm was not measured in this session or by the dev; the server slices' census found none there.
  5. Doors. HookSchema, the registered hook type schema (the metadata save door), ActionSchema, the registered action type schema, defineStack (422 STACK_SCHEMA_INVALID at hooks.N.body.source) and the artifact stage (safeExtend keeps the superRefine) are each pinned. ActionSchema is a .refine(...) chain, so the new .superRefine runs beside the existing ones; its position before the feature-gate term keeps the convention the comment names. Correct.
  6. Refusal text names a remedy. One custom issue per object at body.source; for the two assignment tables it says set runAs: 'user' (hook) or move the grant to a runAs: 'user' flow (action); for invitations and memberships it names the four organization endpoints. Same routing as the run-time refusal. Correct.
  7. False positives. A comment or string that spells ctx.api.object('sys_member').insert( refuses the body, and a shadowed binding name can refuse a body that writes another object. Both are conservative and both carry the remedy; the test's "reads stay legal" control shows the common case is not caught by accident. Not blocking.
  8. Docs truth. content/docs/references/data/hook.mdx is regenerated from the new .describe() and is true at the head: the governed-write list matches GOVERNED_OPERATIONS, the code is PERMISSION_DENIED, and the save-time sentence matches the refinement. The FlowSchema.runAs sentence it mirrors is at flow.zod.ts line 1183. The action reference did not change because the body .describe() did not change (only its JSDoc); that is consistent.
  9. Public face. The leaf is not exported from the barrel; the test imports it by path. The accept set of two published schemas narrows; no key is added, removed or re-shaped; no error code is minted. The four-object set and the prescription text now exist in the spec leaf and in plugin-security's FLOW_SYSTEM_IDENTITY_WRITE_OBJECTS; the dev names it in the PR's acceptance notes. Not blocking.

② Semver level

  • Changeset @objectstack/spec: minor, title feat(spec)!:, a **BREAKING** banner with Before/Now/Fix, Clause-②: no (narrowing) — … and Metadata-protocol: narrowed (…). The PR body carries the same two lines. clause2-line.mjs reads the arm as the first token inside the parenthetical and keeps trailing reasoning (lines 269 to 273), and readMetadataProtocolLine reads narrowed and ignores the parenthetical (lines 553 to 556). check-changeset-no-major.mjs: a declared narrowing is BREAKING and ships minor in the launch window (lines 1793 to 1840); narrowed beside a (narrowing) arm is the consistent pair. Check Changeset is green on the head. The level and the declarations agree.
  • The claim 6431680234 reads Clause-②: no with no arm, while the PR and changeset read no (narrowing). The dev declared the deviation in the report. The review judges the PR body and the changeset, which agree; the claim line is the seat's own and should be amended to no (narrowing) so the three carriers match (③ item 2).
  • ADR-0087: entry id hook-action-body-system-identity-assignment-write-refused, file 18.hook-action-body-system-identity-assignment-write-refused.ts, the same 18.id.ts rule as its neighbour 18.flow-run-as-system-identity-assignment-write-refused.ts; PROTOCOL_VERSION is 18.0.0, so step 18 is the open step. registry.ts is generated by build-migration-registry.ts from a readdir of entries/semantic, so no registry edit is owed, and the test reads the entry from MIGRATIONS_BY_MAJOR[18]. The changeset marker adr-0087: registered hook-action-body-system-identity-assignment-write-refused names only the id this diff adds, which is the check's rule. No conversionIds and a "NOT convertible" reason, the same disposition as the neighbour; no tombstone, pinned. surface carries no backticks and no pipes. Correct.

③ Boundary flags

  1. Open question (APP_AUTOMATION_ELEVATION as a declared ExecutionContext field). The dev recommends A (keep the core symbol). Answer: agreed for this PR. A parse judges metadata, not a context, so nothing here reads the mark, and a declared field would be a protocol addition that triage 6431012701 says returns to triage first. Nothing owed on this head.
  2. Claim line. The claim's Clause-②: no lacks the (narrowing) arm the PR and changeset carry. The seat amends the claim; no change to the diff.
  3. Prose that still reads unqualified. content/docs/automation/hooks.mdx line 157 calls runAs: 'system' "a full-access, RLS-bypassing system principal"; content/docs/ui/actions.mdx line 443 says an action body "bypass[es] row- and field-level security by design"; skills/objectstack-automation/SKILL.md line 185 says a hook elevates itself with runAs. None names the four-object exception. The run-time slices left flows.mdx the same way and put the fact on content/docs/permissions/system-context.mdx row 1b, and the reference page now carries it for hooks. One sentence in hooks.mdx and ui/actions.mdx (and the automation skill) is owed as a docs follow-up; the prose face is the lane seat's ACCEPT, not this review's gate.
  4. Dev-flagged, out of this card. FlowSchema has no save-time twin (a system flow's write node aimed at the four objects is refused only at run time); the set and prescription text live twice (spec leaf, plugin-security). Both named in the PR's acceptance notes; neither is in the card's ask. Follow-ups.
  5. Dogfood reds on main. The dev's full battery shows 10 failures in test/authz-conformance.test.ts and test/authz-probe-blind-spot.test.ts from feat(plugin-approvals,rest,client): a record-anchored approval lock read, { locked } for every caller who can read the record #26361, an ancestor of the base; this diff touches nothing under packages/rest or packages/qa. Not this PR's.
  6. Author. PR user.login is objectstack-fleet[bot]; no login note owed.
  7. CI on the head at review time. Green: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected, Governed Surface Queue Guard, Lint (ESLint, PM dispatch-gates, docs and code guards, repo and tooling gates, CI release and engine gates, three scoped), Spec property liveness, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Plan Test Core shards, filter. Skipped (opt-in or matrix): 18.x upgrade harness, Packed-tarball smoke, Dogfood Verify CLI, Temporal Conformance, Dogfood matrix shards. Still in progress at the last read (47 runs, 0 non-green, 13 pending): Test Core 1 to 8, Build Core, Type Check workspace and consumer gates, Type Check UI, Build and E2E UI. Nothing on the head is red. The landing pre-check (every check green) is the seat's, once those finish; this verdict judges the contract, and the seat re-reads the head's check-runs before the queue.

Implemented-by: claude/project-thread-zz446j
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

Rendered by an isolated CONTRACT_REVIEW_TIER subagent of seat session_01WGufzfhobWEgUUrjz4rrbh (domain:spec#1), read-only, adversarial to the dispatch's own conclusions.

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T04:21Z

Head 1234236ab8fe2060cd98c1558a3570839786eee4. Card #25653 (p1, security), claim 6431680234 plus amendment 6433428484, dev report 6433303741.

  • Contract review: owed at CONTRACT_REVIEW_TIER (a (narrowing) arm on @objectstack/spec). PASS 6433419685 names this head.
    • Fixes #25653 is right. The run-time refusal on main (flow-system-identity-write-refusal.ts, ahead of the middleware's isSystem bypass) already covers a runAs: 'system' hook, inherited elevation, sudo(), an action body and ObjectRepository.execute(). What retriage 6431012701 left is exactly this diff: the save-time refusal, the Hook.runAs sentence and the D3 entry.
    • The save-time reader is literal-only, as the card ruled, and every dynamic spelling is still refused at run time.
  • Shape: base main, Fixes #25653. Clause-②: no (narrowing) / Metadata-protocol: narrowed agree across the PR body, the changeset and the claim (amended in 6433428484). feat(spec)!, a BREAKING banner, @objectstack/spec: minor, and one ADR-0087 marker naming the new id. check-governed-merges --pr 26430: NOT governed, 532 changed lines.
  • Narrowing census: no examples, showcase, template or fixture body is refused. The only literal hit is an in-process handler test, which does not parse with HookSchema. The refusal names a per-object remedy.
  • Gates: check-runs on the head are 42 success, 7 skipped (fast-set filter and opt-in), 0 red.
  • Accepted deviations: no reviewer request (this record and the contract review are the review). APP_AUTOMATION_ELEVATION stays a plugin-security symbol, not a spec field, per the dev's answer A. A spec field would be a protocol addition that goes back to triage.
  • Follow-up, filed separately: three prose pages still describe system elevation as an unqualified bypass: content/docs/automation/hooks.mdx:157, content/docs/ui/actions.mdx:443 and skills/objectstack-automation/SKILL.md:185. That staleness predates this PR, because the run-time gate predates it. The dev's two other follow-ups (no save-time twin on FlowSchema, and the four-object set duplicated between the spec leaf and plugin-security) go on the same card.
  • Landing: Tier S, not governed. pr_ready + automerge_enable in this act.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants