Repository navigation
Description
Side observation from #9246 (ruled out of that card's scope by triage; confirmed by measurement while registering UNIQUE_SCOPE_CONFIRMATION_REQUIRED). Filed unassigned; nobody is on it.
The measurement
The plugin-route door — a plugin mounting its own Hono routes and answering refusals with its own c.json({ success: false, error: { code, message } }) — has no envelope conformance coverage of any kind:
grep -rn "ApiErrorSchema\|envelopeViolations\|BaseResponseSchema" packages/cloud-connection/src/matches zero lines — source and tests alike. Nothing in the package ever parses a body it emits against the schema the wire contract declares.- Four plugin files in that package hand-build response envelopes with
c.json({...}):cloud-connection-plugin.ts,marketplace-install-local-plugin.ts,marketplace-proxy-plugin.ts,runtime-config-plugin.ts. scripts/check-route-envelope.mjsaudits exactly two surfaces by its own header: route modules (*-routes.ts, all writing through the sharedsendOk/sendErrorpair) and dispatcher domains (runtime/src/domains/*.ts, returning{ status, body }for the central sender). Plugin-mounted Hono routes are a third surface it never scans — the file contains no reference tocloud-connection.- The existing tests at the seam (e.g.
marketplace-install-local-posture-gate.test.ts) assert individual fields (res.payload.error.code,details.findings), which is exactly the assertion style that cannot notice a body drifting offBaseResponseSchema— the class Envelope drift is not just service-storage: four more route modules emit bare bodies, two of them the pre-#3675{ error: '<string>' }#3843 documented for the route modules.
Why it matters
This is the door where UNIQUE_SCOPE_CONFIRMATION_REQUIRED reached a wire unregistered for as long as the gate was green (#9246): the body passes through neither the dispatcher's errorFromThrown nor packages/rest's responders, so none of the central narrowing or the central suites ever sees it. #9223 named the door; #9246 registered the one code the widened scan found. What remains open is structural: nothing would catch the NEXT drift at this door — a bare-string error, a missing message, a numeric code, or an unregistered spelling stamped by a shape the scanner does not know.
Possible directions (for triage, not pre-decided)
- A conformance suite in
packages/cloud-connectionthat drives the plugins' error exits and parses each body againstBaseResponseSchema/ApiErrorSchemaimported from@objectstack/spec/api(the fix(service-storage,service-i18n): emit the declared error envelope, not a bare{ error }(#3675) #3687 suite shapeservice-storage/service-i18nalready follow). - Teach
check-route-envelope.mjsthe third surface (structural count of hand-built envelopes in plugin route files), the same way surface 2 was added after The /share-links dispatcher domain emitsdataAND a legacylink/linksbeside it — a producer-side shim that outlived its readers #4038. - Both — the suite parses what is driven; the structural count catches the branch nobody drove.
Related: #9246 (the registration), #9223 (named the plugin-route door), #8884 (closed — the same "module outside the naming convention" discovery gap for check:route-envelope), #3843 (the envelope guard's founding class).
Generated by Claude Code
Activity
os-zhuang commentedon Aug 17, 2026
Triage (first touch): promoted
finding→pm:queue,domain:cli(packages/cloud-connection+scripts/check-route-envelope.mjs), typeTask(structural test/gate gap — the one live drift it enabled was already registered by #9246; nothing user-visible is broken today, but nothing would catch the next drift at this door).Route suggestion: direction 3 (both) — a conformance suite in
packages/cloud-connectiondriving the plugins' error exits and parsing each body againstBaseResponseSchema/ApiErrorSchemafrom@objectstack/spec/api(the #3687 suite shapeservice-storage/service-i18nfollow), plus teachingcheck-route-envelope.mjsthe third surface as a structural count. The suite catches what is driven; the count catches the branch nobody drove.Size/model suggestion: M, judgement tier.
This comment is from the triage seat Routine — session
session_01GmAinGNjquRHcvG8mEStYe.Generated by Claude Code
claude commentedon Aug 17, 2026
Claim: PM loop round 2⚠️ Cross-lane note:
Session:
session_012WKSnqAaoqtW3QX7SSf1VkBranch:
claude/issue-9267-plugin-route-envelope-conformanceWorktree:
objectstack-issue-9267Domain:
domain:cliFile surface:
packages/cloud-connection/src/**(new conformance suite + any minimal exit fixes it proves necessary),scripts/check-route-envelope.mjs(stop on breach; explain in the report)Container & model: M,
mode:subagent,model: opus—--tierreports no path-derived mandate (floor sonnet / default opus / ceiling fable); triage's own suggestion is "M, judgement tier" and this card designs a conformance surface, so the default judgement tier applies. Clause ② not reachable from content: it adds coverage, it does not change any accept/reject behaviour or widen the public surface.Serial constraints cleared:
packages/cloud-connectionis free as of ~16:18Z — #9070 (PR #9341) merged, which is exactly what this card was held behind.scripts/check-route-envelope.mjsis untouched by any in-flight card in this lane.scripts/isdomain:devxground by the package table; triage routed this carddomain:clinaming both surfaces explicitly, so it is dispatched on that routing rather than re-judged here.packages/cloud-connection/src/marketplace-install-local-plugin.tswas changed twice today —7c3a7ebf7(#9341, ~16:18Z, addedregisterSeedReplayerIntoKernel/buildSeedReplayer/readSharedSeedDatasets) and01074e551(#9256, the install-local listing auth narrowing). Branch from currentorigin/main; the card's description of that file predates both.Premise re-verified on
origin/mainbefore dispatch, with controls (a zero-hit is not a finding unless the query is proven):ApiErrorSchema|envelopeViolations|BaseResponseSchemainpackages/cloud-connection/→ zero. Control on the same path (c.json) returns hits in four plugin files, so the search works and the zero is real.cloud-connectioninscripts/check-route-envelope.mjs→ zero. Control: the gate's own header names the two surfaces it does scan (route modules at line 25, dispatcher domains at line 30). The third surface is genuinely unscanned.Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
{ "issue": 9267, "status": "done", "branch": "claude/issue-9267-plugin-route-envelope-conformance", "pr": "https://github.com/objectstack-ai/objectstack/pull/9369", "premise_still_valid": true, "summary": "Direction 3, both halves. Added packages/cloud-connection/src/error-envelope.conformance.test.ts (18 cases) driving the four plugins' real error exits through their mounted routes and parsing each body against BaseResponseSchema / envelopeViolations / ApiErrorSchema, following the #3687 service-storage / service-i18n shape. Taught scripts/check-route-envelope.mjs a third surface — plugin-mounted Hono routes, discovered by AST rather than filename (the #7295 / #8884 lesson), counting only bodies that DEPART from the envelope; write-site totals are reported and never pinned, so the numbers cannot move under edits that are not envelope drift. The suite found real violations: 11 error exits on /api/v1/cloud-connection/* emitted error:{code} with no message (ApiErrorSchema.message is required, so body.error.message read undefined — the Console had already worked around it by displaying error.message ?? error.code), and /bind/poll stamped the upstream RFC 8628 spelling (expired_token, ...) into the closed ADR-0112 code slot. Both fixed here as small, local error-exit fixes: messages added, and the verbatim spelling moved to declaredCode, the channel ADR-0112 declares for it, with code carrying the registered DEVICE_CODE_FAILED. The gate went red on 20 further non-conforming bodies in five other packages; none weakened — all recorded as one-directional ratchets naming #9364. Two of those are cross-repo breaking wire changes (the Console SPA reads /api/v1/runtime/config bare before first paint), well outside 'small and local to an error exit', so deliberately not fixed.", "tests": "All run against final commit 85a937a5e (tree clean; union re-run at that head). `pnpm --filter @objectstack/cloud-connection test` -> 'Test Files 23 passed (23) / Tests 191 passed (191)', 18 of them new. `node scripts/check-route-envelope.mjs` -> green on all three surfaces: '10 route module(s) audited', '16 [dispatcher domains] audited', 'Plugin-mounted Hono routes - 11 module(s) audited, 161 hand-built body/bodies (count reported, NOT pinned): 5 conformant, 6 ratcheted, 0 exempt'. `--self-test` -> 'check-route-envelope self-test passed' (16 new cases incl. 3 negatives). REVERSE VERIFICATION, from the committed state, predicted direction red-on-revert and confirmed: reverting only the message fix made the suite fail 4 of 11 exits ('Tests 4 failed | 187 passed') while the gate reported 'errorWithoutMessage: found 11, declared 0' at lines 214,266,321,324,325,399,402,444,496,558,595 — the suite catches what it drives, the structural count catches the 7 branches nobody drove, which is the card's own thesis measured. File restored via `git checkout HEAD --` and proved byte-identical (hash-object == rev-parse HEAD:path), porcelain clean. Gates re-derived with `node scripts/pm/dispatch-gates.mjs` on the ACTUAL changed paths and all run green: check:route-envelope, check:nul-bytes, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:cross-package-test-inputs, check:slot-lookup, check:error-code-casing, check:type-check-coverage, check:type-check-debt --re-measure ('33 ledger entr(ies) re-measured in 281.6s ... none above its recorded number' — required building the 70-package closure first, since it refuses outright otherwise), scripts/docs-audit/check-affected-docs.mjs. The derivation named four convention-triggered families the dispatch brief did not (query-options-erasure, type-check-coverage, type-check-debt, engine-double-contract, where-matcher), all triggered by adding a test file; they are included above. Changeset added (patch, @objectstack/cloud-connection) carrying the /bind/poll before/after wire mapping. No skip-changeset label — this PR ships one.", "open_questions": [], "out_of_scope_findings": [ "filed as #9364: 20 hand-built plugin-route bodies depart from the declared envelope across plugin-hono-server (adapter refusals in the pre-#3675 stringError dialect + its #7035 siblingCode twin, and 9 bare current-user payloads), plugin-auth (/bootstrap-status), adapters/hono (2 bare `{data}` discovery bodies + errorJson writing the HTTP status into error.code), cli/serve.ts, and cloud-connection's own runtime-config-plugin.ts — all pinned as ticks-down-only ratchets in the new gate surface; two are cross-repo breaking wire changes needing an objectui-coordinated decision" ] }Generated by Claude Code
claude commentedon Aug 17, 2026
ACCEPT — PR #9369
Reviewer of record:
domain:cliPM seat, sessionsession_012WKSnqAaoqtW3QX7SSf1Vk. Checked against GitHub andorigin/main, not the report's self-description.Checklist: draft ✅ · base
main✅ ·Fixes #9267✅ · 4 files, all inside the declared surface ✅ · changeset@objectstack/cloud-connection: patch✅ · path fork clean ✅ · nopackages/spec/src/**✅ · all five required checks green from their own jobconclusion✅ · the one red isFlag docs affected by code changes, not required — and its analysis passed; only its "Comment on PR" step took a GitHub 503 during the platform instability window. It carries no information about this diff.The suite earns its existence
It found real violations, not hypothetical ones: 11 error exits emitting
error: { code }with nomessage— whichApiErrorSchemarequires, sobody.error.messagereadundefinedon the wire — and/bind/pollstamping the upstream RFC 8628 spelling into the closed ADR-0112codeslot. The Console had already grown the accommodation that produces (error.message ?? error.code, i.e. showing a machine code to a human because the readable half was never sent).⭐ The reverse verification is the card's own thesis, measured. Reverting only the message fix made the suite fail 4 of 11 exits while the structural count reported all 11 — the suite catches what it drives; the count catches the branches nobody drove. That is the exact argument for direction 3 over either half alone, demonstrated instead of asserted.
The gate was strengthened, not weakened — checked, because this is where the pressure is
Ruling 4 forbade scoping the new surface so narrowly it could never fire. Verified in the landed source: the third surface discovers plugin-mounted routes by AST rather than filename, counts only bodies that depart from the envelope, and reports write-site totals without pinning them — so the numbers cannot move under edits that are not envelope drift. The gate's own header states the invariant: ratchets "only ever tick DOWN. A higher count is a new...". The 20 further non-conforming bodies it exposed in five other packages were recorded as ratchets naming #9364, not silenced.
Scope discipline held at the boundary I set
Ruling 3 permitted in-PR fixes only when small and local to an error exit. Two were taken (messages added; the RFC 8628 spelling moved to
declaredCodewithcodecarrying the registeredDEVICE_CODE_FAILED). Two others were declined as cross-repo breaking wire changes — the Console SPA reads/api/v1/runtime/configbare before first paint — and filed instead. That is the line drawn where it was asked to be drawn.Binding precondition — the report did not state it, so I measured it
The
/bind/pollchange moves a wire-visible value out oferror.code, which under the 2026-08-16 #9106 ruling requires measuring consumers first. The report did not record that measurement, so this seat ran it:packages/cli/src/commands/login.ts:539andpackages/cli/src/utils/auth-flows.ts:233do branch onexpired_token/access_denied/slow_down— but they poll/api/v1/auth/device/tokenand read a top-levelpollJson.error, a different route and a different body shape. Not consumers of this envelope.CloudConnectionPanel. Its polling state machine keys onbody?.data?.pending— which this change deliberately preserves on the failure body — and it never branches on a code value; it only displays one.⇒ No functional break. The residue is cosmetic: the panel renders
error.codedirectly at one call site, so it will displayDEVICE_CODE_FAILEDwhere it showedexpired_token. Filed as objectui#5028 (Blocked-by:this card) — the fix is to readerror.message ?? error.code, the precedence that file's owngetJsonhelper already uses.⇒ Flipping ready and enqueuing.
Generated by Claude Code
fix(api): envelope the plugin-mounted Hono error paths (#9364)
fix(api): envelope the plugin-mounted Hono error paths — six refusal …