Repository navigation
Description
Found while fixing #7448 (PR #7660). Out of scope there — that card is the observability of ctx.log.{info,warn,error}; this is a fourth method that does not exist. Filed rather than folded in, per Prime Directive #10. Unassigned — recording only.
The gap
ctx.log.debug is declared on three surfaces and implemented on none.
| Surface | Says | Anchor (main @ 245d1dc) |
|---|---|---|
| CLI capability extractor | matches debug, grants the log capability |
packages/cli/src/utils/extract-hook-body.ts:56 — /ctx\.log\.(?:info|warn|error|debug)\b/ |
| Docs | lists ctx.log.info / warn / error / debug → log |
content/docs/automation/hook-bodies.mdx:317 |
Sandbox ScriptContext |
declares info / warn / error only |
packages/runtime/src/sandbox/script-runner.ts:243 |
| QuickJS VM install | installs info / warn / error only |
packages/runtime/src/sandbox/quickjs-runner.ts:639 — for (const level of ['info', 'warn', 'error'] as const) |
So an author writes ctx.log.debug(…), the CLI reads it, infers ['log'] and emits a body whose declared capability is satisfied — and the call then throws inside the VM.
Measured
Hook body ctx.log.debug('hi') with capabilities: ['log'], run through hookBodyRunnerFactory + QuickJSScriptRunner with a logger that has a real debug method:
hook 'h' threw: TypeError: not a function
Not a silent drop — the hook fails. Under onError: 'abort' that aborts the write.
Why this is the crypto.hash shape again
script-runner.ts:249-258 documents the precedent in its own comment: crypto.hash carried a signature, a capability token and CLI build-time inference for a function the sandbox never installed, so the one call it typed threw inside the VM. It was removed rather than implemented (#4391, ADR-0049 enforce-or-remove). ctx.log.debug is the same shape one member over — and unlike hashing it has no security-surface argument against implementing it.
Decision needed (enforce-or-remove)
- Enforce — add
debugto the VM install loop and toScriptContext['log']. Cheap;Logger.debug(message, meta)already exists on the contract (packages/spec/src/contracts/logger.ts:15), and after [finding] Hook body ctx.log output is unobservable — body-runner wires log: engineCtx?.logger with no fallback, so the declared ['log'] capability can silently produce nothing #7448 the capability is served from a realLoggerthat has it. - Remove — drop
debugfrom the extractor regex and the docs table, so the three surfaces agree on three methods.
Enforcing looks right here (a body emitting debug-level diagnostics is exactly what --log-level debug is for), but it is a capability-surface call, not a bug fix, so it should be decided rather than assumed.
Dedup
Searched open issues for ctx.log, log.debug, capability log, extract-hook-body: no existing card. #7448 is the adjacent one and covers only the three installed methods.
Activity
claude commentedon Aug 11, 2026
Route repair (finding triage route A):
domain:cliappended — routing only; thefindinggrade is unchanged and no ownership is taken.Landing site read, not guessed: whichever arm of the enforce-or-remove fork wins, the diff lands in
domain:clipackages — enforce touchespackages/runtime/src/sandbox/quickjs-runner.ts:639(the['info', 'warn', 'error']install loop, verified onorigin/main@6a9dec6) plusscript-runner.ts'sScriptContext; remove touchespackages/cli/src/utils/extract-hook-body.ts:56(the extractor regex, verified —debugstill in the alternation) plus the docs table. Bothpackages/runtimeandpackages/clisit in thedomain:clirow of the lane table.Dedup: #7448 (the
ctx.logobservability card,pm:dispatched) explicitly scopes this out and PR #7660 does not touch thedebugmember; no other open card namesctx.log.debug.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).
Graded: promote.
ctx.log.debugis declared on three surfaces (extractor grants it, docs teach it) and installed on none — an author following the documentation getsTypeError: not a function. Declared = enforced, live author impact, S-M fix (installdebugin the sandbox, or retire it from all three declaring surfaces in one pass — implementer measures which direction ADR posture prefers).finding→pm:queue(domain:cli).Generated by Claude Code
hotlong commentedon Aug 12, 2026
Claim: PM loop round 2 —
domain:cliseat.session_01B3Kurx8qufrDzNjk4rag7V(accounthotlong)claude/issue-7661-ctx-log-debugobjectstack-issue-7661domain:clipackages/runtime/src/sandbox/quickjs-runner.ts(the install loop ~:639),packages/runtime/src/sandbox/script-runner.ts(ScriptContext['log']~:243), plus their tests. ⛔ Notpackages/cli/src/utils/extract-hook-body.tsand not the docs table — those are already correct under the ruling below. Stop on breach.mode:subagent(cloud dispatch unavailable — sanctioned fallback),model: opus.runtime/src/sandbox/**is untouched by every in-flight cli claim (finding:/meta/_draftsserves DRAFT object schemas unmasked — the one ADR-0106 outlet left uncovered #6599 =rest-server.ts+runtime/domains/meta.ts; [finding]packages.ts:337promises a wire-field rename that the objectui follow-up measured out — the comment is now a false pointer #7578 =runtime/domains/packages.ts; mcp-stdio-fail-closed c3: the stdio MCP transport starts but is permanently deaf (oclif arg parser pauses process.stdin) #7645 =packages/mcp). Disjoint.Ruling: ENFORCE, on a named and falsifiable premise
Taking this as a restore-invariant call (declared = enforced) rather than escalating: ADR-0049 governs, and the two arms are not evenly matched here.
debugis already granted by the extractor and already taught by the docs, so the three surfaces disagree in the direction of promising more than we deliver — the class the invariant exists to close.Direction: enforce — install
debugin the VM loop and declare it onScriptContext['log'], so all four surfaces agree on four methods.The premise this ruling hangs on (verify BEFORE writing code):
Logger.debug(message, meta)exists on the contract (packages/spec/src/contracts/logger.ts:15) and the logger thelogcapability is actually served from after #7448 really carries a callabledebug. If either half is false — the contract lacks it, or the served logger would throw/no-op — ⛔ do not implement, and ⛔ do not silently switch to the remove arm. Report the fork with the measurement; the direction becomes a maintainer call at that point.Why not the remove arm by default: the cited precedent (
crypto.hash, #4391) was removed because implementing it meant opening a security surface. Emitting a debug-level diagnostic from a hook body has no such argument, and--log-level debugis exactly what such a body is for. That asymmetry is the whole reason this is rulable rather than escalatable — if the dev's measurement contradicts it, the ruling loses its basis and the fork comes back.Pin the consequence: the reproduction in this card — a hook body calling
ctx.log.debug('hi')withcapabilities: ['log'], run throughhookBodyRunnerFactory+QuickJSScriptRunner— must go fromTypeError: not a functionto a delivered debug record. Assert the record reaches the logger, not merely that no throw occurred: adebuginstalled as a no-op would pass a throw-only test and still not be a real fourth method.Generated by Claude Code
hotlong commentedon Aug 12, 2026
Dispatch-mode correction to the claim above:
mode:subagent→mode:cloud.Cloud dispatch was unavailable when I wrote the claim and became available minutes later, so this card is dispatched the way #7341 requires (M and above default to a dedicated cloud container) rather than the shared-container fallback.
session_01CUymLqPDq3d7PgiwpZsbA2cli #7661 ctx.log.debug declared but never installedclaude-opus-5,outcome_branch: claude/issue-7661-ctx-log-debugEverything else in the claim stands unchanged — the ENFORCE ruling, the two-part premise it hangs on (contract
Logger.debugexists AND the served logger really carries a callabledebug), the ⛔ do-not-silently-switch-to-remove clause, and the pin that the debug record must actually reach the logger rather than merely not throw.Generated by Claude Code
hotlong commentedon Aug 12, 2026
{ "issue": 7661, "status": "done", "branch": "claude/issue-7661-ctx-log-debug", "pr": "https://github.com/objectstack-ai/objectstack/pull/7908", "premise_still_valid": true, "summary": "ENFORCE arm implemented as ruled — ctx.log.debug is now installed in the QuickJS ctx.log bridge and declared on ScriptContext['log'], so the four surfaces (extractor, docs, ScriptContext, VM install) agree on four methods. BOTH halves of the premise were measured before any code was written and BOTH hold: (1) Logger.debug(message, meta) is declared on the contract at packages/spec/src/contracts/logger.ts:15; (2) the logger the 'log' capability is served from is opts.logger — the kernel's ctx.logger, passed by all four app-plugin.ts construction sites — which resolves to createLogger() -> ObjectLogger whose debug (packages/core/src/logger.ts:401) writes at level 'debug' through the real sink; it is not absent, not a throwing stub, not a no-op, and body-runner.ts:164 already calls opts.logger?.debug?.() on that same object (the '[BodyRunner] hook fired' line QA #7439 saw at --log-level debug). The gap is also still live on current main @ d91fad5: the extractor regex still carries debug in its alternation (extract-hook-body.ts:56), the docs table still teaches it (hook-bodies.mdx:317), and the install loop was still ['info','warn','error'] (quickjs-runner.ts:639). THIRD FILE, DECLARED: besides the two named files the fix also required packages/runtime/src/sandbox/body-runner.ts, which builds ScriptContext['log'] from the host logger (buildBodyLogSurface, added by #7448). installCtx forwards through the OPTIONAL call ctx.log?.[level]?.(…), so a VM-side debug with no host-side member is a silent drop rather than a throw — i.e. exactly the no-op that the card said must not pass. It is the third file in the same runtime/src/sandbox/** directory the claim reserved and nothing outside that directory was touched. Deliberately NOT touched: extract-hook-body.ts and the docs table (already correct under the enforce arm), runtime/src/domains/**, packages/rest/**.", "tests": "New pin: 4 tests in packages/runtime/src/sandbox/body-log-capability.test.ts (describe '[#7661] hook body ctx.log.debug is installed and delivers') — the card's own reproduction ctx.log.debug('hi') with capabilities:['log'] through hookBodyRunnerFactory + QuickJSScriptRunner asserting a DELIVERED debug record on the host logger (level + attribution), structured data crossing the VM boundary as a value, capability gating, and the no-logger warn-once branch. REVERSE-VERIFIED IN BOTH DIRECTIONS: (a) install reverted -> 4 failed | 5 passed, failing with the card's own \"SandboxError: hook 'h' threw: TypeError: not a function\"; (b) VM install kept but the host-side forward removed (the no-op arm) -> 2 failed | 7 passed with NOTHING throwing, failing on the delivery assertions ('expected +0 to be 1', 'expected undefined to be debug') — so the pin cannot be satisfied by a no-op debug. After the fix: 9 passed. Suites: sandbox 12 files/138 passed; full @objectstack/runtime 137 files/2090 passed. Build: pnpm --filter @objectstack/runtime... build success, and the full closure turbo build 70/70 successful. Gates (derived via scripts/pm/dispatch-gates.mjs): pnpm check:changeset-gate-self-tests OK (118+153+117 assertions), check-empty-changeset OK (1 declaring changeset), check-changeset-no-major OK, check-nul-bytes OK (7231 files). RATCHET: pnpm check:type-check-debt first reported @objectstack/runtime TEST_DEBT 227 -> 229 (+2); both were real errors caused by making debug required on ScriptContext['log'] (TS2741 in two pre-existing three-member ctx.log test doubles at capability-denial-is-a-fault.test.ts:95 and quickjs-runner.test.ts:114). They were FIXED, not ledgered — re-measured back to exactly 227 and the gate now reports OK with none above its recorded number. The debt ceiling was NOT raised.", "open_questions": [ "Resolved by me, recorded not escalated: the declared file surface named only quickjs-runner.ts and script-runner.ts, but the consequence the card pinned (a DELIVERED record, not merely no throw) is unreachable without also forwarding debug in body-runner.ts's buildBodyLogSurface, because installCtx's forward is an optional call. I treated that as inside the claimed runtime/src/sandbox/** surface rather than a breach, and flagged it explicitly in the PR body.", "The action path gets debug for free (actionBodyRunnerFactory shares buildBodyLogSurface and the same installCtx), but the new pin only exercises the hook path plus the pre-existing action test for info. If the PM wants an action-path debug pin too it is a two-line addition; I judged the shared code path made it redundant.", "Not done, and I believe correctly so: no docs change. The docs table already lists debug and is now accurate for the first time. If a release-notes mention is wanted it belongs to the central release process, not this PR (CLAUDE.md forbids touching content/docs/releases/ in a code PR)." ], "out_of_scope_findings": [] }Generated by Claude Code
hotlong commentedon Aug 12, 2026
ACCEPTED — PR #7908. Landing held on the gate jobs.
Reviewed by the
domain:cliseat (hotlong, sessionsession_01B3Kurx8qufrDzNjk4rag7V) against the diff.The ruling's premise held and was measured, not assumed.
Logger.debug(message, meta)was already on the contract, so the ENFORCE arm required nothing new of the host logger — which is exactly the condition the ruling hung on. Had it not held, the instruction was to report the fork rather than switch arms silently; that path was not needed.The find that makes this PR better than its brief. I scoped this to the QuickJS install loop and
ScriptContext['log']. The dev also fixedbuildBodyLogSurface's no-logger branch, which returned{ info, warn, error }— and becauseinstallCtxforwards throughctx.log?.[level]?.(…), an optional call, a missingdebugthere is not a throw but a silent drop: the VM-side method exists, the body runs to completion, and the line goes nowhere. That is #7448's defect verbatim, one construction shape over. It is a third file beyond the two I named, and it is in scope in substance — same seam, same defect, and shipping without it would have madedebug"installed" in one construction path and silently inert in the other. Declared in the PR body rather than slipped in.That gap was only visible because the pin asserts what the host logger received. A throw-only test would have gone green over it — which is the whole reason that clause was in the dispatch.
Coverage checked, not skimmed: the card's own reproduction (
ctx.log.debug('hi')→ a delivereddebugrecord, attributed to the emitting hook); structureddatacrossing the VM boundary as a value rather than"[object Object]";debuggated behind thelogcapability exactly like the other three (so the fourth method is no cheaper to reach than the first three); and the warn-once "output is discarded" stub naming the hook. The two host-double updates (capability-denial-is-a-fault.test.ts,quickjs-runner.test.ts) are the type system correctly forcing doubles to match the widened seam — not test-weakening.Scope:
packages/runtime/src/sandbox/**+ changeset. Nodocs/adr/**, no skills paths, nocontent/docs/releases/. The extractor regex and the docs table are untouched — correct under ENFORCE, since both were already right.⛔ Not flipped yet: ESLint and TypeScript Type Check must both read⚠️ Specific risk on this one — the sibling PR #7907 just failed that job because new
conclusion: successoff the head commit first.packages/runtimetest-layer type errors tripped thecheck:type-check-debtratchet (227 → 231), and this PR also adds runtime test code. If it trips, the fix is to type the new tests correctly; ⛔ the ledger will not be raised to buy them.Generated by Claude Code
fix(runtime): install ctx.log.debug in the sandbox (objectstack-ai#7661…
docs(skills): objectstack-data factual sweep (3/3) — hooks reference,…