Skip to content

Commit afba4ec

Browse files
os-steveclaude
andauthored
fix(gate): see a non-literal code: in an object literal — a constant resolves, a template is reported (#9223) (#9247)
* fix(gate): see a non-literal `code:` in an object literal (#9223) * fix(gate): declare the sites the widened scan surfaces (#9223) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2abb66b commit afba4ec

3 files changed

Lines changed: 547 additions & 27 deletions

File tree

packages/runtime/src/dispatcher-error-vocabulary.ts

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,22 @@ export type CodeStampShape =
6868
/** `readonly code = CONST` — the same, through a resolved constant. */
6969
| 'classconst'
7070
/** `code: 'X'` in an object literal. */
71-
| 'objlit';
71+
| 'objlit'
72+
/**
73+
* [#9223] `code: CONST` in an object literal — the same indirection
74+
* `classconst` follows, in the shape that stamps most of this repo's codes.
75+
* Missing from the scan until #9223, which is why the rows carrying it were
76+
* all added at once: `objlit` demanded a quoted literal, so a constant in an
77+
* object literal matched NOTHING and was not even reported as unresolved.
78+
*/
79+
| 'objlitconst'
80+
/**
81+
* [#9223] `` code: `A_${x}_B` `` — a code built by interpolation. No source
82+
* scan can evaluate one, so the gate reports it under its FAMILY identity
83+
* (`${…}` → `*`, e.g. `APPROVAL_*_FAILED`) and a row must classify it. The
84+
* only verdict that can honestly cover a family is `runtime-pinned`.
85+
*/
86+
| 'objlittemplate';
7287

7388
/**
7489
* Where the stamped code can end up. `dispatcher` is the door this card is
@@ -85,8 +100,15 @@ export type CodeStampShape =
85100
* — so a code stamped on a thrown value and passed through remains exactly the
86101
* reachability question this table answers. Both were spelled `sendError` until
87102
* #9098; that collision is what let the door's hole read as closed.
103+
*
104+
* [#9223] `plugin-route` is a THIRD door, surfaced by the widened scan: a plugin
105+
* that mounts its own Hono routes answers refusals with its own `c.json({
106+
* success: false, error: { code, message } })` and passes through neither the
107+
* dispatcher's `errorFromThrown` nor `packages/rest`'s doors. The envelope is
108+
* still an ADR-0112 `error.code` on a wire with a live reader, so a code that
109+
* reaches it is a registration question exactly like the other two.
88110
*/
89-
export type CodeDoor = 'dispatcher' | 'rest' | 'none';
111+
export type CodeDoor = 'dispatcher' | 'rest' | 'plugin-route' | 'none';
90112

91113
export type CodeVerdict =
92114
/**
@@ -114,7 +136,25 @@ export type CodeVerdict =
114136
* `MONGODB_MULTI_TENANT_UNSUPPORTED` was UNregistered by #8035 for exactly
115137
* this reason, and "host boot matching is not wire vocabulary".
116138
*/
117-
| 'boot-refusal';
139+
| 'boot-refusal'
140+
/**
141+
* [#9223] The site builds its code by INTERPOLATION, so no source scan can
142+
* say which codes it produces or whether they are registered — and a named
143+
* test does that job at runtime instead, by enumerating the family and
144+
* parsing each member against the closed union.
145+
*
146+
* This is the one verdict that does not decide reachability; it records a
147+
* DIVISION OF LABOUR, and it is deliberately hard to misuse:
148+
* `check-dispatcher-error-vocabulary` refuses it on any shape other than
149+
* `objlittemplate` (on a literal it would be an exemption from the registry
150+
* check — the very hole this gate exists to close) and fails when the
151+
* {@link UnregisteredCodeSite.pin} file does not exist.
152+
*
153+
* ⛔ Not a place to park a template nobody checks. If no runtime pin covers
154+
* the family, the fix is to stamp a LITERAL code per branch — then the scan
155+
* checks it like any other and the door can narrow it.
156+
*/
157+
| 'runtime-pinned';
118158

119159
export interface UnregisteredCodeSite {
120160
/** The literal as it is stamped. */
@@ -126,6 +166,13 @@ export interface UnregisteredCodeSite {
126166
readonly verdict: CodeVerdict;
127167
/** Why this verdict — the evidence, not a restatement of the verdict. */
128168
readonly why: string;
169+
/**
170+
* [#9223] Required by `runtime-pinned`, meaningless otherwise: the
171+
* repo-relative test that does at runtime what the scan cannot do
172+
* statically. The gate checks that this file EXISTS — a deleted pin would
173+
* otherwise leave the row asserting a guarantee nothing provides.
174+
*/
175+
readonly pin?: string;
129176
}
130177

131178
/**
@@ -146,6 +193,48 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [
146193
// unswept producer lands here as an `unclassified-site` finding and gets a
147194
// new row (then a spec-lane registration, then the row comes out again). ──
148195

196+
// ── pending registration, found by #9223's widened scan ────────────────
197+
{
198+
code: 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED',
199+
file: 'packages/cloud-connection/src/marketplace-install-local-plugin.ts',
200+
shape: 'objlitconst',
201+
door: 'plugin-route',
202+
verdict: 'pending-registration',
203+
why:
204+
'Returned as `error.code` by the marketplace install seam when the ADR-0120 D5e posture gate ' +
205+
'stops an install, and READ off the wire: `packages/cli/src/commands/package/install.ts` ' +
206+
"branches on `res.body?.error?.code === 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED'` to print the " +
207+
'per-index decision list. That the seam speaks REGISTERED vocabulary is measurable rather ' +
208+
'than assumed: every sibling code in the same file (PLUGIN_MANIFEST_INVALID, ' +
209+
'MARKETPLACE_UNAVAILABLE, INVALID_REQUEST, RESOURCE_NOT_FOUND, CLOUD_FETCH_FAILED, …) is in ' +
210+
'the ledger already, which is why the scan never reported them — this one member is the gap. ' +
211+
'Invisible until #9223 for one reason only: it is stamped through a constant ' +
212+
'(GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, `packages/types/src/unique-scope-install-gate.ts`) in ' +
213+
'an object literal, the shape `objlit` could not see. ⇒ the spec lane registers it.',
214+
},
215+
216+
// ── runtime-pinned: an interpolated family, checked where it can be ─────
217+
{
218+
code: 'APPROVAL_*_FAILED',
219+
file: 'packages/rest/src/rest-server.ts',
220+
shape: 'objlittemplate',
221+
door: 'rest',
222+
verdict: 'runtime-pinned',
223+
pin: 'packages/rest/src/rest-approvals-wire-codes.test.ts',
224+
why:
225+
"Three approvals route factories (`decisionRoute`, `flowMoveRoute`, `threadRoute`) spell the " +
226+
'terminal 500 catch\'s code as a template — `` `APPROVAL_${action.toUpperCase()}_FAILED` `` and ' +
227+
'two siblings — so the family, not a literal, is what exists in source. #8885 registered all ' +
228+
'nine codes the family produces, and its pin is what keeps that true: it enumerates the ' +
229+
'registered `POST /approvals/requests/:id/<action>` routes and asserts the code each catch arm ' +
230+
"would generate parses against ApiErrorSchema's closed union, mirroring the production " +
231+
"template exactly (single-occurrence `.replace('-', '_')` included). So a tenth action route " +
232+
'whose generated code nobody registers fails THERE, mechanically. This row records that ' +
233+
'division of labour instead of letting the scan imply it checked something it cannot: #9223 ' +
234+
'widened the scan enough to SEE the template, and seeing it is what makes the pin an ' +
235+
'accounted-for half rather than a local habit in one package.',
236+
},
237+
149238
// ── sandbox-authored: outside any ledger, by design ────────────────────
150239
// (no source site — the producer is tenant code; see SANDBOX_AUTHORED_LIMB)
151240

@@ -187,6 +276,61 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [
187276
'`error.code`. This is the row that shows why verdicts are DECLARED: it is written exactly ' +
188277
'like FLOW_FAILED and a documented catch one layer up makes it unreachable.',
189278
},
279+
{
280+
code: 'YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER',
281+
file: 'packages/plugins/plugin-auth/src/auth-manager.ts',
282+
shape: 'objlitconst',
283+
door: 'none',
284+
verdict: 'foreign-vocabulary',
285+
why:
286+
"better-auth's own APIError vocabulary — the vendor's `YOU_ARE_NOT_ALLOWED_TO_*` family, " +
287+
'thrown as `new APIError(\'FORBIDDEN\', { message, code })` so the remove-member guard\'s ' +
288+
'refusal SET stays byte-for-byte the vendor\'s (see the contract note in ' +
289+
'`remove-member-permission-guard.ts`; only the envelope differs). It cannot reach this door, ' +
290+
'by the same route the IMPERSONATION_ROTATION_FAILED row documents and re-verified here: ' +
291+
'`domains/auth.ts` catches everything the auth service throws and answers ' +
292+
'`deps.error(INTERNAL_ERROR_MESSAGE, 500)` — unconditionally, never `errorFromThrown` (#5085). ' +
293+
'So the string never lands in an ADR-0112 `error.code`.',
294+
},
295+
{
296+
code: 'OS_METADATA_CONVERTED',
297+
file: 'packages/spec/src/conversions/apply.ts',
298+
shape: 'objlitconst',
299+
door: 'none',
300+
verdict: 'foreign-vocabulary',
301+
why:
302+
'The ADR-0087 D4 conversion-notice vocabulary, not an error vocabulary at all: ' +
303+
'`applyConversions` PASSES this code to an `onNotice` callback in a structured ' +
304+
'ConversionNotice, and the loader, `validate` and the MCP deprecations surface consume that ' +
305+
'shape. Nothing is thrown and no envelope is built. Same class as the INVALID_SCREEN_INPUT ' +
306+
'row — a result envelope that merely spells itself `code`.',
307+
},
308+
{
309+
code: 'OS_METADATA_CONVERSION_CONFLICT',
310+
file: 'packages/spec/src/conversions/apply.ts',
311+
shape: 'objlitconst',
312+
door: 'none',
313+
verdict: 'foreign-vocabulary',
314+
why:
315+
'The conflict twin of the OS_METADATA_CONVERTED row: handed to `onConflict` when a rename ' +
316+
"target is a live name owned by something else, so the conversion refuses to rewrite it and " +
317+
'surfaces a loud diagnostic instead (ADR-0078: never silent). A callback payload, not a ' +
318+
'thrown error and not a response body.',
319+
},
320+
{
321+
code: 'ERR_BULK_PER_ROW_HOOK_LIMIT',
322+
file: 'packages/spec/src/data/bulk-write-hook-conformance.ts',
323+
shape: 'objlitconst',
324+
door: 'none',
325+
verdict: 'foreign-vocabulary',
326+
why:
327+
'The declaring source rules on this itself, in the doc comment on the constant: ' +
328+
'"Deliberately an `ERR_`-prefixed operational code on the thrown error\'s own property bag, ' +
329+
'NOT an ADR-0112 wire code … minting a member of it by side effect is the exact ' +
330+
'`declared != enforced` shape that vocabulary exists to prevent." It is RETURNED in a ' +
331+
'`BulkPerRowHookBudgetVerdict` by a function documented pure and total (no throw); the engine ' +
332+
'raises, the contract decides.',
333+
},
190334
{
191335
code: 'SQLITE_ERROR',
192336
file: 'packages/spec/src/migrations/entries/semantic/18.driver-sql-unresolvable-where-column-refused.ts',

packages/runtime/src/error-envelope.conformance.test.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,15 @@ describe('#8087 — every code the dispatcher door can emit is parsed against Ap
242242
);
243243

244244
it('drives a code from the derivation rather than a list written by hand', () => {
245-
// #8846 registered every code the first derivation reported, so the
246-
// pending list is EMPTY now and the per-code rejection drives below
247-
// are vacuous BY DESIGN — that emptiness is the discharged state, not
248-
// a broken wiring. The suite does not go quiet with it: the seven
249-
// registered codes are driven POSITIVELY in the '#8846 discharge' case
250-
// below, and a future pending row re-populates the loop on its own.
245+
// #8846 registered every code the FIRST derivation reported, which
246+
// emptied this list; [#9223] widening the scan to see a `code: CONST`
247+
// in an object literal re-populated it with one member —
248+
// UNIQUE_SCOPE_CONFIRMATION_REQUIRED, at the marketplace install seam's
249+
// `plugin-route` door — which is the loop working, not a regression.
250+
// PENDING_AT_DISPATCHER_DOOR stays empty because that code never passes
251+
// through this door, so the per-code drives below remain vacuous BY
252+
// DESIGN; the suite does not go quiet with them, since the seven
253+
// registered codes are driven POSITIVELY in the '#8846 discharge' case.
251254
// Spread: both lists are `readonly string[]`, and `arrayContaining`
252255
// takes a mutable one — passing the frozen list straight in is a tsc
253256
// error that only the TEST_DEBT ratchet would have caught.
@@ -360,8 +363,33 @@ describe('#8087 — every code the dispatcher door can emit is parsed against Ap
360363
expect(site.why.length, `${site.code} at ${site.file} carries no evidence`).toBeGreaterThan(40);
361364
// A site that reaches a door must be on its way to a ledger row;
362365
// anything else must say which non-wire vocabulary it belongs to.
363-
if (site.door !== 'none') expect(site.verdict).toBe('pending-registration');
364-
else expect(site.verdict).not.toBe('pending-registration');
366+
//
367+
// [#9223] One exception, and it is about what a SOURCE SCAN can
368+
// know rather than about reachability: a code built by template
369+
// interpolation reaches the `rest` door and yet has no literal to
370+
// register, so `runtime-pinned` names the test that enumerates the
371+
// family at runtime. It stays out of PENDING_LEDGER_REGISTRATION
372+
// deliberately — a family identity like `APPROVAL_*_FAILED` can
373+
// never be registered, and parking it in that list would hand
374+
// #8846 a debt nobody can discharge.
375+
if (site.door !== 'none') {
376+
expect(['pending-registration', 'runtime-pinned']).toContain(site.verdict);
377+
} else {
378+
expect(site.verdict).not.toBe('pending-registration');
379+
}
380+
}
381+
});
382+
383+
it('[#9223] every runtime-pinned row names its runtime half, and only a template may', () => {
384+
// The declaration-side half of the same guard the gate enforces on the
385+
// scan side. `runtime-pinned` is the one verdict that does not decide
386+
// reachability, so it is the one that could become an exemption from
387+
// the registry check — on a literal it would be exactly that.
388+
for (const site of UNREGISTERED_CODE_SITES.filter((s) => s.verdict === 'runtime-pinned')) {
389+
expect(site.shape, `${site.code} is runtime-pinned but spells a literal`).toBe('objlittemplate');
390+
expect(site.pin, `${site.code} is runtime-pinned with no pin`).toBeTruthy();
391+
expect(site.code, `${site.code} is not a template family identity`).toContain('*');
392+
expect(PENDING_LEDGER_REGISTRATION).not.toContain(site.code);
365393
}
366394
});
367395
});

0 commit comments

Comments
 (0)