Repository navigation
[finding] The metadata save door accepts a hook whose sandboxed body targets a stored-metadata table, which the runtime then refuses at bind: authoring answers 200 for a hook that never runs #21565
Description
Activity
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:spec·area:access·pm:blocked. Reject at authoring, with the runtime's own predicateTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T08:52Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.Why p2. The runtime floor holds: such a hook never runs, so nothing is exposed. The defect is that authoring answers 200 for a hook that will never fire, and the author learns it only from a server log.
Routing:
domain:spec, aHookSchemarefinement, not a check in the save door.- The family predicate already lives in the spec's kernel (
isStoredMetadataBodyObject), and that is what the runtime boundary judges by. - A schema refinement refuses at every authoring and publish door at once: the metadata save, compile and an artifact's parse. That is Prime Directive 12.
- ⛔ No second list of tables.
- ⛔ No door-local check in
protocol.ts.
Ruling (#21520's ruling A, at authoring):
- A hook carrying a sandboxed
bodywhose target is a family table is refused at parse, with the runtime's prescription (change metadata through the metadata API). - Platform code hooks are outside it, as ruling A says.
- A census comes first: every
HookSchema-parsed hook inpackages/**,examples/**andobjectstack-ai/hotcrmthat targets a family table. Zero is expected. Any hit is named before the refinement lands. - It is a narrowing of an authoring surface, so
Clause-②: yes (narrowing)applies, with the contract kit.
Pins:
PUT /api/v1/meta/hook/NAMErefuses such a hook with the prescription;- a hook on an ordinary object saves as before;
- the runtime's bind refusal stays as the floor.
Why blocked: it consumes the boundary that PR #21563 (#21520) establishes.
Blocked-by: #21520
Generated by Claude Code
- The family predicate already lives in the spec's kernel (
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardspriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 3, 2026 objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsTriage: unlocked —
pm:blocked→pm:queue. The boundary this card consumes has landedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T09:58Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.- The blocker closed. [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 closed
completedat 2026-10-03T09:03Z, when PR fix(runtime)!: an app-authored body may not bind a hook to, or write, the stored-metadata tables (#21520) #21563 merged asbd70706713. The runtime's bind refusal for a body hook on the family's tables is onmain. - Re-derived: no other blocker.
- Unchanged: my grade
5967370168. It is aHookSchemarefinement through the same family predicate the runtime boundary judges by, with the runtime's prescription and a census first. ⛔ No second list of tables. ⛔ No door-local check.
Generated by Claude Code
- The blocker closed. [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 closed
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01YDt3PzwfrkuFzUBF89WPmM
Account:os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21565-hook-family-target-refused
Worktree:objectstack-issue-21565
Domain:domain:spec
Seat:domain:spec#2
File surface (atorigin/main44072fc2b9), per triage5967370168(unchanged at the unlock5968011459):- A census comes first: every
HookSchema-parsed hook inpackages/**,examples/**andobjectstack-ai/hotcrmthat targets a stored-metadata table. Zero is expected. Any hit is named before the refinement lands. packages/spec/src/data/hook.zod.ts,HookSchema: a hook carrying a sandboxedbodywhoseobjectnames a family table is refused at parse, with the runtime's prescription (change metadata through the metadata API). The predicate is the one the runtime boundary judges by,isStoredMetadataBodyObject(packages/spec/src/kernel/metadata-type-redaction.ts). ⛔ No second list of tables. A platform code hook stays outside it, as [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's ruling A says. What the wildcard'*'does is read from the runtime's own bind (body-runner.ts) and matched, not widened.- Pins:
PUT /api/v1/meta/hook/NAMErefuses such a hook with the prescription; a hook on an ordinary object saves as before; the runtime's bind refusal stays as the floor. - The narrowing kit: an ADR-0087 semantic entry, the regenerated artifacts, and one changeset.
- ⛔ No door-local check in
packages/metadata-protocol/src/protocol.ts(open PRs fix(cloud-connection): an install-local uninstall runs the protocol's registered uninstall cleanups #21512 and fix(objectql): an in-process engine verb refuses an object name the registry does not resolve (#21516) #21545 hold it). ⛔ No runtime change. - Stop on breach and explain in the report.
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceilingCONTRACT_REVIEW_TIER). A narrowing ofpackages/spec/src/**, so the at-tier contract review runs as a separate isolated subagent after the PR opens.
Clause-②: yes (narrowing)
Thread-read: 5968011459
Serial constraints cleared: at 2026-10-03T10:57Z, no open PR touchesdata/hook.zod.tsorkernel/metadata-type-redaction.ts(file lists of all open PRs read). Two of this seat's PRs touchmigrations/registry.ts: PR #21578 (#20234 stage 11, its hand-written parts) and #21464 stage 3 (in flight, a step-18 entry). This card adds a step-18 entry too, so whichever lands later mergesmainand regenerates the registry regions.Seat terms: p2, under the maintainer's terms (「只处理 p0 p1 p2」, widened by
5952442849),batch 3. The PR isFixes #21565.
Generated by Claude Code
- A census comes first: every
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21565, "status": "done", "branch": "claude/issue-21565-hook-family-target-refused", "pr": "https://github.com/objectstack-ai/objectstack/pull/21592", "session": "session_01YDt3PzwfrkuFzUBF89WPmM — subagent of the dispatching PM session; the relay read the same id from the container", "premise_still_valid": true, "summary": "HookSchema now refuses, at parse, a hook that carries a body and whose object names sys_metadata or sys_metadata_history (the string, or any list member), with one custom issue per family member at object / object.N whose message names the table and ends with the runtime's prescription word for word. The refusal is a superRefine on HookSchema judged by the kernel's own isStoredMetadataBodyObject, so the metadata save door (422 INVALID_METADATA), defineHook, defineStack (STACK_SCHEMA_INVALID 422), os validate and an artifact's parse (the JSON-stage hook now derives with safeExtend) all refuse it; a code-handler hook and a wildcard body hook stay accepted, byte for byte. No second table list, no check in protocol.ts, no runtime change. Kit: D3 entry hook-body-stored-metadata-target-refused at protocol 18 (no D2, no tombstone), regenerated registry and export-origins, dropped-refinements ledger +5 sites, @objectstack/spec minor changeset with BREAKING banner, Clause-② yes (narrowing) and the ADR-0087 registered marker. Draft PR #21592 at 478e364795, assigned os-tesla; the local worktree and its node_modules were removed after the push.", "tests": "All at head 478e364795 unless noted, each under scripts/pm/os-verify-lock.sh. Pins: spec src/data/hook-body-stored-metadata-target.test.ts 18/18; door src/protocol.invalid-metadata-422-face-inventory.test.ts 18/18; runtime floor (stored-metadata-body-boundary.test.ts, stored-metadata-body-boundary.pin.test.ts, sandbox/body-runner.test.ts) 51/51 unchanged, the composed pin printing 'metadata door save of a runtime-authored family-table hook answered: 422' over real HTTP as the administrator. Package suites at 8c605b9785 (same spec, metadata-protocol and runtime sources as the head; the later merge brought only rest, cloud-connection and dogfood files): @objectstack/spec build exit 0, test exit 0 (607 files, 17970 passed, 1 todo), typecheck exit 0 (test layer: 'compiles under packages/spec/tsconfig.test.json'); @objectstack/metadata-protocol test exit 0 (207 files passed, 3 skipped), typecheck exit 0 (its tsconfig includes the test file). Closure build turbo runtime^... + metadata-protocol^... 29/29 tasks. Ablation: see ablation_A7.", "mcp_calls": "0", "api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot] (each one POST /repos/objectstack-ai/objectstack/dispatches plus the executor's request): (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, draft, #21592, body read back byte-identical (12182 bytes); (2) label-write.mjs --assign os-tesla: POST /repos/objectstack-ai/objectstack/issues/21592/assignees, read back matches; (3) post-stamped.mjs --comment=21565: POST /repos/objectstack-ai/objectstack/issues/21565/comments (this report). Zero label writes (the dispatch names none; skip-changeset does not apply: @objectstack/spec publishes). Plus git push x2 (8c605b9785, 478e364795), not REST. Reads: REST pulls list and per-PR file lists for A8.", "open_questions": [], "out_of_scope_findings": [], "gates": [ "dispatch-gates --repo objectstack-ai/objectstack --commands at 478e364795: 95 derived; each run with its exit code on disk, all exit 0; --ran: '95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED (a DERIVED zero — all 95 recorded an exit code and none of them is 3)'. (At 8c605b9785 the same union read 94 run + 1 NOT-MEASURED: check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET; at 478e364795 it measured and passed: '106 published require entry point(s) across 66 package(s) load'.)", "@objectstack/spec build — exit 0", "@objectstack/spec check:generated — exit 0 — 'All 15 generated artifacts are up to date'", "@objectstack/spec check:liveness — exit 0 — '990 live · 1 experimental · 1 live-elsewhere · 110 dead · 14 planned = 1116 classified'", "@objectstack/spec check:migration-registry — exit 0 — 'registry.ts is current (357 semantic, 246 retired-key, 218 retired-def)'", "@objectstack/spec check:api-surface — exit 0 — '@objectstack/spec public API surface + factory signatures unchanged'", "@objectstack/spec check:export-origins — exit 0 — 'export-origins/ is current: 5338 exports across 19 entry points'", "@objectstack/spec check:skill-refs — exit 0 — '9 generated files in sync with packages/spec'", "@objectstack/spec test — exit 0 — 607 files, 17970 passed; typecheck — exit 0", "metadata-protocol door pin file — exit 0 — 18 passed; package test exit 0, typecheck exit 0", "runtime bind-refusal tests — exit 0 — 3 files, 51 passed", "pnpm check:dts-closure — exit 0 — '71 built package(s) swept - 169/169 declared declaration file(s) present'", "node scripts/check-adr-0087-registration.mjs --base origin/main — exit 0 — '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition' [BREAKING+clause-②-narrowing] registered hook-body-stored-metadata-target-refused", "node scripts/check-changeset-no-major.mjs --base origin/main --event (the PR body) — exit 0 — 'LEVEL AXIS: this PR declares clause-② yes (narrowing), and no package whose packages/**/src/** it moves is graded patch'", "node scripts/check-empty-changeset.mjs --base origin/main — exit 0", "pnpm check:doc-authoring — exit 0 — '17343 customer-facing string(s) across 1253 spec sources clean'", "pnpm check:nul-bytes — exit 0 — 'OK (scanned 9955 text file(s) ... no raw ASCII control bytes)'", "NOT MEASURED locally, declared to CI: the CI-only steps dispatch-gates names (Test Core shards, Dogfood, Temporal Conformance, Build Core, the workspace type-check programs)" ], "line_budget": "11 files, +573 / -31 = 604 changed lines vs merge base 901e7cf13a; under the 5000 human-merge threshold. No governed surface touched (no skills/**, .claude/**, docs/adr/**, AGENTS.md).", "files_changed": [ ".changeset/21565-hook-body-stored-metadata-target-refused.md (+34)", "packages/spec/src/data/hook.zod.ts (+79 -1)", "packages/spec/src/kernel/stored-metadata-body-objects.ts (+57, new leaf)", "packages/spec/src/kernel/metadata-type-redaction.ts (+12 -26)", "packages/spec/src/stack.zod.ts (+7 -1)", "packages/spec/src/migrations/entries/semantic/18.hook-body-stored-metadata-target-refused.ts (+47)", "packages/spec/src/migrations/registry.ts (+59, generated regions)", "packages/spec/export-origins/kernel.json (+2 -2, generated)", "packages/spec/dropped-refinements.baseline.json (+6 -1, hand-edited ledger)", "packages/spec/src/data/hook-body-stored-metadata-target.test.ts (+206)", "packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts (+64)" ], "census_A1": "TypeScript AST walk of every object literal with both object and events, target resolved through string, list and const. objectstack packages/** + examples/** at 44072fc2b9: 8140 files, 317 hook declarations (24 outside tests), 3 family-target hits; at 26a45c595c (this run's resume head): 319 declarations, the same 3 hits. All 3 are in packages/runtime/src/stored-metadata-body-boundary.pin.test.ts, the runtime floor's own refusal fixtures, each with a body: line 108 (boundary_hook_on_metadata, 'sys_metadata'), line 109 (boundary_hook_on_history, ['sys_metadata_history']), line 290 (boundary_authored_on_metadata, 'sys_metadata'). No authored app hook targets a family table. 108 and 109 reach the runtime raw through AppPlugin and are never HookSchema-parsed (git grep -c HookSchema packages/runtime/src: 0 hits; control HookBodySchema: hits in body-runner.ts); 290 is sent through PUT /api/v1/meta/hook/NAME, is HookSchema-parsed at the door, and that test records the status without asserting it (now 422; the test still passes). hotcrm at 94668373f2 (fetched origin/HEAD, unchanged across the run): 568 files, 44 declarations (40 outside tests), 0 hits. Unresolved targets (computed values): 17 in objectstack (runtime boundary unit tests with variables, code-handler objectql tests, generated translation forms, hook.zod.ts itself), 4 in hotcrm (code handlers on objectName, a test helper iterating hook.object) — none a family table. Control: a scratch fixture with a const-held 'sys_metadata' target and a list member 'sys_metadata_history' was found 2/2; hotcrm's account hook (crm_account) was not a hit.", "refused_set_A2": "Read on main (boundary landed bd70706713): packages/runtime/src/sandbox/body-runner.ts:297 returns early when the hook has no body (a code handler never reaches the boundary); :310-311 asks storedMetadataBodyHookBindingRefusal and throws BEFORE HookBodySchema.safeParse at :313, so every body form is refused, not only JS; packages/runtime/src/stored-metadata-body-boundary.ts:56-58 reads the target as a list (string = list of one), :66-67 filters it by isStoredMetadataBodyObject, :80-82 refuses when any member is a family table (one member refuses the whole hook); body-runner.ts:328 and :341: the wildcard '*' names no family table, binds, is logged once, and its body is never run for a family table's event (not refused). The refinement (packages/spec/src/data/hook.zod.ts:121-139, attached :478) refuses exactly that set: body present AND some member of object satisfies isStoredMetadataBodyObject. Not wider (code handler on a family table and wildcard body hook parse unchanged), not narrower (expression body refused; ['*','sys_metadata'] refused at object.1).", "predicate_and_message_A3": "Import: hook.zod.ts imports isStoredMetadataBodyObject from ../kernel/stored-metadata-body-objects. Importing it from kernel/metadata-type-redaction.ts caused no cycle and the bundle tree-shook it, but it pulled that module's closure (the datasource credential derivation and, through shared/metadata-collection.zod.ts, the conversion chain) into the hook schema's source graph; check:skill-refs went stale ('objectstack-data 7 core + 44 deps' vs 37), i.e. the generated skills/objectstack-data/references/_index.md would have gained seven unrelated schema pointers, and skills/** is a Tier H governed surface. Smallest fix, taken: the set and the predicate moved unchanged into the import-free leaf src/kernel/stored-metadata-body-objects.ts; metadata-type-redaction.ts re-exports both, so every importer and @objectstack/spec/kernel keep the very same objects (a pin asserts toBe identity); only export-origins/kernel.json's origin path changed (generator output; a re-run rewrote 0 shards). The table list exists once. Message: the runtime keeps its sentence in a module-private const PRESCRIPTION (stored-metadata-body-boundary.ts:41) that packages/spec cannot import and no shared constant exists, so the spec message repeats it word for word ('Change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`, the metadata protocol), where it is validated and its provenance is recorded. Elevation (`runAs`, a system context) does not change this.'), with no tracker number, after a lead naming the table and saying the runtime refuses to register the hook. Field path: object, or object.N.", "pins_A5": "spec: each family table refused at object with the prescription (code custom); JS and expression bodies both refused; list members at object.N, one member enough, ['*','sys_metadata'] at object.1; defineHook throws; getMetadataTypeSchema('hook') (what the door validates against) refuses; defineStack throws code STACK_SCHEMA_INVALID status 422 at hooks.1.object; ArtifactStagePackageBodySchema refuses at hooks.0.object while a lowered string handler on a family table parses and an inline callable is still refused; controls byte-identical (JSON.stringify of parse output = input + schema defaults) for a code handler on each family table, a wildcard body hook, ordinary string and list targets; the D3 entry registered with no conversion and no tombstone. Door (beside the existing save-refusal sections, section 6): saveMetaItem as usr_admin with writeFace meta-envelope, the gate PUT /api/v1/meta/hook/NAME reaches, answers code INVALID_METADATA status 422, issue [custom, object|object.1], message containing the prescription, nothing stored; control: the same body hook on an ordinary object is stored. protocol.ts untouched. Runtime floor: unchanged and green.", "ablation_A7": "Two-file mutation from committed head 8c605b9785 via scripts/ablation-replace.mjs (--hold, 7 anchors, each verified on disk) under a trap restore against absolute paths: the .superRefine(refuseBodyOnStoredMetadataTarget) attach removed (hook.zod.ts blob 6cc51014 -> 35c8ea56, marker 1 -> 0) and dropped-refinements.baseline.json reverted in the same leg (mutated blob 8b331234 == the file at 152e36aacc^), because the build's gen:schema holds that ledger exact. Mutate: spec build exit 0; ablation-dist-preflight --absent: 'marker absent from all 228 built files'; spec pin 10 failed / 8 passed (all 10 refusal cases red, all 8 controls green); door pin 3 failed / 15 passed (the three family-target saves red; the ordinary control and the file's other sections green). Restore: ablation-replace --restore on both files; blobs == HEAD (6cc51014, 412d577d), git diff HEAD empty, tree clean; rebuild exit 0; preflight 'marker present in 20 built files' and 'working tree clean against HEAD'; spec pin 18/18, door pin 18/18. A first attempt (hook.zod.ts alone) was VOID and is discarded: the build refused at the dropped-refinement ledger (the 5 sites no longer dropped), dist kept the refinement (preflight --absent exit 1), so its green door run measured the old artifact; its src-resolved spec pin had gone 10 red / 8 green. A second attempt aborted before any build because one anchor's replacement was a prefix of the anchor (the tool's rise check refused it; the tree was restored, blob == HEAD); the anchor was widened and the third attempt is the one reported.", "semver": "@objectstack/spec minor (BREAKING narrowing under the launch-window convention), judged by: check-changeset-no-major --event 'LEVEL AXIS: this PR declares clause-② yes (narrowing), and no package whose packages/**/src/** it moves is graded patch' / 'direction arm: narrowing — a BREAKING change; during the launch window it ships minor'; check-adr-0087-registration '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition' [BREAKING+clause-②-narrowing] registered. The PR body's first line is the closing keyword for this card, the second 'Clause-②: yes (narrowing)'.", "serial_A8": "origin/main merged twice (cc645f2385, then 901e7cf13a: #21580 rest and #21581 cloud-connection, no overlap with these packages; rest and cloud-connection rebuilt; pins and the gate union re-run at 478e364795). #21578 (stage 11) had landed before the first merge and its registry regions verified current. Open PRs re-read just before pr_create: #21590 (#21464 stage 3, draft) touches packages/spec/src/migrations/registry.ts and packages/spec/dropped-refinements.baseline.json; it has not landed, so whichever of #21590 / #21592 lands second merges main, regenerates the registry regions with gen:migration-registry and re-measures the dropped-refinement ledger, keeping both changes. No other open PR touches hook.zod.ts, the kernel modules, stack.zod.ts, export-origins/kernel.json or the door test file (the version-packages PR's file list was read to 100 entries; #20388's could not be read through the proxy).", "deviations": [ "resumed after a container restart at 26a45c595c. Kept the one uncommitted change, packages/spec/export-origins/kernel.json, after re-running its generator: fresh spec build exit 0, check:generated exit 0 with check:export-origins green against it, and gen:export-origins rewrote 0 shards (blob a0e8901802 unchanged); committed as 8c605b9785. My first commit attempt, before that confirmation, was refused by the permission classifier and was not retried until the generator evidence existed. Re-ran, because batch A had no recorded exit beyond its first step: spec build + check:generated, the closure build, all pins, the full spec and metadata-protocol suites, the ablation, and the derived gate union (twice: at 8c605b9785 and again at 478e364795 after main moved).", "A1 stop condition: the census has 3 hits carrying a body, all the runtime floor's own refusal fixtures in one test file (none an authored app hook; two never pass HookSchema; the third is a door probe the test records without asserting). The killed run had already committed the refinement; I read the hits as outside the condition's intent and name them here rather than stopping. If the PM reads the condition literally, the refinement commit d200459269 is the one to hold.", "A5 door pin is at the protocol's saveMetaItem gate (where the door's existing save-refusal tests live), not an HTTP harness; the over-HTTP reading is the runtime composed pin's printed 422.", "Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md, not the harness's model-named trailer." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21565, "addendum": "merge round after #21590 (#21464 stage 3) landed as 958cfe29be", "status": "done", "branch": "claude/issue-21565-hook-family-target-refused", "pr": "https://github.com/objectstack-ai/objectstack/pull/21592", "head": "bab28dbaea (was 478e364795, the reviewed head); merge of origin/main d2e687fdff through scripts/pm/os-regen-merge.sh, pushed without force", "merge": "One text conflict, in packages/spec/dropped-refinements.baseline.json, on the measured counts only: both sides kept (main's ui/ObjectFormProps site and this branch's five hook sites), counts stacked to 218 schemas / 659 sites; the build's own generator re-measured it: '659 refinement site(s) across 218 published schema(s) ... all declared in dropped-refinements.baseline.json'. registry.ts merged without a conflict; gen:migration-registry rewrote it byte-identical (358 semantic, 246 retired-key, 218 retired-def), so no regeneration commit was needed. os-regen-merge step 2 kept the branch's export-origins/kernel.json (main did not move it). #21590's entry, implementation and pins are byte-identical to main in the merge.", "files_changed_beyond_main": "Of the 22 files that differ between 478e364795 and bab28dbaea, 20 are byte-identical to main's side; the 2 that are not: packages/spec/dropped-refinements.baseline.json (+7 -2 vs the reviewed head: main's entry and the stacked counts) and packages/spec/src/migrations/registry.ts (+65 -0 vs the reviewed head: main's step-18 entry in the generated regions). The branch delta vs main is unchanged: 11 files, +573 / -31.", "gates": [ "@objectstack/spec build (turbo metadata-protocol^... closure, spec a cache miss) — exit 0", "gen:migration-registry — exit 0, tree clean after", "@objectstack/spec check:generated — exit 0 — 'All 15 generated artifacts are up to date'", "check:migration-registry — exit 0 — 'registry.ts is current (358 semantic, 246 retired-key, 218 retired-def)'", "check:export-origins — exit 0 — 'export-origins/ is current: 5338 exports across 19 entry points'", "check:api-surface — exit 0 — 'public API surface + factory signatures unchanged'", "spec pin hook-body-stored-metadata-target.test.ts — exit 0 — 18 passed", "door pin protocol.invalid-metadata-422-face-inventory.test.ts — exit 0 — 18 passed", "@objectstack/spec test — exit 0 — 608 files, 18010 passed, 1 todo" ], "api_writes": "1 — this comment, POST /repos/objectstack-ai/objectstack/issues/21565/comments through the fleet-write relay; plus git push (bab28dbaea), not REST. No PR, label or assignee write.", "mcp_calls": "0" }
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsLanded: PR #21592 →
9e9d693468.Fixes #21565closed this carddomain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5968496458· 2026-10-03T15:44Z- Landed: PR feat(spec)!: HookSchema refuses a hook body that targets a stored-metadata table, with the runtime's prescription (#21565) #21592 went through the merge queue as
9e9d693468, with one parent (f6b7520839).Fixes #21565closed this card ascompleted. - Blob check against the ACCEPTed head
bab28dba(contract review PASS5969922144on478e3647; ACCEPT5970419770, which reads the merge round's delta): 11 of 11 files are blob-equal. - Delivered:
HookSchemarefuses, at parse, a hook that carries abodyand whoseobjectnames a stored-metadata table (as a string or as any list member). It uses the runtime boundary's own predicate,isStoredMetadataBodyObject, now declared once in an import-free kernel leaf. The message ends with the runtime's prescription word for word.- The refusal applies at every authoring door: the metadata save (
PUT /api/v1/meta/hook/NAMEanswers422 INVALID_METADATA),defineHook,defineStack,os validateand an artifact's parse. - A code-handler hook and a wildcard body hook are unchanged, and the runtime's bind refusal stays as the floor.
- Release:
@objectstack/specminor, BREAKING, D3 entryhook-body-stored-metadata-target-refused.
- The refusal applies at every authoring door: the metadata save (
- The census: no authored app hook targets a stored-metadata table, in this repository or in hotcrm (
94668373f2). pm:dispatchedand the assignee come off in this act.
Generated by Claude Code
- Landed: PR feat(spec)!: HookSchema refuses a hook body that targets a stored-metadata table, with the runtime's prescription (#21565) #21592 went through the merge queue as
Filing gate: ① a defect with a named position, a
findingof class (c): authoring accepts what the runtime refuses.reach:was measured at a public door by the [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 dev, through PR fix(runtime)!: an app-authored body may not bind a hook to, or write, the stored-metadata tables (#21520) #21563's composed pin (os-dev report5967051141on [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520, out-of-scope finding 1).domain:cliseat,session_016GiHYRmLSNWTfbX9gVQkpz. ⛔ Not a claim.domain:specor the metadata door (domain:engine). ⛔ Classes, doors and roles only.What happens (measured)
With PR #21563 (#21520, ruling A
5965059068), an app-authored hook whose sandboxedbodytargets one of the two stored-metadata tables is refused at bind. The runtime logs it at error, and the hook never fires.But the metadata door's save,
PUT /api/v1/meta/hook/:nameas the administrator, answered 200 for exactly such a runtime-authored hook. Authoring accepts a hook the runtime will never honour, and the author learns it only from a server log.Why
Ruling A is enforced at the runtime's binding point (
packages/runtime/src/sandbox/body-runner.ts,hookBodyRunnerFactory) and at the write seam. Neither the hook's schema (HookSchema'sobjecttarget,packages/spec) nor the metadata door's save validation (packages/metadata-protocol/src/protocol.ts) consults the boundary.AGENTS.md Prime Directive 12 says to fix it at the producer and reject it at authoring or publish. The binding refusal is the runtime's floor. The authoring refusal is missing.
Direction (⛔ not a ruling)
The authoring door refuses a hook whose sandboxed
bodytargets either stored-metadata table, loudly, with the same prescription the runtime gives (change metadata through the metadata API). It uses the same predicate the runtime boundary uses. ⛔ No second list of tables.The position is either a
HookSchemarefinement (domain:spec) or the save door's validation (protocol.ts, held today by open PR #21473). Pin:PUT /api/v1/meta/hook/:namerefuses such a hook, and a hook on an ordinary object saves as before.Serial: this card depends on PR #21563 landing, because it consumes that boundary's predicate.
Dedupe
MCP
search_issues, repo-scoped, open and closed: 「metadata door accepts hook on sys_metadata refused at bind not at save authoring」 gave 10 hits. #21520 (open) is the runtime half and this finding's source; the rest are closed and on other subjects (#21470, #21412, #21207, #20863, #7990, #7529, #9066). None covers this.Dedupe words: metadata door accepts hook on sys_metadata; hook body target stored-metadata table refused at bind not at save; runtime-authored hook family table 200 then refused.
Generated by Claude Code