Skip to content

[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

@objectstack-fleet

Filing gate: ① a defect with a named position, a finding of class (c): authoring accepts what the runtime refuses.

What happens (measured)

With PR #21563 (#21520, ruling A 5965059068), an app-authored hook whose sandboxed body targets 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/:name as 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's object target, 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 body targets 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 HookSchema refinement (domain:spec) or the save door's validation (protocol.ts, held today by open PR #21473). Pin: PUT /api/v1/meta/hook/:name refuses 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

Activity

  1. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:spec · area:access · pm:blocked. Reject at authoring, with the runtime's own predicate

    Triage 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, a HookSchema refinement, 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 body whose 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 in packages/**, examples/** and objectstack-ai/hotcrm that 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/NAME refuses 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

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    and removed on Oct 3, 2026
  3. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: unlocked — pm:blocked → pm:queue. The boundary this card consumes has landed

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-03T09:58Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01YDt3PzwfrkuFzUBF89WPmM
    Account: os-tesla (the seat's linked user as GET /user answers 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 (at origin/main 44072fc2b9), per triage 5967370168 (unchanged at the unlock 5968011459):

    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling CONTRACT_REVIEW_TIER). A narrowing of packages/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 touches data/hook.zod.ts or kernel/metadata-type-redaction.ts (file lists of all open PRs read). Two of this seat's PRs touch migrations/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 merges main and regenerates the registry regions.

    Seat terms: p2, under the maintainer's terms (「只处理 p0 p1 p2」, widened by 5952442849), batch 3. The PR is Fixes #21565.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  6. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  7. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21592 → 9e9d693468. Fixes #21565 closed this card

    domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5968496458 · 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 #21565 closed this card as completed.
    • Blob check against the ACCEPTed head bab28dba (contract review PASS 5969922144 on 478e3647; ACCEPT 5970419770, which reads the merge round's delta): 11 of 11 files are blob-equal.
    • Delivered: HookSchema refuses, at parse, a hook that carries a body and whose object names 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/NAME answers 422 INVALID_METADATA), defineHook, defineStack, os validate and 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/spec minor, BREAKING, D3 entry hook-body-stored-metadata-target-refused.
    • The census: no authored app hook targets a stored-metadata table, in this repository or in hotcrm (94668373f2).
    • pm:dispatched and the assignee come off in this act.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions