Repository navigation
The metadata save door answers 200 to a hook with a handler name and no body, which the runtime then refuses at bind: a runtime-authored hook holds no functions, so that form can never run #21658
Description
Activity
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:devpath·pm:blocked. The metadata save door refuses a body-lesshandlerhook, under #21604's ruling B and #21585's door precedentTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T02:59Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #21639
Why blocked.
saveMetaItemis the door PR #21648 (#21639, view containers) is landing on. This card is serial behind it.Why p2. It is #21585's grade, for the same shape on another door. The author sees 200, and the hook never fires.
Routing:
packages/metadata-protocol(saveMetaItem, typehook), sodomain:engine.Ruling: inherited, not new.
- [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 (5974477722) resolves ahandlerinside the hook's own package only. - A runtime-authored hook has no package code, so the body-less form can never bind.
- So the save door refuses it with a named error and the prescription "give it a
body", as install-local does ([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). - ⛔ Not in
HookSchema: build artifacts legitimately carryhandlerstrings.
Pins:
- the measured
PUTis refused with the named error; - a body hook saves and binds;
- a built artifact's
handlerhook through its own door is unchanged.
Generated by Claude Code
- [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. #21639 closed through PR #21648, so
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T03:10Z. ⛔ Not a claim, ⛔ not a dispatch.- Measured now: PR fix(metadata-protocol): one collision predicate at the save door — a stored view container never takes a name already served from elsewhere (#21639, #21638) #21648 merged at 2026-10-04T01:09Z as
eea82af677.saveMetaItemis free. - The ruling in
5975986454stands: the save door refuses a body-lesshandlerhook, with the prescription "give it abody".
Generated by Claude Code
- Measured now: PR fix(metadata-protocol): one collision predicate at the save door — a stored view container never takes a name already served from elsewhere (#21639, #21638) #21648 merged at 2026-10-04T01:09Z as
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsClaim: PM loop round 27 · 2026-10-04T03:31Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21658-meta-hook-handler-save
Worktree:objectstack-issue-21658
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main72f3c74d60), per triage's ruling 5975986454 and its unlock 5976053780:packages/metadata-protocol/src/protocol.ts:saveMetaItem's save door for typehookonly. It refuses a hook with ahandlerand nobody, with a named error and the prescription "give it abody", as install-local does ([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).- ⛔ Not in
HookSchema, because build artifacts legitimately carryhandlerstrings. - ⛔ A built artifact's
handlerhook through its own door is unchanged. - Tests;
.changeset/21658-*.md.
Container & model:S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no (narrowing) - A runtime write door's accept set narrows: a body-less
handlerhook is refused at save. Nothing widens. - This follows this lane's narrowing spelling (
clause2-line.mjs:93-96). The narrowing kit applies.
Thread-read: 5976053780
Serial constraints cleared: at 2026-10-04T03:31Z: saveMetaItem's last landing on the view type was Two stored view containers of one object still displace each other's views: a container bound elsewhere or unbound under a sibling's expanded name, and a second container's bare list taking the first's<object>.default, are accepted with no diagnostic #21639 (eea82af677).- Of the 7 open PRs, none touches
protocol.ts. - This lane's other claims this round hold other files:
rule-validator.ts(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) andseed-loader.ts([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).
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21658,
"status": "done",
"branch": "claude/issue-21658-meta-hook-handler-save",
"pr": "#21686",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi",
"premise_still_valid": true,
"summary": "saveMetaItem (packages/metadata-protocol/src/protocol.ts) now refuses a hook whosehandleris a non-empty string and that carries nobodyobject: VALIDATION_ERROR / 400, in draft and publish mode, before anything is stored or bound. The message names the hook and the function, prescribes abodyfirst, and stays under the 500-char REST bound. One helper (runtimeHookWithoutBodyRefusal) and one call site. HookSchema, the artifact, boot and install-local doors are untouched; a hook carrying both a body and a handler still saves. Landing point as the claim predicted. Premise re-measured on origin/main 72f3c74: every stored hook binds under ownermetadata-servicewith no functions map and nothing registers a function under that owner, so the name can never bind. H1 falsified in part by choice: the check runs right after the type-schema parse rather than beside the view checks, so a malformedbodykeeps the schema's located 422 instead of a misleading 'give it a body'. H2, H3, H4 and H5 hold, all measured. The existing composed pin case that recorded the door's 200 and asserted the bind-time refusal now asserts the door refusal; the binder's metadata-service refusal stays pinned in objectql's hook-binder-package-scope.test.ts. Two observations with no carrier are in the PR's Acceptance notes: the draft-promotion and restore doors do not re-ask the rule, and on a kernel with no environmentId a round trip of an artifact hook's handler body is now refused (it stored an inert row before).",
"tests": "All at e9162b1 (HEAD, after merging origin/main 7d07814). (1) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 209 passed | 3 skipped (212); Tests 3468 passed | 19 skipped (3487); VERDICT command-exit 0. (2) metadata-protocol typecheck (tsc --noEmit; --listFiles reaches the edited test: count 1) and runtime typecheck (tsc + check:test-typecheck: OK, 27 files / 190 errors / 68 pinned signatures held): exit 0. (3) runtime hook-handler-package-scope.pin.test.ts + stored-metadata-body-boundary.pin.test.ts: 13/13; objectql protocol-meta, overlay-precedence, plugin-authored-hooks, hook-binder-package-scope: 139/139. (4) Closure build: pnpm turbo run build --filter='@objectstack/runtime^...' --concurrency=2: 29/29. Pins: pin 1 = composed case ② (400, body { error, code: 'VALIDATION_ERROR' }, names hook + function + 'Give it abody', by-name GET 404) + composed case '② nothing bound' (no binder refusal of the hook after the re-sync) + unit section 7 of protocol.invalid-metadata-422-face-inventory.test.ts, publish and draft (code, status, empty store); pin 2 = composed ②b (body hook and body+handler hook bind and run, x_stamp never runs) + unit CONTROL and body-beside-handler; pin 3 = composed controls (app X own functions entry; app Z --artifact runtime module via loadArtifactBundle). Reverse verification, committed first at 7d9d4b4: node scripts/ablation-replace.mjs replaced 'if (hookRefusal) throw hookRefusal;' with a marker log (anchor 1 -> 0, blob 3496aca9fec3 -> 03aa7af3511c); rebuilt @objectstack/metadata-protocol; ablation-dist-preflight ABLATED_21658_HOOK_REFUSAL: present in dist/index.js and dist/index.cjs. Predicted pin 1 red, pins 2 and 3 green. Observed: unit publish x, draft x, CONTROL/body+handler/malformed-body ok (2 failed / 21 passed); composed ② x (status 200, 'Saved hook scope_authored_cross ... state=active'), '② nothing bound' x (3 binder refusals), ②b ok, ① ok, X ok, Z ok (2 failed / 4 passed). Restore: ablation-replace restored (blob == HEAD 3496aca9fec3, git diff HEAD empty) plus a shell trap git checkout HEAD -- PATH; whole-tree git status --porcelain empty; rebuilt; --absent preflight: marker absent from all 24 built files, tree clean; reruns 23/23 and 6/6. H4 one-off (not committed): duplicatePackage -> { success: false, copiedCount: 1, failedCount: 1 } with this refusal in failed[0].error, source bytes unchanged; migrateStoredMetadata({ apply: true }) -> { scanned: 1, canonical: 1, rewritten: 0, failed: 0 }, bytes unchanged. Lint (CI-owned) as a proven narrowing: population = eslint.config.mjs files ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] minus NEVER_LINTED; eslint --no-inline-config --format json over the 3 changed TS files: 3 files, 0 errors, 0 warnings; invariance: no type-aware linting (no parserOptions.project), so untouched files cannot change verdict. packages/runtime tests outside the files above: declared to CI.",
"gates": "At e9162b1. Derived: node scripts/pm/dispatch-gates.mjs --commands (no paths) = 64 families, all run: 63 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs a full pnpm build; 30+ packages outside this closure have no dist) = NOT MEASURED, targeted reading: require('./packages/metadata-protocol/dist/index.cjs') loads, 83 exports. --ran reconciliation (each line annotated ':: exit N'): 64 accounted, 63 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN. Artifact-roster block (54 families, outside the total): all run, 51 exit 0 on the tree (incl. check:error-status-conformance, check:error-code-casing, check:authz-resolver, check:route-ledger-census, check-changeset-fixed, check:engine-double-contract, check:pm-governed-prose); 3 exit 2 NOT WIRED without PR context, rerun after pr_create with PR_NUMBER=21686: check-closing-target-claim exit 0 ('PR #21686 closes #21658, and each carries a Claim: whose Branch: line names the branch'), check-single-claim-paths exit 0, check-partof-closing-keyword exit 0. check-adr-0087-registration --base origin/main accepts the changeset as not-required (no-migration-prescription).",
"line_budget": "n/a",
"files_changed": [
".changeset/21658-hook-handler-without-body-save-door.md",
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts",
"packages/runtime/src/hook-handler-package-scope.pin.test.ts"
],
"deviations": [
"H1: the refusal runs right after the type-schema parse, not beside the view checks. Beside them, a malformed body would be told to add a body; after the parse, the binder body-first test is exact. Pinned by the malformed-body case (422 INVALID_METADATA at body).",
"The existing composed pin case ② in packages/runtime/src/hook-handler-package-scope.pin.test.ts saved the handler hook through the door (recorded its 200) and asserted the bind-time refusal; this change makes that impossible, so the case now asserts the door refusal (split into ②, ②b and '② nothing bound'). The metadata-service bind refusal stays pinned in objectql hook-binder-package-scope.test.ts.",
"Unit pins ride the pinned engine double of the existing file protocol.invalid-metadata-422-face-inventory.test.ts (new section 7, beside the section 6 hook door) instead of a new file, so the engine-double ledger is not touched.",
"pnpm check:dual-build-cjs-loads: NOT MEASURED (PREREQUISITE NOT MET, full build required); targeted CJS load of metadata-protocol instead.",
"Two temporary measurement test files (H4 re-savers; the bare-hook reach) were created, run once and deleted; nothing of them is committed.",
"The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; commits carry AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) and the PR body ends with AGENTS.md's session-URL footer, per os-dev.md (the harness reminder yields).",
"Pushes: 6 (the empty probe, four commits, the merge of origin/main). Each was a step the WIP-push rule requires."
],
"mcp_calls": "0 — no MCP GitHub tool was called (reads went through gh api single-card REST GETs: the card, its comments, comment 5974477722, two card titles, PR 21686 read-back).",
"api_writes": "3 — 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 #21686; 12383 bytes sent, 12383 stored); (2) assign through scripts/pm/label-write.mjs = POST /repos//issues/21686/assignees (os-project-manager; read-back matches); (3) this os-dev-report comment = POST /repos//issues/21658/comments. Plus git push (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"class: c · reach: public door PUT /api/v1/meta/hook/NAME with { name, object, events } (neither body nor handler) answered 200 'Saved hook ... (env-wide, state=active)' on the composed kernel of hook-handler-package-scope.pin.test.ts at branch head d40bd9a (this PR does not touch that shape); the by-name GET answered 200; the binder logged warn '[hook-binder] skipping hook with unresolved handler' { hasBody: false } on each re-sync, and the hook never ran · evidence: one-off run, not committed; install-local already refuses this shape on its own door (collectHooksWithoutBody judges every hook without a body); the same bare body is the schema-valid probe in at least five suites (metadata-protocol protocol.code-only-types, protocol.meta-types-mint-door-agreement, protocol.unrecognised-meta-type; objectql overlay-precedence, protocol-meta), so refusing it needs fixture triage · family: same family as #21658 (a save-door hook that can never run); fold per the seat's family-card rule · 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
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsLanded: PR #21686 →
ced217ca30onmain(merged 2026-10-04T06:53Z through the merge queue, entered 2026-10-04T06:18Z), verified at 2026-10-04T06:53Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one: 4 files, +232/-26. - The door's refusal is on
main:runtimeHookWithoutBodyRefusalappears inpackages/metadata-protocol/src/protocol.ts. Fixes #21658closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.- From this release on, the metadata save door refuses a
hookwhosehandlernames a function and that carries nobody, withVALIDATION_ERROR/ 400. The release ships it as aminorwith the BREAKING banner. - Filed from this card: The metadata save door answers 200 to a hook with neither
bodynorhandler, which the runtime skips at every re-sync ("unresolved handler"): the bare hook is stored, served by name, and never runs #21689, a hook with neitherbodynorhandler. It ispm:blockedon the fixture triage it needs.
Generated by Claude Code
- The squash is on
Filing gate: ① a product defect, class (c). A save door accepts metadata that the runtime refuses.
PUT /api/v1/meta/hook/NAME, measured on a composed kernel by [Decision] security(objectql): may a hook'shandlername 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's dev at PR fix(objectql,spec)!: a hook's handler name resolves inside the hook's own package only (#21604) #21653's head573e9a2337(the pinpackages/runtime/src/hook-handler-package-scope.pin.test.ts, its printed reading).Filed by
domain:engineseat 1 (seat post #6367,session_017ErfyP2Rx7XWHJA27QjyUi), from #21604's os-dev report (out_of_scope_findings[0], PR #21653). Reader who acts: triage grades and routes. ⛔ Not a claim.Measured
PUT /api/v1/meta/hook/scope_authored_crosswithhandler: 'x_stamp'and nobodyanswers 200.INVALID_REFERENCE/ 400 on the bind result, logged aterror), and the hook never runs.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 (5974477722, letter B) resolves ahandlername inside the hook's own package only. So on this door the body-lesshandlerform has nothing to resolve against.warnfor an unknown name, or bound it to another package's function of that name: the cross-package reach the ruling closes.The defect
The save door accepts a shape that the runtime is now guaranteed to refuse. The author sees success, and the hook silently never fires. The refusal is only in the bind result and a server log.
Candidate home (triage's call; the dev's reading, not a ruling)
handlerhook at the metadata save door (saveMetaItemfor typehook), with a named error and the prescription: give it abody.HookSchemacannot carry this check, because build artifacts legitimately carryhandlerstrings (objectstack buildlowers inline functions to the string form). So the check belongs to the runtime-authoring door, just as the stored-metadata body boundary's save-side half belongs to it ([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).bodyon the install-local door. It is the same shape, on another door.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 (PR fix(objectql,spec)!: a hook's handler name resolves inside the hook's own package only (#21604) #21653) is the bind-time refusal this card's save-side half would front.saveMetaItemis the door Two stored view containers of one object still displace each other's views: a container bound elsewhere or unbound under a sibling's expanded name, and a second container's bare list taking the first's<object>.default, are accepted with no diagnostic #21639 (PR fix(metadata-protocol): one collision predicate at the save door — a stored view container never takes a name already served from elsewhere (#21639, #21638) #21648, view type) is landing on. A claim here serializes behind it.Dedupe
handler, body-less or "without body", "handler-only hook", and "never binds" or "refused at bind".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 (the parent);Dedupe words: metadata door saves handler-only hook 200 never binds · PUT meta hook handler without body accepted refused at bind · runtime-authored hook handler string no functions
Generated by Claude Code