Repository navigation
[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
Description
Activity
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:cli·area:devpath·pm:queue. #21489's C for hooks: install-local refuses a hook it cannot run, loudlyTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T12:54Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. The door answers "installed" for a hook that never fires, and the only trace is a server log. Nothing is exposed, but declared behaviour is silently missing.
Routing:
packages/cloud-connection, install-local, beside #21489's job refusal, sodomain:cli.Ruling: the same C, applied to hooks.
- install-local refuses a package carrying a hook in the deprecated function-name
handlerform, with nobody. The refusal is a named error at install that names thebodyform as the remedy, the same shape as the enabled-job refusal. ⛔ No silent drop. ⛔ No warn-only. - The
os start --artifactboot, which carries the runtime module, is unchanged. - Measure first, the card's own note: can a handler-form hook on install-local bind to a same-named function another app registered? If it can, that is a different class, cross-app code binding. The claim stops and reports, and triage re-grades. ⛔ Never land the refusal over an unmeasured binding path.
Pins:
- the measured package is refused at
os package install, with a non-zero exit and the named error; - the body-hook control installs and fires as before.
findingcomes off in this act.
Generated by Claude Code
- install-local refuses a package carrying a hook in the deprecated function-name
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratepriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 3, 2026 objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsPointer for this card's claim: two measure-first items from PR #21584's contract review. Same family, not filed
domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· 2026-10-03T14:37Z · ⛔ Not a claim, ⛔ not a dispatchThe contract review of record on PR #21584 (
5970154924, PASS) escalated two traps on the install-local door. Both are code-read only, with no public-door reach measured, so the filing gate keeps them unfiled. They belong with this card's family, "install-local refuses what it cannot run, loudly" (triage5969366675). The claim on this card measures each one once:- An off-spec job
body. The door judges "has abody", not "thebodycan bind". A job whosebodyis an L1 expression, or carriesbody.timeoutMs, passes the door's job refusal (whichos validatealready refuses). The binder then warns server-side and schedules nothing: installed 200, never run. If that is measured, it is the same silent shape this card closes for hooks, and the claim reports it for triage to route. - Job-name collision across packages. A job is identified by its
namealone onIJobService. A later install-local package that declares a job name another package already scheduled replaces that job, and the first package's job is gone with no answer at the door. If that is measured, the claim reports it with its reach, and the seat files it.
Neither changes this card's ruled scope (hooks) unless triage rules so.
Generated by Claude Code
- An off-spec job
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (unblocked by #21489's landing, PR #21584 →
6c5697dffb, where the job refusal it sits beside lives)
Session:session_016GiHYRmLSNWTfbX9gVQkpz
Account:os-bill(the seat's linked user asget_meanswers it; the card's assignee)
Branch:claude/issue-21585-handler-hook-refusal
Worktree:objectstack-issue-21585
Domain:domain:cli
Seat:domain:cli#1
File surface, per triage's ruling5969366675(#21489's C applied to hooks):packages/cloud-connection/src/marketplace-install-local-plugin.ts: the install refuses a package that carries a hook in the deprecated function-namehandlerform with nobody. The refusal sits beside the enabled-job refusal (jobsWithoutBody/describeJobsWithoutBody), with the same shape. It is a named error at install that names thebodyform as the remedy. ⛔ No silent drop, ⛔ no warn-only. Rehydrate follows the job refusal's precedent.- The runtime binder's hook half, only if the judgement must be shared there, as
collectJobsWithoutBodyis for jobs. - Pins:
- the measured package is refused at
os package install, with a non-zero exit and the named error; - the body-hook control installs and fires as before;
os start --artifactis unchanged.
- the measured package is refused at
.changeset/.
Measure first, which triage made a stop condition: can a handler-form hook on install-local bind to a same-named function that another app registered (the engine's function-resolution fallback)? If it can, the claim stops and reports, and triage re-grades. ⛔ Never land the refusal over an unmeasured binding path.
Also measured once, from PR fix(runtime,cloud-connection)!: a job's sandboxed body is scheduled on every door, and install-local refuses an enabled job with no body (#21489) #21584's contract review (5970174066): an off-spec jobbodythat installs and never runs, and a job-name collision across packages. Each is reported with its reach. ⛔ Neither is fixed here unless triage rules so.
⛔ Nopackages/specedit. ⛔ No error-code ledger edit: an existing code that fits is used, as the job refusal'sVALIDATION_ERRORwas. (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default (opus). A refusal on a public install door, with a cross-app binding question that must be measured first.
Clause-②: yes (narrowing). install-local refuses a package it used to accept. BREAKING,fix(...)!:, the ADR-0087 marker andminor. The contract review of record is owed before enqueue.
Thread-read: 5970174066
Serial constraints, other open PRs read 2026-10-03T15:07Z onorigin/main6c5697dffb:- No open PR touches this surface.
- PR fix(spec,runtime,cli)!: the in-memory engine is no longer a boot store — every boot door refuses it and names SQLite instead #21598 (
os dev --database-driver memoryboots, then every data read answers 503 SERVICE_UNAVAILABLE — the package registry's CREATE TABLE sys_packages goes through driver-memory's no-op execute() #21492/spec(data/driver): withdrawmemory/mingo/in-memoryfrom the driver vocabulary's selection face, contract-only kept (the declaration half of #21492's retirement) #21572, this seat, in review) is disjoint: the store resolvers, the parity pin and the driver docs.
domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· 2026-10-03T15:07Z
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21585,
"status": "needs_decision",
"branch": "claude/issue-21585-handler-hook-refusal",
"pr": null,
"session": "session_016GiHYRmLSNWTfbX9gVQkpz",
"premise_still_valid": true,
"summary": "A1's stop condition fired: a handler-form hook on install-local DOES bind to a same-named function another app registered, so nothing was implemented and no PR exists (the branch is the empty probe branch at origin/main 6c5697d). Measured on base 6c5697d through the public door: a host booted withos start --artifact host.json, whose runtime module exports functionshared_stamp; thenos package installof package com.example.crossy, whose beforeInsert hook nameshandler: 'shared_stamp'with no body, beside a body-hook control. Install exit 0; the insert answered 201 with statusHOSTFBODY: the HOST's function ran on the package's own object, then the control. After a restart on the same home, the rehydrated entry answeredHOSTFBODYagain. Unit reading on the built dist: one ObjectQL engine; bindAppArtifactHandlers for app X with functions { shared_stamp } (bound under app:com.example.appx), then for app Y with a handler-only hook naming it. Y binds hooks:1 with no warn, and Y's beforeInsert runs X's function (statusX_FN; resolveFunctionEntry('shared_stamp').packageId = app:com.example.appx). Mechanism (code-read): packages/objectql/src/hook-binder.ts resolveHandler tries the bundle'sfunctions, then falls back toengine.resolveFunction(h)(:322). That is an engine-wide map keyed by bare name (engine.ts :3658private functions = new Map, :3885); the stored packageId is never consulted on lookup.HookSchema.handler's doc DECLARES the fallback (packages/spec/src/data/hook.zod.ts :244, 'anything engine.registerFunction(name, fn) added'). A2 is reproduced as the card filed it, with a handler that resolves nowhere: package com.example.orphanz (handler: 'nowhere_fn') installs with exit 0; the insert answers statusBODY(the control only); the only trace is a server WARN[hook-binder] skipping hook with unresolved handler. So the card's premise holds (the door installs the shape silently), and for a colliding name it understates the defect: the hook fires another app's code. Why this changes A4: the job precedent ('an older entry rehydrates and its handler-only job is warned, not run') holds for jobs only because scheduleAppArtifactJobs resolveshandleragainst the bundle's own functions, with no engine fallback. A hook rehydrated by install-local binds cross-app today, as measured, so following the precedent for hooks needs a binder change: the decision below. Producers of the shape:os build's lowerCallables (packages/cli/src/utils/lower-callables.ts) lowers an inline hook handler tohandler: HOOK_NAME. When body extraction fails, the callable stays only in the runtime module, so a built code app installed through install-local carries exactly this shape. It binds cross-app if and only if a booted app registered a function under that name. Engine-function producers aredefineStack({ functions })apps and every--artifactruntime module.",
"tests": "No code change, so no suite or lint run is owed. Measurements, all on base 6c5697d, afterpnpm --workspace-concurrency=2 --filter '@objectstack/cli^...' buildunder os-verify-lock (VERDICT command-exit 0, held 301 s). (1) Public-door probe: a scratchpad tsx script reusing packages/cli/test/helpers/serve-process.ts (CLI, TSX, childEnv), not committed, run under os-verify-lock (VERDICT command-exit 0, held 80 s). Readings: cross.install exit 0, cross.insert {status 201, recordStatus 'HOSTFBODY'}; orphan.install exit 0, orphan.insert {201, 'BODY'}; server WARN[hook-binder] skipping hook with unresolved handler {hook: orphan_z_handler_hook, handler: nowhere_fn, hasBody: false}. (2) Restart probe, same shape, run under os-verify-lock (VERDICT command-exit 0, held 36 s): install 0, hot {201, 'HOSTFBODY'}, restart {201, 'HOSTFBODY'}. (3) Unit reading: a node script importing packages/objectql/dist and packages/runtime/dist. It printed {bindX: {owner: app:com.example.appx, functions: 1}, bindY: {owner: app:com.example.appy, hooks: 1}, yInsertStatus: 'X_FN', fnOwner: 'app:com.example.appx'}. (4) A6, same boot as (1): L1-expression job body: install exit 0, server WARN[BodyRunner] invalid job.body shape — the job is NOT scheduled. body.timeoutMs job: install exit 0, 0 rows after 6 s (its body writes one row per 1 s run), server WARNjob 'tmo_job_body' carries body.timeoutMs ... NOT scheduled. Job-name collision: see out_of_scope_findings. No ablation: nothing landed to ablate (A7 not reached).",
"mcp_calls": "0",
"api_writes": "1: this os-dev-report comment, POST issues/21585/comments, through scripts/pm/post-stamped.mjs (fleet relay). Plus one git push of the empty probe branch, which is not a REST write. GitHub reads went throughgh api(issue 21585 and its comments; comments 5964305303 and 5968972501; issue 21489's comments).",
"gates": "None run: no diff.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackanswered exit 2: 'this branch changes nothing against origin/main (merge base 6c5697d) — nothing to derive'. Fullpnpm lint, the suites, the integration pins and--ranreconciliation are NOT MEASURED, reason: A1 stop, no code.",
"line_budget": "n/a",
"deviations": "None against the dispatch. A1 said 'stop, push nothing beyond the probe branch, needs_decision', and that was followed. A3–A5 and A7 were not executed, because they were downstream of the stop. A3 code-read only: a hook has no on/off switch (HookSchema tombstonesenabledandactivewith guidance), so 'a disabled hook' is not a category. A hook carrying bothbodyandhandlerbinds its body (resolveHandler is body-first), so it is not in the refused set.",
"files_changed": [],
"open_questions": [
{
"question": "Q1 (triage re-grade). Cross-app code binding on install-local is real, hot and after a restart. What scope does this card carry?",
"options": [
"A. The ruled refusal, plus the rehydrate half in the ONE binder. The runtime binder gainscollectHooksWithoutBody(every hook with nobody; the job half'scollectJobsWithoutBodyarrangement). The install route refuses those hooks with 422 VALIDATION_ERROR, beside the job refusal. bindAppArtifactHandlers takes an install-local-only option that withholds those hooks fromql.bindHookswith a warn, so an older entry rehydrates with the hook warned and NOT bound. That closes the cross-app path on this door, hot and after a restart. Theos start --artifactboot is unchanged. No objectql, spec or ledger edit. Clause-② yes (narrowing), as claimed. Cost: about the size of PR #21584's job half.",
"B. A, plus owner-scoped name resolution in objectql: resolveHandler's engine fallback answers only functions registered under the hook's own packageId. This closes the class on every door: config compositions, and the metadata-service bind of PUT /meta hooks. It edits packages/objectql (stay-out for this dispatch) and contradicts HookSchema.handler's declared doc (hook.zod.ts :244), so it needs a spec change. It would break any Studio-authored hook that names an app function by design. That is its own card and decision.",
"C. The ruled refusal only; rehydrate unchanged. Legacy entries keep binding cross-app after the upgrade, and the PR body could not truthfully claim the job precedent for hooks."
],
"recommendation": "A. Real business need, measured: the producer of the refused shape is real (os buildlowerCallables keepshandler: HOOK_NAMEwhen body extraction fails). Deliberate reliance on cross-app binding has zero in-repo producers:git grep -nE \"handler: *['\\\"]\" -- 'examples/**'at 6c5697d gives 4 hits, all jobs or functions, 0 hooks. Long-term soundness: the binder already owns per-door facts for jobs; A is one judgement shared by the door and the binder, while B rewrites a declared spec behaviour and needs its own ADR-level ruling. Guarding against AI mistakes: A turns both the silent drop and the silent cross-binding into a named refusal with thebodyremedy at install. No scope sprawl: A reuses the job half's mechanism and code, and adds no gate and no code. B is scope growth with no measured pull."
},
{
"question": "Q2 (triage grade). Does the class change the card's grade? A package's metadata can run the host app's function on the package's own object events, with package-controlled input: a confused-deputy shape for marketplace content.",
"options": [
"A. Keepbugp2domain:cli. The install needsmanage_metadata, the same capability that authors a handler-named hook through PUT /api/v1/meta/hook (whose metadata-service bind uses the same resolver; code-read, unmeasured). Q1-A closes the install door.",
"B. Re-grade as security-class, p1. The installer is an operator, but a catalog package's author is third-party, and the measured binding needs only a name collision."
],
"recommendation": "A, with Q1-A, on the four axes. The measured reach needs an operator-capability install plus a function-name collision. Q1-A closes this door completely. Raising the grade buys nothing that Q1-A does not deliver, and no-sprawl argues against a second lane. Triage's call: B if triage reads third-party catalog content as the threat model."
}
],
"out_of_scope_findings": [
"class: b · reach: public door. On 6c5697d,os package installexits 0 for a package whose enabled job hasbody.language: 'expression', and for one whose job body carriesbody.timeoutMs. Neither job ever runs: the timeoutMs job, whose body writes one row per 1 s run, wrote 0 rows in 6 s. The only trace is a server WARN from [BodyRunner], 'NOT scheduled'. Contract: JobSchema.body refuses both (packages/spec/src/system/job.zod.ts :262-266: 'an expression (L1) body is refused'; 'body.timeoutMsis not accepted on a job'). Seam: spec:JobSchema.body → runtime:jobBodyRunnerFactory via scheduleAppArtifactJobs; the install door's collectJobsWithoutBody judges that abodyis present, not that it can bind. This is the family of this card ('install-local refuses what it cannot run, loudly'), from contract review 5970174066 item 1, for triage to route into the family's closing card. Dedupe words: install-local job body expression L1 refused never scheduled; body.timeoutMs job install 200 never runs; collectJobsWithoutBody bindability",
"class: a · reach: public door. On 6c5697d, two install-local packages (com.example.dupone, com.example.duptwo) each declare a body job namedshared_job_name, and bothos package installruns exit 0. dupone's job ran (4 rows, then 11) and stopped once duptwo installed: 11 rows, then 11 again 5 s later, against a 1 s interval. duptwo's ran (7 rows). Nothing at the door said so. The registry namespaces the job per package ('Registered job: com.example.dupone:shared_job_name', then com.example.duptwo:shared_job_name); IJobService.schedule keys by bare name and replaces. This is contract review 5970174066 item 2, for the seat to file. Dedupe words: install-local job name collision replaces another package job silently; IJobService schedule bare name across packages; claimJobName last app wins",
"carrier: triage (Q1-B's card if taken), otherwise 承接者:无. Code-read, unmeasured: ObjectQLPlugin's metadata-service hook bind (packages/objectql/src/plugin.ts resyncAuthoredHooksNow, packageId 'metadata-service') resolves ahandlerstring through the same engine-wide function registry. So a hook authored through the meta door can name any booted app's function, which HookSchema.handler's doc declares as intended. Noted, not filed."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsReleased: triage's stop condition fired. A handler-form hook on install-local binds to another app's function →
pm:queue+pm:retriagedomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· read 2026-10-03T15:25Z · ⛔ classes, doors and roles onlyThis releases the claim
5970412711.pm:dispatchedand the assignee come off in this act. The source is the os-dev report5970542332. No code was written and no PR was opened. The branchclaude/issue-21585-handler-hook-refusalsits empty at6c5697dffb.Measured on
main6c5697dffb(triage's measure-first stop condition,5969366675):- Cross-app code binding is real, hot and after a restart. A package installed through install-local, whose hook names a function in the deprecated
handlerform with nobody, binds to a same-named function that another app in the same runtime registered. That other app's code then runs on the package's own object events. The install answers success, and nothing at the door says so. One unit reading on one engine agrees. - Mechanism (read): the hook binder resolves
handleragainst the bundle's own functions first, then falls back to the engine-wide function registry. That registry is keyed by bare name and ignores the owning package. The fallback is declared inHookSchema.handler's doc. - The card's own premise also holds. A handler naming no function at all installs with success and never fires; the only trace is a server warn.
- Why the job precedent does not transfer. A rehydrated handler-only job is warned and not run, because jobs resolve against the bundle alone. A rehydrated handler-only hook binds cross-app today. So "follow the job precedent on rehydrate" needs a binder change for hooks.
- A real producer of the shape:
os build's callable lowering leaveshandler: HOOK_NAMEwhen body extraction fails.
Asked of triage (
pm:retriage), with the dev's four-axis readings in the report:- Q1, scope:
- A. Refuse at install, and have the ONE binder withhold handler-only hooks on install-local rehydrate (an older entry rehydrates with the hook warned and NOT bound). That closes the path on this door. No
objectql, spec or ledger edit. - B. A, plus owner-scoped name resolution in
objectql, which closes the class on every door. It contradictsHookSchema.handler's declared doc, so it needs a spec change, and it is its own decision. - C. The ruled refusal only. Older entries keep binding cross-app.
- The dev recommends A.
- A. Refuse at install, and have the ONE binder withhold handler-only hooks on install-local rehydrate (an older entry rehydrates with the hook warned and NOT bound). That closes the path on this door. No
- Q2, grade: keep
bugp2, or re-grade to thesecurityclass, given the cross-app binding.- The install needs the metadata-management capability, which is the same capability that authors a handler-named hook through the metadata door.
- The dev recommends keeping p2 with Q1-A, and notes that B is right if triage reads third-party catalog content as the threat model.
- That is triage's call; the seat does not rule it.
Also measured, for routing:
- Same family as this card ("install-local refuses what it cannot run, loudly"): a job whose
bodyis off-spec (an L1 expression, or carryingbody.timeoutMs) installs with success and never runs. The only trace is a server warn. The door's job refusal judges only "has abody", not "thebodycan bind".JobSchema.bodyrefuses both shapes. Triage routes it, in this card's closing scope or its own. - Filed as [finding] Two install-local packages that declare the same job name: the second install silently replaces the first package's job, which stops running, and the door says nothing #21602: two install-local packages declaring the same job name. The second install silently stops the first package's job.
Generated by Claude Code
- Cross-app code binding is real, hot and after a restart. A package installed through install-local, whose hook names a function in the deprecated
8 remaining items
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21585,
"status": "done",
"branch": "claude/issue-21585-handler-hook-refusal",
"pr": "#21615",
"session": "session_016GiHYRmLSNWTfbX9gVQkpz",
"premise_still_valid": true,
"summary": "Implements triage 5970803032 (Q1: A, with the off-spec job body folded in) on draft PR #21615, head 1b41b79. The branch was fast-forwarded from 6c5697d to b610eab, then origin/main 9a4182a was merged in; no rebase and no force-push. The install-local door refuses a package carrying a hook with nobody, and one whose enabled job'sbodydoes not bind (an expression body, or abody.timeoutMs). It answers one 422 VALIDATION_ERROR that names every hook and job with its handler or the refused key, plus the remedies (abody, oros start --artifact). Nothing is registered, persisted, bound or scheduled. The code is reused and there is no ledger edit. One judgement lives in one place, the runtime binder:collectHooksWithoutBodymirrors the engine binder's body-first test;collectJobsWithoutRunnableBodyreadsjudgeJobBody, a parse againstJobSchema.shape.body, and the job body factory now binds by that same parse.bindAppArtifactHandlerstakeswithholdHooksWithoutBody, which only install-local passes, so a hook with nobodyin an entry an earlier build installed rehydrates warned by name and NOT bound.os start --artifact,defineStackconfigs and the metadata door are unchanged. The--artifactapp's own handler hook still binds, as pinned.collectJobsWithoutBody(landed by PR #21584, never published, its changeset still pending) is renamedcollectJobsWithoutRunnableBodybecause it now also judges bindability. There is no objectql, spec or ledger edit, and scheduleAppArtifactJobs's scheduling identity is untouched (#21602 is held behind this PR). CI: 12 checks completed with no failure and 19 in_progress at the single read after the PR opened; not waited on, per contract.",
"tests": "At HEAD 1b41b79, after the main merge:pnpm install --frozen-lockfileandpnpm --filter '@objectstack/cli...' buildunder os-verify-lock (VERDICT command-exit 0). @objectstack/runtime: typecheck green (including check:test-typecheck); fulltest318 files, 4495 passed, 19 skipped. @objectstack/cloud-connection: typecheck green; fulltest36 files, 437 passed. @objectstack/cli--project unit: 255 files, 3745 passed. Before the merge, two published-subpath pins exited PREREQUISITE NOT MET (cli not built); they were rerun green afterpnpm --filter @objectstack/cli build. Install-local integration pins on built packages: hooks and jobs, 19 passed; handlers, boot-steps and uninstall-cleanups, 40 passed. The new pin is packages/cli/test/package-install-local-hooks.integration.test.ts, 8 tests over two boots. Ablations through scripts/ablation-replace.mjs in WRAP mode from committed e961c7f; every leg was rebuilt, checked with ablation-dist-preflight, then measured. (a) Door hook refusal. Anchorif (unrunnable.jobs.length > 0 || unrunnable.hooks.length > 0) {, hooks term dropped; blob 83b6de87a2b1 to 64f0fb4337e1. Preflight--absent: the marker was absent from all 6 built files, against 1 hit in the pristine dist. Red: cloud-connection hooks unit 2/6; CLI integration 1/8, where the install exited 0, not 1. (b) Rehydrate withholding. Anchorif (options.withholdHooksWithoutBody) {, plantedString('ABLATED_21585_B'); blob 8879aeae73fd to ee32244b3f9a; marker in dist/index.js and index.cjs. Red: runtime binder unit 1/9; cloud-connection rehydrate unit 1/6; CLI integration 2/8, where the rehydrated record readhostbody, so the cross-app binding returns, and no warn appeared. (c) Job-body bindability. Anchorif (judged.binds) continue;, plantedString('ABLATED_21585_C'); blob 8879aeae73fd to 9e40620a0d48; marker in dist. Red: runtime collector unit 3/26; cloud-connection jobs unit 2/11; CLI integration 2/8. Every restore was proven by the tool (blob equals HEAD, emptygit diff HEAD), then rebuilt, with the preflight in the opposite mode passing and the whole tree clean. Every direction was red, as predicted.",
"mcp_calls": "0",
"api_writes": "3 this round, all through the fleet relay as objectstack-fleet[bot]. (1) pr_create (draft), POST /repos/objectstack-ai/objectstack/pulls, via scripts/pm/fleet-write/dispatch.mjs: PR #21615, body read back byte-identical (10187/10187). (2) label-write--assign os-billon #21615, POST issues/21615/assignees, via scripts/pm/label-write.mjs; read back with assignee os-bill. (3) This os-dev-report comment, POST issues/21585/comments, via scripts/pm/post-stamped.mjs. git pushes are not REST writes; all were fast-forward, with no force. The earlier round's single write was comment 5970542332.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 1b41b79 derived 64 commands, all run.--ranreports 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: missing dist in unrelated packages), then exited 0 when rerun once they were present; the record carries the rerun. Roster gates whose roster sits in a touched directory all exited 0: check:error-code-casing, check:route-ledger-census, check:authz-resolver, check:filter-alias-parity, check-changeset-fixed, check:engine-double-contract and check:error-status-conformance. check:adr-0087-registration: 1 declared-breaking changeset carrying not-required (no-migration-prescription). Fullpnpm lintat 1b41b79: exit 0. Not measured locally, and owed to CI: the 5 path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood, Dogfood Verify, Build Core) and the workspace type-check lanes.",
"line_budget": "n/a",
"deviations": "Three items. (1) File surface. The claim named packages/runtime/src/app-artifact-handlers.ts. I also edited packages/runtime/src/sandbox/body-runner.ts and packages/runtime/src/index.ts. The job body factory lives in body-runner.ts, and 'one judgement' means the door's bindability check and the factory must read the same parse, sojudgeJobBodysits there and the factory uses it. index.ts carries the binder's exports. No objectql, spec or ledger file was touched. (2) The exportcollectJobsWithoutBodyand the typeJobWithoutBodywere renamed tocollectJobsWithoutRunnableBodyandJobWithoutRunnableBody. They were unpublished, and the old name no longer described the judgement. The PR body says so. (3) The merge commit 1b41b79 carries git's default merge message with no trailer pair; pre-push accepted it.",
"files_changed": [
".changeset/21585-hook-refusal-install-local.md",
"packages/cli/test/package-install-local-hooks.integration.test.ts",
"packages/cloud-connection/src/marketplace-install-local-hooks.test.ts",
"packages/cloud-connection/src/marketplace-install-local-jobs.test.ts",
"packages/cloud-connection/src/marketplace-install-local-plugin.ts",
"packages/runtime/src/app-artifact-handlers.jobs.test.ts",
"packages/runtime/src/app-artifact-handlers.test.ts",
"packages/runtime/src/app-artifact-handlers.ts",
"packages/runtime/src/index.ts",
"packages/runtime/src/sandbox/body-runner.ts"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed (in the PR's Acceptance notes). A hook whosebodyIS an object the hook body runner refuses is warned and not bound on every door, and the binder does not fall back to itshandler. The door does not refuse that shape, because the ruling folded in job-body bindability only. Observation, code-read; no public-door reach was measured for a wrong answer."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsACCEPT — PR #21615 at
1b41b79d2d, pending its contract reviewdomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· review of record, read on GitHub 2026-10-03T17:41Z · ⛔ classes, doors and roles onlyShape:
- Draft, base
main,Fixes #21585, andClause-②: yes (narrowing)line-initial. The title isfix(runtime,cloud-connection)!:. - 10 files, +1280 / −105. Not governed (
check-governed-mergesruns before landing). - CI on
1b41b79d2d, at read: 14success, 3skipped, 14 still running. Landing waits for the full set, and the contract review starts once CI completes.
Against triage's ruling
5970803032(Q1: A, with the off-spec jobbodyfolded in), read from the diff:- The door refuses (
marketplace-install-local-plugin.ts, beside the job refusal):- a package carrying a hook with no
body; - and a package whose enabled job's
bodydoes not bind (an expression body, or one carryingbody.timeoutMs). - Both come in one
422 VALIDATION_ERROR, an existing code with no ledger edit. It names every hook and job, with its handler or the refused key, and the remedies: abody, oros start --artifact. Nothing is registered, persisted, bound or scheduled.
- a package carrying a hook with no
- One judgement, one place, in the runtime binder:
collectHooksWithoutBodymirrors the engine binder's body-first test.collectJobsWithoutRunnableBodyreadsjudgeJobBody, a parse againstJobSchema.shape.body.- The job body factory binds by that same parse, so the door and the factory cannot disagree.
- Rehydrate:
bindAppArtifactHandlerstakeswithholdHooksWithoutBody, and only install-local passes it (checked at the head: one caller).- A hook with no
bodyin an older entry rehydrates warned by name and NOT bound. That closes the measured cross-app binding on this door, hot and after a restart. os start --artifact,defineStackconfigs and the metadata door are unchanged. An--artifactapp's own handler hook still binds (pinned).
- ⛔ No
objectql,packages/specor ledger edit. B (owner-scoped resolution) stays the maintainer's [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. - Pins:
- the new integration pin
package-install-local-hooks.integration.test.ts(8 cases, two boots); - the unit pins beside the door and the binder;
- the install-local integration family, 59 passing on built packages.
- the new integration pin
- Reverse verification: three legs, each red where predicted, each restore blob- and dist-proven.
- (a) The door's hook refusal.
- (b) The rehydrate withholding. With it ablated, the rehydrated record reads the host's stamp, so the cross-app binding returns.
- (c) The job-body bindability check.
Clause-②:
yes (narrowing)is right, because install-local refuses packages it accepted before. The changeset grades@objectstack/runtimeand@objectstack/cloud-connectionasminor, with BREAKING and the ADR-0087 marker (not-required (no-migration-prescription), which passescheck-adr-0087-registration).Deviations, accepted:
sandbox/body-runner.tsandindex.tsare outside the claim's list. One judgement requires the door's check and the job body factory to read the same parse.- The rename of
collectJobsWithoutBody/JobWithoutBodyto…WithoutRunnableBody. Both landed in PR fix(runtime,cloud-connection)!: a job's sandboxed body is scheduled on every door, and install-local refuses an enabled job with no body (#21489) #21584 and are unpublished, with the changeset still pending, so no released consumer exists. The PR body states the rename. The pinned objectui sibling cannot import a symbol that is days old. The landing seat greps it per Post-Task step 4 regardless. - The default-message merge commit, which squashes away.
Noted, not filed (in the PR's Acceptance notes): a hook whose
bodyIS present but is a shape the runner refuses is warned and not bound on every door. The door does not refuse it, because the ruling folded in job-body bindability only. Code-read; no wrong public-door answer measured.Serial: #21602 is held behind this PR's merge on
app-artifact-handlers.ts. This PR leaves the scheduling identity untouched.needs:contract-reviewis hung on PR #21615 in this act.
Generated by Claude Code
- Draft, base
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsREWORK: PR #21615 at
1b41b79d2d. Test Core (2/6) is red on this PR's own rename. The ACCEPT5971770589does not hold on this headdomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· read 2026-10-03T17:49ZThe failing check: Test Core (2/6), job
111255991792, in@objectstack/spec#test:repo:scripts/liveness/evidence.test.ts› shipped ledgers › every localpath#symbolanchor names a symbol its file contains ([finding] A liveness citation can rot IN RANGE:action.targetandaction.requiredPermissionspoint at the wrong lines one day after being repointed, and every check stays green #12516).- Received:
job/enabled → packages/runtime/src/app-artifact-handlers.ts#collectJobsWithoutBody (symbol gone).
Why it is this PR's. The PR renames the exported
collectJobsWithoutBody/JobWithoutBodyto…WithoutRunnableBody. The shipped job ledger, landed by PR #21584, cites the old symbol asjob/enabled's evidence anchor. The rename was a deviation the ACCEPT accepted, and that acceptance was wrong to make without checking what cites the symbol. That is the seat's error.The directed fix: restore the names. ⛔ Do not repoint the ledger.
- Keep the exports
collectJobsWithoutBodyandJobWithoutBodyunder their landed names. Update their TSDoc to state that the judgement now covers abodythat does not BIND (an expression body, or one carryingbody.timeoutMs). - The reason: the claim's fence is "⛔ No
packages/specedit", and repointingpackages/spec/liveness/job.jsonwould put@objectstack/specinto this PR and its changeset for a name change alone. - ⛔ No second exported name or alias. One name, with its TSDoc.
- Then run the failing test locally and name its result, so it is green before the push:
Use the package's own repo-tier spelling.
pnpm --filter @objectstack/spec exec vitest run --project repo scripts/liveness/evidence.test.ts - Re-run the runtime and cloud-connection suites, the install-local integration pins, dispatch-gates
--commandsreconciled with--ran, and fullpnpm lint. Update the PR body (the rename paragraph goes) and post oneos-dev-report.
Instrument note for the seat's record: the round's
dispatch-gatesderivation, taken from the touched paths, did not schedule@objectstack/spec'stest:repofor a symbol rename inpackages/runtime. A ledger anchor in another package is a reader that path-derived gates cannot see. Noted on the seat post; not filed.
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21585,
"status": "done",
"branch": "claude/issue-21585-handler-hook-refusal",
"pr": "#21615",
"session": "session_016GiHYRmLSNWTfbX9gVQkpz",
"premise_still_valid": true,
"summary": "Patch round 1 (REWORK 5971830444 and the PM's docs addendum), head c7144b6. The landed export namescollectJobsWithoutBodyandJobWithoutBodyare restored in the runtime binder, its index, the door, the tests and the changeset. There is no alias, no second name and nopackages/specedit. Their TSDoc now says the judgement also covers abodythat does not bind (an expression body, or one carryingbody.timeoutMs). The shippedpackages/spec/liveness/job.jsonanchorjob/enabledresolves again, and its evidence prose still holds: the door judges enabled jobs only. The worktree was re-created from the branch andorigin/main83b3d32 was merged in, with no rebase and no force-push. Docs:content/docs/automation/jobs.mdx's install-local sentence now states the refusal of a jobbodythat does not bind, and the remedy is a validbody. No hand-written page states how a hook'shandlerbehaves onos package installor install-local, so no hook sentence needed correcting. Searched withgit grepunder content/docs, excluding releases and references: the pages namingos package installare jobs.mdx, deployment/cli.mdx, deployment/publish-and-preview.mdx, api/error-catalog.mdx and protocol/kernel/*. None pairs it with a hookhandler. automation/hook-bodies.mdx coversos build's lowering, not install. The PR body was edited once: the rename paragraph is gone, the restored names are stated, and the docs line and this round's verification are added. CI on c7144b6 at the single read: Test Core (2/6) and every other Test Core shard succeeded; 38 checks completed with no failure, and 4 were still in_progress (the claim and changeset guards). Not waited on, per contract.",
"tests": "At HEAD c7144b6. The CI-red test, run with the repo-tier spellingpnpm --filter @objectstack/spec exec vitest run --project repo scripts/liveness/evidence.test.ts: 1 file, 42 passed. The wholepnpm --filter @objectstack/spec test:repo: 51 files, 879 passed.pnpm --filter '@objectstack/cli...' buildunder os-verify-lock: VERDICT command-exit 0. @objectstack/runtime: typecheck green, including check:test-typecheck; fulltest318 files, 4495 passed, 19 skipped. @objectstack/cloud-connection: typecheck green, with 0 TS errors in the src and test programs; fulltest36 files, 437 passed. @objectstack/cli--project unit: 255 files, 3745 passed. Install-local integration pins on built packages: hooks and jobs 19 passed; handlers, boot-steps and uninstall-cleanups 40 passed. The ablation legs (a), (b) and (c) from the first delivery stand. They were taken on e961c7f, and the rename restoration touches none of their anchors or logic, so they were not re-run.",
"mcp_calls": "0",
"api_writes": "2 this round, both through the fleet relay as objectstack-fleet[bot]. (1) issue_patch on #21615 (PATCH /repos//issues/21615, the PR body) via scripts/pm/fleet-write/dispatch.mjs, read back byte-identical at 11094/11094, footer intact. (2) This os-dev-report comment, POST issues/21585/comments, via scripts/pm/post-stamped.mjs. Two git pushes, both fast-forward: the first carried 53c5916 and merge 7f4fd6c, the second c7144b6.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat c7144b6 derived 93 commands, including the docs families the jobs.mdx edit brings in: docs-audit check-affected-docs and check-drift-comment, check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:docs-transcript-drift, spec check:docs and check:variant-docs, lint check:doc-formula-expressions and check:doc-security-posture, and check-docs-section-name. All 93 were run.--ranreports 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN. Two gates first exited 3 (PREREQUISITE NOT MET: unrelated packages, client-react and studio among them, not yet built):check:skill-examplesandcheck:dual-build-cjs-loads. A later gate in the same run built those packages, both then exited 0 on rerun, and the record carries the reruns. The derivation did not call the tree stale. main moved to 5c9138b (objectql and driver-sql) after the merge, so no second merge was made. Fullpnpm lintat c7144b6: exit 0.",
"line_budget": "n/a",
"deviations": "None this round. The first delivery's rename deviation is reversed here, as the REWORK directed.",
"files_changed": [
".changeset/21585-hook-refusal-install-local.md",
"content/docs/automation/jobs.mdx",
"packages/cli/test/package-install-local-hooks.integration.test.ts",
"packages/cloud-connection/src/marketplace-install-local-hooks.test.ts",
"packages/cloud-connection/src/marketplace-install-local-jobs.test.ts",
"packages/cloud-connection/src/marketplace-install-local-plugin.ts",
"packages/runtime/src/app-artifact-handlers.jobs.test.ts",
"packages/runtime/src/app-artifact-handlers.test.ts",
"packages/runtime/src/app-artifact-handlers.ts",
"packages/runtime/src/index.ts",
"packages/runtime/src/sandbox/body-runner.ts"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed (in the PR's Acceptance notes). A hook whosebodyIS an object the hook body runner refuses is warned and not bound on every door, with no fallback to itshandler. The door does not refuse that shape, because the ruling folded in job-body bindability only. Observation from a code read; no public-door reach measured."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsACCEPT — PR #21615 at
c7144b68bc(patch round 1 included), pending its contract reviewdomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· review of record, read on GitHub 2026-10-03T18:49ZThis answers the REWORK
5971830444, plus the seat's docs addendum. The round's report is5972348018.- The landed names are restored.
collectJobsWithoutBodyandJobWithoutBodyare exported again from the binder and its index; checked at the head, with 0 hits for the…WithoutRunnableBodyspelling. Their TSDoc states the judgement now covers abodythat does not bind. ⛔ No alias, no second name, nopackages/specedit. - The shipped ledger anchor resolves again.
packages/spec/liveness/job.json'sjob/enabledcitesapp-artifact-handlers.ts#collectJobsWithoutBody, and that symbol exists at the head. The test that was red,scripts/liveness/evidence.test.ts, passes locally (42/42), and so does the whole@objectstack/spectest:repo(879). - Docs:
content/docs/automation/jobs.mdx's install-local sentence now states the bindability refusal. It is declared to devx in this act. No hand-written page states a hookhandler's install-local behaviour, so no hook sentence was owed.
Shape:
- Draft, base
main,Fixes #21585, andClause-②: yes (narrowing)line-initial. - 11 files, +1276 / −96. Not governed (
check-governed-mergesruns before landing). - CI on
c7144b68bc: 33successand 2skipped(Console Pin Gate, Packed-tarball smoke);mergeable_stateisclean. Test Core 2/6, red on1b41b79d2d, issuccess. The red rows on7f4fd6cafcwere cancellations from the newer push.
Everything else in the round-1 ACCEPT
5971770589stands. The deviation it accepted, the rename, is withdrawn by this round. The seat's error there (accepting a rename without checking its citers) is recorded on the seat post.The contract review of record is started on this head in this act.
needs:contract-reviewstays on.
Generated by Claude Code
- The landed names are restored.
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsPointer: one measure-first item from PR #21615's contract review. Same family, not filed
domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· 2026-10-03T19:01Z · ⛔ Not a claim, ⛔ not a dispatchThe contract review of record on PR #21615 (
5972451433, PASS) escalated one item to the seat. It is the dev's own out-of-scope note, which carriedcarrier: none:- A hook whose
bodyIS an object, but one the hook body runner refuses, installs with success on install-local and is only warned and not bound, on every door. - The door now judges job bodies by bindability, but hook bodies only by whether a body object is present. So an off-spec hook body that
os validaterefuses could install silently.
It is code-read only, with no public-door reach measured, so the filing gate keeps it unfiled. It sits outside triage's ruled scope for this card, which folded in job-body bindability only.
Carrier: the next claim in this family, or triage's next sweep of it, measures it once through the public door. If it is silent, the seat files it with its reach.
Generated by Claude Code
- A hook whose
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsLanded: PR #21615 →
045b946256domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· read 2026-10-03T19:33Z · ⛔ classes, doors and roles only- Merged 2026-10-03T19:32Z through the merge queue (
added_to_merge_queue2026-10-03T19:02Z), at headc7144b68bc. That is the head both the patch-round ACCEPT5972366943and the contract review PASS5972451433read. - Shape:
git rev-list --parents -n 1 045b946256gives 2 fields, so it is a single-parent squash. The commit is an ancestor oforigin/main. It is 11 files, +1276/−96, matching the PR. - Content read on
origin/main:packages/runtime/src/app-artifact-handlers.tsexportscollectHooksWithoutBodyand the restoredcollectJobsWithoutBody, and carrieswithholdHooksWithoutBody. The shipped ledger anchorjob/enabledresolves. - The card closed
completedviaFixes #21585.pm:dispatchedis stripped in this act. - Next on this seat: [finding] Two install-local packages that declare the same job name: the second install silently replaces the first package's job, which stops running, and the door says nothing #21602 (the job-name identity in
scheduleAppArtifactJobs), which was held serially behind this PR on the same file. It is dispatched in this round. - Still open, elsewhere:
- [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 (owner-scoped function resolution) is the maintainer's decision. - The measure-first item
5972494706(a refused hook-body object) waits for this family's next claim.
- [Decision] security(objectql): may a hook's
Generated by Claude Code
- Merged 2026-10-03T19:32Z through the merge queue (
- added 4 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a product defect with reach measured.
reach:public door, measured once (below). Filed by thedomain:cliseat (seat post #6024,session_016GiHYRmLSNWTfbX9gVQkpz), as ruling5964305303on #21489 directs: 「whether install-local refuses or silently drops a hook in the deprecatedhandlerform is unmeasured. The claim on this card measures it once and files it if it is silent.」 It is silent. ⛔ Not a claim. Triage sets the grade and the lane.Reader who acts: triage grades it. The natural fix lands where #21489's C refusal lives (
packages/cloud-connection, install-local), beside the refusal of an enabled job with nobody.Dedupe: MCP
search_issues, repo-scoped, open and closed together:/databyte-for-byte? The action door still ships thehook NAME threw:wrapper, and closing that needsSANDBOX_ERROR_PASSTHROUGHwidened — a declared decision about what sandboxed code may read #17682, a nested hook refusal's wire sentence; 17.3.0 seals@objectstack/clisubpaths but ratifies only./console—extractHookBody(and./package.json) have no public entry, and an app's hook-body fidelity harness breaks with no replacement #15325, the cli subpath seal).Measured (the #21489 dev, at PR #21584's head
c866c5ac9d, through the public door)handlerform, a function name with nobodyand nofunctionsentry the door could carry. A control hook on the same object uses abody.os package installanswers exit 0 andPackage installed into the running kernel.WARN [hook-binder] skipping hook with unresolved handler. A remote installer, human or AI, never reads the server log.Why it is this family
handlernames code that no JSON install door carries, so install-local cannot run it, by contract.HookSchema.handleris DEPRECATED in favour ofbody(hook.zod.ts).5964305303makes C the loud answer for the one shape no JSON door can run. On [Decision] install-local: a package's declared jobs are never scheduled — refuse the install, name them in the install answer, or make job handlers declarable bodies (the jobs half of #21322) #21489 it is applied to jobs only.Noted beside it (code-read, unmeasured): a handler-form hook falls back to the engine's function resolution, so on install-local it could bind to a same-named function that another app registered. Measure that first; it would change the class.
Generated by Claude Code