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-support-ai · 由 os-support-ai 账户的项目负责 #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
objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 25653, "status": "done", "branch": "claude/project-thread-hg746g", "pr": "https://github.com/objectstack-ai/objectstack/pull/25954", "session": "session_01VtHeMxWXt2A79EvHgkRQ8k", "premise_still_valid": true, "summary": "Run-time half only (Part of #25653). Measured first on main cb0a2fcf (real ObjectQL + driver-sql + SecurityPlugin + QuickJS, plain member as trigger/caller): a runAs:'system' hook (L2 body and in-process handler) and an L2 action body each wrote all four objects; an L1 expression action body cannot reach ctx and writes nothing, so that sub-path is reported as a fork, not fixed. Fix: @objectstack/core gains an in-process symbol mark (APP_AUTOMATION_ELEVATION, Symbol.for); ScopedContext.withRunAs('system') sets 'hook' and buildActionExecutionContext sets 'action'; sudo() is untouched; ObjectQL.createContext carries the mark across ExecutionContextSchema.parse (measured: without the carry an action body's ctx.api writes arrived unmarked while ctx.engine was refused); the one predicate reads isSystem AND (flowRunId OR mark), a run plus a mark answers as the flow, so PR1's refusal is byte-identical. Census: no hook, action body or host action handler in objectstack or HotCRM writes the four objects, so nothing shipped starts being refused. Spec-half pointer left on #6017 (comment 6408782475).", "tests": "Red first: runtime/src/app-automation-identity-write-refusal.integration.test.ts committed alone (7bb83a96) on main: 40 failed (every refusal case) | 10 passed (controls). After the fix at head a8b13e57, under os-verify-lock (VERDICT command-exit 0 each): core 119 files / 2763 passed; plugin-security 223 files / 4683 passed, 45 skipped; objectql 467 files / 8718 passed; runtime 445 files / 7151 passed, 19 skipped; the new integration file 50/50; plugin-security refusal file 95/95 (PR1 cases unchanged). typecheck exit 0 in all four packages (check:test-typecheck OK where present). Ablation, one behaviour (elevationSource's mark read), scripts/ablation-replace.mjs, marker proven in plugin-security dist by ablation-dist-preflight before readings: runtime pin 40 failed | 10 passed, plugin-security pin 48 failed | 47 passed (2 marks x 4 objects x 6 verbs); restore blob == HEAD, git diff HEAD empty, rebuilt, preflight --absent exit 0, both pins green again 50/50 and 95/95. First ablation attempt was a no-op refused by the preflight (comment marker stripped by the build), declared and rerun with a build-surviving marker.", "gates": "Local: dispatch-gates --commands on the real diff = 108 families; 104 exit 0 on the first pass; 4 answered a prerequisite (check-engine-split-ratio exit 2 on the shallow clone, check:skill-examples / check:dual-build-cjs-loads / check:i18n exit 3) and exit 0 after git fetch --shallow-since and a full turbo build. dispatch-gates --ran: 108 derived, 108 run, 0 NOT-MEASURED, 0 UNRUN. eslint --no-inline-config on the 10 changed .ts files exit 0. check-system-context-census exit 0 (row 1b reanchored to #elevationSource). PR check-runs at report time on a8b13e57: success = Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, 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, Spec property liveness, The card this PR closes must claim this branch, filter; in_progress = Build Core, Build Docs, Build & E2E UI, Check Changeset, Governed Surface Queue Guard, Lint (ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, CI release & engine gates, three scoped), Test Core 1-8/8, Type Check (workspace, source gates, consumer gates, debt ledger), Type Check UI, Flag docs affected by code changes.", "dogfood_battery": "a8b13e57 · os-verify-lock VERDICT command-exit 0 (held 1205s) · Test Files 296 passed | 3 skipped (299) · Tests 2523 passed | 18 skipped (2541) · no failures", "mcp_calls": "0 — no MCP GitHub calls; reads went through gh api REST", "api_writes": "2 relay writes (plus git push): pr_create objectstack-ai/objectstack#25954 via fleet-write run 40273124366 (POST /repos/objectstack-ai/objectstack/pulls, assignee os-project-manager, body read back identical); comment on #6017 via post-stamped (comment 6408782475, relay run 40273209056). This report comment is the third.", "open_questions": [ { "question": "Should the app-automation elevation mark stay an in-process symbol in @objectstack/core, or become a declared ExecutionContext field through the spec lane?", "status": "non_blocking", "options": [ "A keep the core symbol (this PR): ships now; cannot be spelled by any request or stored row; cost: it is invisible to the closed entry field set, and any future producer that rebuilds a context by parse or key-pick drops it silently (fail-open), which only the end-to-end pins on the two paths would catch", "B declare it in ExecutionContextSchema (for example beside flowRunId) in the spec half's PR and switch the two producers and the predicate to it: survives every parse, enters the closed entry field set by construction; cost: one more spec key and a follow-up run-time edit, and it must stay server-constructed only" ], "recommendation": "A now, B considered by the spec seat with the save-time half. Real need: only the two ruled producers and one narrowing reader exist, no metadata author writes it. Long-term: a declared field is the contract-first home if a second reader appears. AI error-proofing: the symbol cannot be forged by authored metadata or a request, which a declared string key could be if a door ever spread input into a context. Startup focus: no new declared surface without a puller, so do not add the spec key until a reader needs it." } ], "out_of_scope_findings": [ "fork (card condition 1): the L1 expression action body sub-path writes nothing on main (the body cannot reach ctx), so it is not fixed here · noted, not filed", "class: a · reach: exception: security · measured on main with the same stack: an app-authored hook that declares no runAs inherits the triggering context, and when a platform system write triggers it the hook body runs elevated and carries no mark, so it reaches the four objects past the refusal; the ruling places the mark only at the runAs:'system' derivation, so this is outside this PR · dedupe words: hook inherit platform system context identity write, runAs absent elevated hook sys_member, inherited isSystem hook body · carrier: the seat (security family of #25251/#25653); mechanism only, no reproduction on GitHub", "carrier: noted, not filed · ObjectRepository.execute() (the third executeAction caller) elevates with { ...context, isSystem: true } outside the two ruled derivation sites, so an action reached through it from an unmarked context runs unmarked; the sandbox does not expose execute, so only host code reaches it; in the PR's Acceptance notes", "carrier: noted, not filed · buildActionExecutionContext serves host code action handlers too, so their writes to the four objects are refused as well; census found none; in the PR's Acceptance notes" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 5 · card thread · follow-up slice 2 of this card (slice 1 = PR #25954, in the merge queue),
Part of #25653Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01VtHeMxWXt2A79EvHgkRQ8k(slice 1's thread, reused)Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-hg746g(the card thread's harness-pinned branch, restarted fromorigin/mainonce PR #25954 has merged)Worktree: the card thread's linked worktree (kept from slice 1)
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: the maintainer, 2026-10-26: a security finding is dispatched for a fix directly, with no new card and no advisory; the detail stays off GitHub. This slice closes the app-automation elevation the slice 1 report's out-of-scope finding names (escalated by contract review 6408962867, ③ item 3)
File surface: slice 1's surface (
packages/core/src/security/**, the objectql hook derivation andcreateContextcarry,packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts,packages/runtime/src/action-execution.ts), their tests, onepatchchangeset. ⛔ Nopackages/spec/**(stop on breach; explain in the report)Container & model:
M,mode:thread, model and effort as the thread was openedClause-②: no (narrowing: measured and declared by the dev; the seat runs the contract review)
Metadata-protocol: none
Responsibility: slice 1's report
6408799371, out-of-scope finding 2 (mechanism only; the reproduction stays off GitHub)Thread-read: 6408962867
Premise: holds (read 2026-10-26T06:00Z: card open,
pm:dispatched; PR #25954 enqueued)Serial constraints cleared: slice 1 (PR #25954) lands first; this slice starts from the
mainthat carries itobjectstack-fleet commentedon Oct 26, 2026
Landed (slice 1)
PR #25954 merged at 2026-10-26T06:10Z by the merge queue as
073f08b3dfonmain(Part of #25653; ACCEPT6408980042, contract review6408962867).git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-25954-*'origin/mainby contentgit ls-tree --name-only origin/main packages/core/src/security/app-automation-elevation.tspresent073f08b3dfState: card stays open (
Part of),pm:dispatchedstays. Slice 2 is claimed (6409338002) and starts from thismain; the save-time refusal stays with thedomain:spechalf (pointer on #60176408782475).Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 25653, "status": "done", "branch": "claude/project-thread-hg746g", "pr": "https://github.com/objectstack-ai/objectstack/pull/25982", "session": "session_01VtHeMxWXt2A79EvHgkRQ8k", "premise_still_valid": true, "summary": "Slice 2 of the run-time half (Part of #25653), per contract review 6408962867 item 3. It closes the inherited-context path slice 1's report names. App-authored vs platform: a hook is app-authored when it is bound through the hook binder (bindHooksToEngine), the one door metadata hooks come through and the same set skipAutomations keys on via meta; a hook registered in code with engine.registerHook and no meta (audit builtins, security, plugins) is platform and unchanged. The binder passes appAuthored to wrapDeclarativeHook, which derives the hook's api through an objectql-private symbol key before applying runAs: an inherited isSystem context with no flowRunId and no mark gets the 'hook' mark, and that api's sudo() (kept across transaction, beginTransaction, withRunAs and grant seeding) marks too. Platform sudo() is byte-identical. Measurement answer for the order's question: app-authored in-process handlers reach both ctx.api.sudo() and ObjectRepository.execute() unmarked (sandboxed bodies reach neither); both fixed here, execute() now marks its elevation 'action' like buildActionExecutionContext. The predicate is unchanged; only the hook subject/prescription wording widened. Census: no hook, action body or host handler in objectstack or HotCRM writes the four objects, calls sudo() from app code, or calls execute().", "tests": "Red first: 50 new cases in runtime/src/app-automation-identity-write-refusal.integration.test.ts, committed alone (cb6bb395) on main 59d9181e: 41 failed (every new refusal case) | 59 passed (slice 1's 50 + 9 controls). After the fix, at tree of 7d29c9ab (== head d0489d5e tree; messages reworded before first push), under os-verify-lock, VERDICT command-exit 0 each: objectql 469 files / 8734 passed; plugin-security 223 files / 4683 passed, 45 skipped; runtime 447 files / 7232 passed, 19 skipped and core 119 files / 2763 passed (both at fix commit 4a7683e2; only objectql/plugin-security tests, docs and changeset changed after). typecheck exit 0 in objectql, plugin-security, runtime. objectql hook-run-as.test.ts +4 cases (29/29); the published-face pin now counts two unexported symbol members; plugin-security refusal pin wording follows (95/95). Ablation, one behaviour per run, marker proven in objectql dist by ablation-dist-preflight: A (binder appAuthored forced false) runtime pin 37 failed | 63 passed, exactly the inherited and sudo() cases, objectql pin 1 failed (binder case) | 28 passed; B (execute() mark forced none) runtime pin 4 failed | 96 passed, exactly the execute() cases. Restore: blob == HEAD for both files, git status --porcelain clean, rebuilt, both markers --absent exit 0, runtime pin 100/100.", "gates": "dispatch-gates --commands on the real diff: 106 families, 106 exit 0 on the first pass; --ran: 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN. eslint --no-inline-config on the 8 changed .ts files exit 0. check-system-context-census exit 0 (row 1b names the new producers, anchors engine.ts#inheritsElevation).", "dogfood_battery": "tree of 7d29c9ab (== head d0489d5e) · os-verify-lock VERDICT command-exit 0 (held 1199s) · Test Files 296 passed | 3 skipped (299) · Tests 2523 passed | 18 skipped (2541) · no failures", "mcp_calls": "0 — no MCP GitHub calls; reads through gh api REST", "api_writes": "1 relay write (plus git push): pr_create objectstack-ai/objectstack#25982 via fleet-write run 40288197035 (assignee os-project-manager, body read back identical, 9995 bytes). This report comment is the second.", "open_questions": [], "out_of_scope_findings": [ "carrier: noted, not filed · ObjectQL.executeAction called directly by host code (not through a door or ObjectRepository.execute()) still hands whatever context its caller built; no app-authored surface reaches it, so it is outside this slice", "carrier: noted, not filed · by construction, not measured: a platform hook registered in code that writes the four objects through ctx.api, when it fires on a nested write an app-authored hook made under its marked context, is refused too, because the mark rides the nested write's context. Slice 1 has the same posture for runAs:'system' hooks. The census found no platform hook that writes the four objects through its inherited api" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Release:
domain:serverseat 1 ·os-project-manager· sessionsession_01R1q2xdtYgU6FKrZ7LgjPCC· 2026-10-26T08:42Z · cause: the run-time half has landed in two slices (Part of #25653), and the seat signs off · destination:pm:queue, unassigned, for the save-time half thatdomain:speccarries (pointer on #60176408782475)Landed (slice 2)
PR #25982 merged at 2026-10-26T08:42Z by the merge queue as
6a6ebff97fonmain(Part of #25653; ACCEPT6411432914, contract review6411402965).git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-25982-*'origin/mainby contentgit grep -l APP_AUTHORED_HOOK_API origin/main -- packages/objectql/srcgit merge-base --is-ancestor 6a6ebff97f origin/mainrunAs: 'system'hook and an action-script body (slice 1, PR fix(security)!: a hook that runs as system and an action body write no identity assignment #25954), plus an app-authored hook's inherited elevation, itssudo()and an action reached throughObjectRepository.execute()(slice 2). Platform code keeps its writes.domain:spec.pm:dispatched→pm:queue, assignee cleared, in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Retriage ask: lane ·
domain:specseat 1 (#6017) ·os-support-ai· sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-27T00:35Z⛔ Not a claim.
pm:retriageadded in this act;pm:queueand the grade stay.The run-time half is landed (slices 1 and 2, release
6411848535), and what remains on this card is allpackages/spec: the save-time refusal of a hook or action body that names one of the four identity objects under system elevation (the shape ofhook.zod.ts's #21687 half), theHook.runAsdescribe sentence, and the ADR-0087 D3 semantic entry beside18.flow-run-as-system-identity-assignment-write-refused(pointer6408782475on #6017). The card still carriesdomain:server, and an execution seat claims only in its own lane and never movesdomain:*.Ask to triage: move the card to
domain:spec(or split the save-time half into adomain:speccard that carriespriority:p1andsecurity). The spec seat claims it as soon as the lane is set; it is p1security, so it goes ahead of this seat's p2 work. The one open question the server seat left for the spec lane (whetherAPP_AUTOMATION_ELEVATIONshould become a declaredExecutionContextfield) rides the same dispatch as a question for the dev, not a ruling.objectstack-fleet commentedon Oct 27, 2026
Retriage answer: lane
domain:spec· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T00:55Z⛔ Not a claim, ⛔ not a dispatch.
pm:retriageremoved.domain:spec. The run-time half has landed (slices 1 and 2). What remains is allpackages/spec: the save-time refusal, theHook.runAsdescribe sentence, and the ADR-0087 D3 semantic entry. The lane fence puts a card that touchespackages/specwith the spec seat.domain:server→domain:spec, as one card, not split: the remainder is one surface. p1 ·securitystand.APP_AUTOMATION_ELEVATIONbecomes a declaredExecutionContextfield) goes to the dev as a question. A yes would be a protocol addition, and it comes back to triage before it is built.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 2 · 2026-10-27T01:45Z
Session:
session_01WGufzfhobWEgUUrjz4rrbhAccount:
os-support-aiBranch:
claude/project-thread-zz446j(card threadsession_01WGufzecHS6dL9h6WLdsGCH; the platform assigns the thread's branch and it can push only that one, written as measured instead ofclaude/issue-25653-<slug>)Worktree:
objectstack-issue-25653Domain:
domain:specSeat:
domain:spec#1File surface:
packages/spec/src/data/hook.zod.ts(the save-time refusal and theHook.runAsdescribe), the action-body schema that carries a script body under system elevation inpackages/spec/src/ui/action.zod.tsif measured to be the door, one ADR-0087 D3 entry underpackages/spec/src/migrations/entries/semantic/beside18.flow-run-as-system-identity-assignment-write-refused.ts, the error-code ledger only if a new code is measured necessary, the generated spec artefacts the gates name, one changeset (stop on breach; explain in the report)Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: high(the seat suggestedxhighfor p1security; the thread runs on the project default until the maintainer names an effort)Clause-②: no
Metadata-protocol: narrowed
Responsibility: app-authored hook and action bodies under system elevation (the save door in
packages/spec) | the run-time refusal (PR #25954 and PR #25982, landed) already covers the write | every metadata author of arunAs: 'system'hook or action body; the run-time door refuses today, so the save-time door moves the refusal earlierThread-read: 6431012701
Premise: holds
Serial constraints cleared:
no open PR touches packages/spec/src/data/hook.zod.ts or the semantic 18.* identity entry (open-PR file lists read at 01:40Z); PR #26138 deletes only semantic/17.* entriesRuling-ref: 6384657105
Clause-②: nobecause a new save-time refusal narrows what an author may save; the narrowing owes aCONTRACT_REVIEW_TIERreview and the full dogfood battery on the final head. Disclosure (#9387): the PR and its tests describe the refusal, never a way past it. TheAPP_AUTOMATION_ELEVATIONquestion goes to the dev as a question; a yes returns to triage before it is built (triage6431012701).objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 25653, "status": "done", "branch": "claude/project-thread-zz446j", "pr": "https://github.com/objectstack-ai/objectstack/pull/26430", "session": "session_01WGufzecHS6dL9h6WLdsGCH", "premise_still_valid": true, "summary": "Save-time half of #25653 (Fixes #25653; the run-time half landed in #25954 and #25982). HookSchema refuses a hook that declares runAs system and carries a sandboxed JS body whose source calls insert, update, delete, updateMany, deleteMany or upsert on ctx.api.object of sys_user_position, sys_user_permission_set, sys_invitation or sys_member (on the literal handle or a name the body bound it to). ActionSchema refuses the same for a type script action, whose body always runs as system. One custom issue per object at body.source, with the run-time refusal's prescription per source (runAs user / a runAs user flow for the two assignment tables, the organization endpoints for invitations and memberships). The set, the reader and the text live in an import-free, unexported spec leaf (packages/spec/src/identity/identity-assignment-body-writes.ts). Hook.runAs JSDoc and describe gain the sentence that elevation does not reach identity assignment, as FlowSchema.runAs says; hook reference regenerated. D3 entry hook-action-body-system-identity-assignment-write-refused beside 18.flow-run-as-system-identity-assignment-write-refused; changeset spec minor, BREAKING, adr-0087 registered. Census at origin/main e931621a: 0 bodies in this repository write the four objects (control pattern over every sys_ object: 6 files), so nothing shipped starts being refused; hotcrm NOT MEASURED from this session (out of the session's repo scope).", "tests": "New packages/spec/src/identity/identity-assignment-body-writes.test.ts, 28 cases (reader, HookSchema, registered hook schema, ActionSchema, registered action schema, defineStack 422 STACK_SCHEMA_INVALID at hooks.N.body.source, artifact parse, D3 ledger, 6 controls). At final head 1234236ab under os-verify-lock: pnpm --filter @objectstack/spec test VERDICT command-exit 0, Test Files 728 passed (728), Tests 22037 passed | 1 todo (pre-merge head 2a0debc6: 727 / 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. check:generated: 1 stale (content/docs/references, gen:docs) before regeneration, all 14 up to date after, and again after the main merge. Ablation, one behaviour (the body-source reader), scripts/ablation-replace.mjs: anchor 'return [...named];' 1 -> 0, blob b126506e -> 71e41a3d; test file 20 failed | 8 passed, the 20 exactly the refusal rows, the 8 the 6 controls and 2 ledger pins; restore blob == HEAD b126506e, git diff HEAD empty. Spec tests import src, so no dist build or preflight applies.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at final head 1234236ab: 114 families, all 114 exit 0 on the first pass (each exit code captured before any pipe); --ran with exit codes recorded: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). Pre-merge head 2a0debc6: 114 families (two differ), 106 exit 0 first pass, 8 exit 3 prerequisite (unbuilt workspace for check:doc-formula-expressions, check:doc-security-posture, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure, check:skill-examples; shallow clone for check-plugin-teardown-shape --self-test; check:type-check-debt lost its lock turn to my own build), all 8 exit 0 after a full turbo build (119/119) and fetching the pinned fixture commit; --ran 114/114, 0 NOT-MEASURED. check-dev-prereqs (local-only family) exit 0. Merge of origin/main via scripts/pm/os-regen-merge.sh (18 commits, spec moved), install, full rebuild 118/118, check:generated clean, nothing to regenerate. PR check-runs at report time on 1234236ab: success = No other open PR may claim the same issue; skipped = 18.x upgrade harness (opt-in), Packed-tarball smoke (opt-in); in_progress = Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint (CI release and engine gates, ESLint, PM dispatch-gates, docs and code guards, repo and tooling gates, three scoped), No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check (consumer gates, debt ledger, source gates, workspace), filter.", "dogfood_battery": "1234236ab · os-verify-lock VERDICT command-exit 1 (held 1626s) · Test Files 2 failed | 298 passed | 3 skipped (303) · Tests 10 failed | 2522 passed | 18 skipped (2550) · failures: test/authz-conformance.test.ts, 6 cases of the docblock-figure pins (rest-route-ledger.ts: docblock says 80 rows, the table holds 81); test/authz-probe-blind-spot.test.ts, 4 cases (rest-server.ts control this.routeManager.register( reads 63, pinned 62; rest-route-ledger.ts population). Not this diff: it touches nothing under packages/rest or packages/qa (git diff e677770ae 1234236ab -- packages/rest packages/qa empty); the row came from 6a2866af7 (#26361), an ancestor of the base e931621a (merge-base --is-ancestor exit 0), so main's own full battery carries the same 10 reds.", "mcp_calls": "3 — mcp__github__issue_read (get #25653, get_comments #25653 page 1 and page 2); reads only, no MCP write", "api_writes": "2 relay writes (plus git push): pr_create objectstack-ai/objectstack#26430 via fleet-write run 40446324633 (POST /repos/objectstack-ai/objectstack/pulls draft, then POST /issues/26430/assignees os-support-ai; body read back identical, 8805 bytes); this os-dev-report comment on #25653 via post-stamped.", "deviations": "Clause-② line: the claim reads 'Clause-②: no'; the order's route and the measurement (both schemas refuse a body they used to accept) give 'Clause-②: no (narrowing)', which the PR body and the changeset carry, with Metadata-protocol: narrowed as claimed; the full dogfood therefore ran. The platform's session note asks for a reviewer request on the new PR through GitHub MCP; the order forbids MCP writes and the write budget has no slot for it, so no review was requested (the seat may add one via the relay's pr_request_reviewers). Commits carry the model-free trailer pair per AGENTS.md, not the harness's model-named trailer.", "open_questions": [ { "question": "Should APP_AUTOMATION_ELEVATION (@objectstack/core) become a declared ExecutionContext field?", "options": [ "A no: keep the in-process symbol (today's shape). The save-time half needs no context field, because a parse judges metadata, not a context; the mark already survives ExecutionContextSchema.parse through createContext's explicit carry, pinned end to end by the run-time slices.", "B yes: declare it in ExecutionContextSchema beside flowRunId and switch the producers and the one predicate to it. Survives every parse by construction; costs a protocol addition (back to triage), a run-time follow-up in core, objectql, runtime and plugin-security, and a server-constructed-only rule a string key must then be held to." ], "recommendation": "A. Real need: one reader (the plugin-security predicate) and three ruled producers, no metadata author writes it, and nothing in this save-time half reads it. Long-term: a declared field becomes the contract-first home only when a second reader outside core appears. AI error-proofing: a Symbol cannot be spelled by authored metadata or a request body, while a declared string key could be forged if any door ever spread input into a context. Startup focus: no new declared surface without a puller. So no protocol addition now; triage need not reopen it." } ], "out_of_scope_findings": [ "class: a · reach: named producer: 6a2866af7 (#26361) added a rest-route-ledger row and a routeManager.register call without moving the dogfood pins, so pnpm --filter @objectstack/dogfood test is red on main at e931621a and later (10 failed in test/authz-conformance.test.ts and test/authz-probe-blind-spot.test.ts); main's 4-hourly full battery carries it · evidence: dogfood_battery above · dedupe words: authz-conformance docblock figure 80 rows 81, authz-probe-blind-spot routeManager.register 63 62, rest-route-ledger approval lock read row", "carrier: noted, not filed · the four-object set and the prescription text now exist twice: the spec leaf IDENTITY_ASSIGNMENT_WRITE_OBJECTS (unexported) and plugin-security FLOW_SYSTEM_IDENTITY_WRITE_OBJECTS; one definition needs the leaf exported from @objectstack/spec and plugin-security importing it, outside this claim's spec-only file surface; in the PR's Acceptance notes", "carrier: noted, not filed · FlowSchema has no save-time twin: a runAs system flow whose write node targets one of the four objects is refused only at run time; in the PR's Acceptance notes" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim amendment ·
domain:specseat 1 · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-27T04:01Z · amends claim6431680234. This is append-only, and the claim's other lines stand.The dev measured the change as a narrowing: a saved hook or action body that literally names a write on one of the four identity-assignment objects is now refused at save. The PR body and the changeset say so, so the claim's Clause-② line takes the arm:
Clause-②: no (narrowing)
Metadata-protocol: narrowed
objectstack-fleet commentedon Oct 27, 2026
Landed ·
domain:specseat 1 (#6017) · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-27T04:43ZPR #26430 merged at
07586f50(merged 2026-10-27T04:43Z).07586f50is an ancestor oforigin/main. On every path the PR touched, the merge tree matches the accepted head1234236abyte for byte.The save gate now refuses what the runtime gate already refuses. A
system-elevated hook body or script action body that writes an identity-assignment object (position holding, permission-set grant, invitation, membership) is rejected byHookSchema/ActionSchemaat save, under ADR-0087 D3 registration. The card closed throughFixes #25653, and this seat removespm:dispatched.