Repository navigation
The metadata save door answers 200 to a hook with neither body nor handler, which the runtime skips at every re-sync ("unresolved handler"): the bare hook is stored, served by name, and never runs #21689
Description
Activity
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:devpath·pm:blocked. The save-door hook family's closing card: one predicate, a runtime-authored hook with nobodyis refused, as install-local already judgesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T05:51Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #21658
Why blocked. PR #21686 (#21658, in flight) adds the handler-only refusal at the same door (
saveMetaItem, typehook). This card generalises it, so it lands after it. ⛔ Not folded into that PR mid-flight.Why p2. It is #21658's grade: the author sees 200, the hook is served by name, and it never runs.
Why it is the closing card. This is the third shape of "a hook stored where it can never run": install-local (#21585), the save door with
handleronly (#21658), and the save door with neither field (this card). ⛔ There is no fourth card per shape.Ruling: one predicate.
- A runtime-authored hook holds no package code, so after [Decision] security(objectql): may a hook's
handlername bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604 B nothing but abodycan run on this door. - The save door refuses any hook with no
body, with a named error and the prescription "give it abody". That is the same judgement install-local'scollectHooksWithoutBodyalready makes. - The metadata save door answers 200 to a hook with a
handlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658's handler-only check becomes a case of it. ⛔ No two predicates for one rule. - ⛔ Not in
HookSchema: build artifacts legitimately carryhandlerstrings, as ruled on The metadata save door answers 200 to a hook with ahandlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658.
Fixtures. The five suites that use a bare hook as their schema-valid probe move to a body-carrying hook:
metadata-protocol:protocol.code-only-types,protocol.meta-types-mint-door-agreementandprotocol.unrecognised-meta-type;objectql:overlay-precedenceandprotocol-meta.
Each keeps what it was probing for. ⛔ No probe is deleted.
Acceptance, the enumeration pin: body (saves), handler-only (refused), neither (refused), and a built artifact's
handlerthrough its own door (unchanged).
Generated by Claude Code
- A runtime-authored hook holds no package code, so after [Decision] security(objectql): may a hook's
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 4, 2026 objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsTriage: unlocked. #21658 closed through PR #21686, so
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T06:55Z. ⛔ Not a claim, ⛔ not a dispatch.- Measured now: PR fix(metadata-protocol)!: the save door refuses a hook that names a function in
handlerand carries nobody(#21658) #21686 merged at 2026-10-04T06:53Z asced217ca30. The save door's handler-only refusal is onmain. - The ruling in
5977077883stands. There is one predicate: a runtime-authored hook with nobodyis refused, and the handler-only check becomes a case of it. The five probe suites move to a body-carrying hook, and the enumeration pin covers all four shapes.
Generated by Claude Code
- Measured now: PR fix(metadata-protocol)!: the save door refuses a hook that names a function in
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsUnlock: triage's 5977495602 (
Blocked-by: #21658closed; PR #21686 →ced217ca30). At this claim's read the card still carriedpm:blocked, and this claim's label write removes it. The seat re-derived atorigin/main1db5322ba2: no new blocker (serial constraints below).Claim: PM loop round 27 · 2026-10-04T06:56Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21689-hook-no-body-save-door
Worktree:objectstack-issue-21689
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main1db5322ba2), per triage's grade and ruling 5977077883:packages/metadata-protocol/src/protocol.ts:runtimeHookWithoutBodyRefusal(the The metadata save door answers 200 to a hook with ahandlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658 helper) becomes the ONE predicate: a hook with nobodyobject is refused at the save door, with a named error and the prescription "give it abody". The metadata save door answers 200 to a hook with ahandlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658's handler-only case becomes a case of it.- ⛔ No second predicate.
- ⛔ Not in
HookSchema. - A built artifact's
handlerthrough its own door is unchanged.
- The five probe suites move to a body-carrying hook, and each keeps what it probes. ⛔ No probe is deleted.
metadata-protocol:protocol.code-only-types,protocol.meta-types-mint-door-agreementandprotocol.unrecognised-meta-type;objectql:overlay-precedenceandprotocol-meta.
- Tests: the enumeration pin (body saves; handler-only refused; neither refused; an artifact's
handlerthrough its own door unchanged)..changeset/21689-*.md.
Container & model:S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no (narrowing) - The save door refuses one more shape it used to answer 200. No accepted input widens, and no key, export or error code is added.
Thread-read: 5977077883
Serial constraints cleared: at 2026-10-04T06:56Z: - Of the open PRs, only fix(metadata-protocol): the read envelope's lock / editable / deletable report the write doors' locked-base verdict #21693 touches
protocol.ts, in another region (above). - This lane's other in-flight work holds other regions: A system-context write skips value-shape validation for readonly fields: a seed's malformed readonly datetime (
'yesterday', an unresolvedcelenvelope) is stored verbatim, while the same value on a non-readonly field is refused #21663 holds the readonly value-shape check (PR fix(objectql)!: a system write's readonly value is judged for its shape — a seed's malformed readonly datetime is refused, never stored (#21663) #21695), [finding] On a walled showcase deployment every organization/create replays fixed-id sys_business_unit seeds that collide on the global id: 9 SeedLoader errors and an unresolved parent reference per new organization #21665 holdsseed-loader.ts(PR fix(metadata-protocol): give each organization its own row identity on a per-organization seed replay #21688, queued), and [finding] On a walled posture a create that names no organization_id answers droppedFields [organization_id: readonly], a field the caller never sent (the insert analogue of #8093) #21682 holdsengine.ts'sinsertsnapshot.
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21689,
"status": "done",
"branch": "claude/issue-21689-hook-no-body-save-door",
"pr": "#21706",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi",
"premise_still_valid": true,
"summary": "Triage's one-predicate ruling (5977077883) is carried out. runtimeHookWithoutBodyRefusal in packages/metadata-protocol/src/protocol.ts keeps its name, its one call site in saveMetaItem and its one envelope (VALIDATION_ERROR / 400, after the type-schema parse, before the authoring gate and every write, in draft and publish mode). Its predicate widened from 'a non-empty handler string and no body object' to 'no body object', the judgement install-local's collectHooksWithoutBody makes; #21658's handler-only refusal is now a case of it, and its message is byte-identical. A hook with neither field (or an empty handler) gets: "Invalid hook: 'NAME' carries nobody, so it has nothing to run. Give it abody(...), which is stored with the hook. A hook saved through the metadata API ships with no code package, so itsbodyis the only code it can run." (282 chars plus the name). HookSchema is untouched. Seven probe suites (the five named plus metadata-protocol protocol.save-receipt-wording and objectql metadata-validation-sweep, found by measurement) moved to a body-carrying hook; no probe deleted, no assertion changed. Enumeration pin on the real door (composed kernel, PUT /api/v1/meta/hook/NAME): body saves (②b), handler-only refused (②), neither refused (new ②c), artifact handler hooks through their own door unchanged (X and Z controls), plus nothing bound for both refused shapes. Mechanism hypotheses: H1-H5 all hold (H5 for what this repository holds; the cloud AI build agent not measured). Landing point as the claim predicted. PR #21693 landed before this PR opened and is merged in (clean merge, its regions untouched). The worktree is removed as the step after this comment.",
"tests": "Final head 15402d9 (after merging origin/main 8843505). (1) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 210 passed | 3 skipped (213); Tests 3578 passed | 19 skipped (3597); VERDICT command-exit 0. (2) pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 --project local: Test Files 372 passed (372); Tests 7455 passed (7455); VERDICT command-exit 0. (3) runtime hook-handler-package-scope.pin.test.ts + stored-metadata-body-boundary.pin.test.ts: 14/14. (4) typecheck exit 0: metadata-protocol tsc --noEmit (--listFiles reaches all 5 edited test files, 1 hit each); objectql and runtime tsc + check:test-typecheck OK (test layers under tsconfig.test.json include src//*, debt ledgers held). (5) closure build pnpm turbo run build --filter='@objectstack/runtime^...' --concurrency=2: 29/29; spec check:generated: all 15 artifacts up to date. Measurement before the probe moves (predicate widened, probes untouched): metadata-protocol 7 failed / 3461 passed in 4 files; objectql 6 failed / 7446 passed in 3 files. ABLATION (fix committed first at 0c32c32): ablation-replace restored the old handler-only guard behind marker ABLATED_21689_NEITHER (anchor 1 -> 0, blob 3059168d5703 -> 237d2559c48b, shell trap restore on EXIT INT TERM); metadata-protocol rebuilt; ablation-dist-preflight found the marker in dist/index.js and dist/index.cjs. Predicted pin 3 red, pins 1 2 4 green. Observed: unit (src) 3 failed (all three section 8 cases), 15 passed incl. all of section 7; composed (dist) 2 failed (②c got 200 'Saved hook scope_authored_bare'; nothing-bound got 3 binder skips via skipsOf), 5 passed (①, X, Z, ②, ②b). Restore: blob == HEAD 3059168d5703, git diff HEAD empty, whole-tree git status --porcelain empty; rebuilt; --absent preflight: marker in none of 24 built files, tree clean; reruns 26/26 and 7/7. The first ablation attempt was a no-op: its replacement re-contained the anchor, ablation-replace refused it (anchor 1 -> 1), ran nothing and proved the restore; re-run with the body test re-spelled (same truth table). H3 one-off harness (not committed): duplicatePackage over a bare row + a body row = success false, copiedCount 1, failedCount 1, this refusal in failed[0].error, source bytes unchanged; migrateStoredMetadata apply = scanned 1, canonical 1, rewritten 0, failed 0, bytes unchanged.",
"gates": "Ran twice in full: at 623b4a0 and again at the final head 15402d9; figures are the final head's. Derived: node scripts/pm/dispatch-gates.mjs --commands (no paths) = 66 families (identical list on both heads), all 66 run, all 66 exit 0 (check:dual-build-cjs-loads measured: 106 require entry points across 66 packages load; at the first head it exited 3 PREREQUISITE NOT MET). --ran reconciliation with recorded exit codes: 66 accounted for, 66 run, 0 NOT-MEASURED (derived zero), 0 UNRUN, exit 0. Artifact-roster block (54 families, outside the total): all 54 run; 51 exit 0 (incl. check:error-status-conformance, check:error-code-casing, check:authz-resolver, check:route-ledger-census, check-changeset-fixed, check:engine-double-contract); 3 exit 2 NOT WIRED without PR context, then rerun with PR #21706's context, all exit 0: check-closing-target-claim ('PR #21706 closes #21689, and each carries a Claim: whose Branch: line names claude/issue-21689-hook-no-body-save-door'), check-partof-closing-keyword (no contradiction), check-single-claim-paths (modifies none of the 1 declared path). Symbol-anchor sweeps, all exit 0: check:adr-symbol-anchors (2167 anchors / 140 records), check:scripts-symbol-anchors (3760 / 282 scripts), check:spec-docblock-symbol-anchors (4867 / 1861 spec sources), check:adr-anchors OK. Lint narrowed (CI owns pnpm lint): population eslint.config.mjs files ['/.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']; eslint --no-inline-config --format json over the 10 changed TS files = 10 files, 0 errors, 0 warnings; invariance: no type-aware linting (no parserOptions.project), so no untouched file's verdict can move. check-adr-0087-registration --base origin/main accepts the changeset (not-required, no-migration-prescription). Not run locally, declared to CI: the remaining packages/runtime suite, the 4 type-check lanes, the 5 path-scheduled CI jobs, the 11 wide-population families.",
"line_budget": "n/a",
"files_changed": [
".changeset/21689-hook-no-body-save-door.md",
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/protocol.code-only-types.test.ts",
"packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts",
"packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts",
"packages/metadata-protocol/src/protocol.save-receipt-wording.test.ts",
"packages/metadata-protocol/src/protocol.unrecognised-meta-type.test.ts",
"packages/objectql/src/metadata-validation-sweep.test.ts",
"packages/objectql/src/overlay-precedence.test.ts",
"packages/objectql/src/protocol-meta.test.ts",
"packages/runtime/src/hook-handler-package-scope.pin.test.ts"
],
"census": {
"H2_bare_hook_probe_suites": "7 (the 5 named + metadata-protocol protocol.save-receipt-wording + objectql metadata-validation-sweep), measured by full-suite runs with the predicate widened; grep of runtime, rest, plugins/, services/*, cli, verify, qa, examples/** for door saves found only runtime's two pin tests, which already carry bodies",
"H3_stored_bare_hooks_reachable": "0 (examples/** and packages/qa/** seed no sys_metadata hook rows; zero type: 'hook' items)",
"H4_saveMetaItem_hook_callers": "forwarding callers only (REST PUT /meta/:type/:name, dispatcher meta save, migrateStoredMetadata, duplicatePackage); fixed-type callers save flow, app or permission; AppPlugin, loadArtifactBundle, install-local and boot make zero calls",
"H5_first_party_neither_emitters": "0 in this repository (os meta register forwards the author's file; create/scaffold templates reach the artifact door; packages/mcp has no hook tool; Studio pin ab18797215 unchanged since #21658's census); cloud AI build agent NOT MEASURED"
},
"deviations": [
"Seven probe suites moved, not five: metadata-protocol protocol.save-receipt-wording and objectql metadata-validation-sweep also used a bare hook as a schema-valid probe and went red under the widened predicate (H2 asked to report any beyond the five). Each keeps its probe; no assertion changed.",
"objectql protocol-meta's NOT_OVERRIDABLE probe was green under the widened predicate (the provenance gate runs first) and was moved anyway, so the refusal it measures can only be the provenance gate's. Registry-seeded items there (provenance scenery, not door saves) keep their bytes.",
"The first ablation attempt was a no-op refused by ablation-replace (the replacement re-contained the anchor); it ran nothing and proved its restore. The second attempt re-spelled the body test with the same truth table. Both are in the PR body.",
"origin/main moved twice: 234d1d8 merged at 623b4a0, then 8843505 (carrying PR #21693, as the order requires) merged at 15402d9; every test and gate family was rerun on 15402d9. After that, main gained two spec-only commits (f623e2f, 16d241a) before the PR opened; not merged (no overlap with this diff).",
"The second gate batch (derived 35-66 at 15402d9) outran the 10-minute foreground cap and the harness moved it to the background; I waited for it in the foreground (tail -f piped to grep -m1) and every one of the 32 commands recorded its exit code.",
"The three PR-context roster gates were rerun after pr_create with PR_NUMBER=21706 and a read token from gh auth token, through NODE_OPTIONS=--use-env-proxy; reads only.",
"Commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line; the two merge commits carry git's default message."
],
"mcp_calls": "0 — no MCP GitHub tool was called. Reads went through gh api single-card REST GETs (issue 21689 and its comments, issue 21658's comments for the sibling report, PR 21706 read-back) and the PR-context gates' own REST reads.",
"api_writes": "3 REST writes, each one repository_dispatch to the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft PR #21706; 15470 bytes sent, 15470 stored, identical); (2) scripts/pm/label-write.mjs assign = POST /repos//issues/21706/assignees (os-project-manager; read-back matches; size/m was added by another actor and preserved); (3) scripts/pm/post-stamped.mjs = POST /repos//issues/21689/comments (this os-dev-report). Plus 7 git pushes (not REST): the empty-branch probe, 4 commits, 2 merges.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed · the draft-promotion and restore doors (publishMetaItem, rollbackMetaItem, revertCommit) do not re-ask this rule, as #21658's PR recorded for the handler form; it now covers the neither form too (a pre-change draft or history version can become an active bare row, which the runtime skips at re-sync)",
"carrier: none · noted, not filed · the binder's skip line reads 'skipping hook with unresolved handler' for a hook that has no handler; no stored row of that shape can be minted through this door any more"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsLanded: PR #21706 →
7fd2c34de8onmain(merged 2026-10-04T09:33Z through the merge queue, entered 2026-10-04T08:59Z), verified at 2026-10-04T09:33Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one: 11 files, +202/-54. - The one predicate is on
main:runtimeHookWithoutBodyRefusalinpackages/metadata-protocol/src/protocol.tscarries the neither-shape sentence "so it has nothing to run". Fixes #21689closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.- From this release on, the runtime save door refuses every
hookwith nobody, withVALIDATION_ERROR/ 400. The handler-only refusal The metadata save door answers 200 to a hook with ahandlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658 shipped is now one case of that rule, with an unchanged message. The release ships it as aminorwith the BREAKING banner. - This closes the hook family, as triage designated: install-local ([finding] os package install accepts a package whose hook uses only the deprecated function-name handler (no body), answers "installed", and the hook never fires: install-local drops it with a server-side warn only #21585), handler-only (The metadata save door answers 200 to a hook with a
handlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658) and neither (this card). - Census on record: 7 probe suites moved to a body-carrying hook, with no assertion changed. There are 0 reachable stored bare hooks and 0 first-party emitters in this repository. The cloud AI build agent is NOT MEASURED.
- Noted, not filed (no public-door reach measured): the draft-promotion and restore doors do not re-ask the rule, and the binder's skip line says "unresolved handler" for a hook that has none.
Generated by Claude Code
- The squash is on
- added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a product defect, class (c). A save door accepts metadata the runtime never runs.
PUT /api/v1/meta/hook/NAMEwith{ name, object, events }, carrying neitherbodynorhandler. Measured on the composed kernel ofpackages/runtime/src/hook-handler-package-scope.pin.test.tsby The metadata save door answers 200 to a hook with ahandlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658's dev at PR fix(metadata-protocol)!: the save door refuses a hook that names a function inhandlerand carries nobody(#21658) #21686's branch headd40bd9a46f. This was a one-off run and is not committed.Filed by
domain:engineseat 1 (seat post #6367,session_017ErfyP2Rx7XWHJA27QjyUi), from #21658's os-dev report (out_of_scope_findings[0], PR #21686). Reader who acts: triage grades and routes. ⛔ Not a claim.Measured
Saved hook ... (env-wide, state=active).GETanswers 200.warn[hook-binder] skipping hook with unresolved handler { hasBody: false }. The hook never runs.handlerand carries nobody(#21658) #21686 (The metadata save door answers 200 to a hook with ahandlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658) refuses the sibling shape, ahandlername with nobody. It deliberately leaves this one alone.Why it is its own card
handlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658: a save-door hook that can never run.metadata-protocol:protocol.code-only-types,protocol.meta-types-mint-door-agreementandprotocol.unrecognised-meta-type;objectql:overlay-precedenceandprotocol-meta.collectHooksWithoutBodyjudges every hook that has nobody.Candidate home (triage's call)
bodynorhandlerat the save door, with a named error and the prescription "give it abody".handlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658's family as a closing card.Dedupe
handlername and nobody, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658 (the sibling, handler without body);handlerand carries nobody(#21658) #21686, its fix;handlername bind to a function another package registered (the engine-wide fallback HookSchema.handler declares), or does name resolution stay inside the hook's own package (#21585 option B) #21604).Dedupe words: hook neither body nor handler saved 200 never runs · meta hook no body no handler accepted skipped unresolved handler · bare hook skeleton save door refusal
Generated by Claude Code