@ggui-ai/protocol-conformance 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +45 -25
  2. package/dist/cli.d.ts +4 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +13 -8
  5. package/dist/dispatch-idempotency-conformance/index.d.ts +78 -0
  6. package/dist/dispatch-idempotency-conformance/index.d.ts.map +1 -0
  7. package/dist/dispatch-idempotency-conformance/index.js +173 -0
  8. package/dist/fixtures/index.d.ts +22 -4
  9. package/dist/fixtures/index.d.ts.map +1 -1
  10. package/dist/fixtures/index.js +23 -3
  11. package/dist/fixtures/reserved-channel-replay/fresh-subscribe-replays-reserved-preview.json +30 -0
  12. package/dist/fixtures/reserved-channel-replay/index.d.ts +4 -0
  13. package/dist/fixtures/reserved-channel-replay/index.d.ts.map +1 -0
  14. package/dist/fixtures/reserved-channel-replay/index.js +18 -0
  15. package/dist/host-helper-conformance/index.d.ts +195 -1
  16. package/dist/host-helper-conformance/index.d.ts.map +1 -1
  17. package/dist/host-helper-conformance/index.js +383 -2
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -2
  21. package/dist/match-behavior.d.ts.map +1 -1
  22. package/dist/match-behavior.js +47 -8
  23. package/dist/n1-compat-conformance/cases/forward-app-theme-carry-uninterpretable.json +21 -0
  24. package/dist/n1-compat-conformance/cases/forward-ops-list-blueprints-cloned.json +88 -0
  25. package/dist/n1-compat-conformance/cases/forward-ops-list-blueprints-stamped.json +77 -0
  26. package/dist/n1-compat-conformance/cases/forward-view-proof-later-version.json +14 -0
  27. package/dist/n1-compat-conformance/cases/forward-view-proof-root-later-claim.json +14 -0
  28. package/dist/n1-compat-conformance/cases/release-13-render-result.json +25 -0
  29. package/dist/n1-compat-conformance/cases/release-14-ops-list-blueprints.json +41 -0
  30. package/dist/n1-compat-conformance/index.d.ts +1 -1
  31. package/dist/n1-compat-conformance/index.d.ts.map +1 -1
  32. package/dist/n1-compat-conformance/index.js +138 -20
  33. package/dist/reporter.d.ts +5 -0
  34. package/dist/reporter.d.ts.map +1 -1
  35. package/dist/reporter.js +30 -4
  36. package/dist/run-conformance.d.ts +37 -1
  37. package/dist/run-conformance.d.ts.map +1 -1
  38. package/dist/run-conformance.js +32 -17
  39. package/dist/types.d.ts +16 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +8 -3
@@ -60,11 +60,279 @@
60
60
  * reference validate is `@ggui-ai/design`'s `validateThemeCoverage`
61
61
  * bound to the shipped `consumed-tokens.manifest.json` tokens.
62
62
  *
63
+ * - `M1-model-tool-set` — OPTIONAL like T1, graded when the host supplies
64
+ * the filter it uses to derive the MODEL's tool list from a served
65
+ * `tools/list` (ggui#1414): the result must withhold every tool whose
66
+ * `_meta.ui.visibility` lacks `'model'` (ggui's six `ggui_runtime_*`
67
+ * app-only tools) and keep every other tool. SEP-1865 assigns this door
68
+ * to the host, and the server cannot observe a violation — a
69
+ * view-issued and a model-issued call are indistinguishable on the wire
70
+ * — so the kit is where a host proves it. Absent ⇒ `skip`.
71
+ *
72
+ * - `V1-view-material-withheld` — OPTIONAL (ggui#1415, SPEC §4.7): the
73
+ * host keeps view-delivered material out of the model's context. The
74
+ * kit feeds the host's own context rules a render result and a render
75
+ * read body whose `_meta["ai.ggui/render"]` slice carries a unique
76
+ * marker in every string value, and requires none of them (raw,
77
+ * JSON-escaped, percent-encoded, or inside a base64 / base64url run) in
78
+ * what the model sees, while the
79
+ * model still sees the result's model-visible marker; and the host's
80
+ * model-facing resource list, when it has one, must withhold
81
+ * `ui://ggui/render/*` while keeping an ordinary resource. Absent ⇒
82
+ * `skip`.
83
+ * - `L1-view-locator-binding` — OPTIONAL (ggui#1415, SPEC §4.7): the host
84
+ * binds a view's calls and reads to its own locator. The kit feeds the
85
+ * host's own relay decision a view mounted for one session, and reads
86
+ * (every locator form) and every session-naming call a view may make,
87
+ * for that session (relay) and another (refuse). Absent ⇒ `skip`.
88
+ *
89
+ * The three host-rule grades (M1, V1, L1) grade the host's RULE, fed
90
+ * fixtures: they prove the rule is right, not that a running host applies
91
+ * it (self-certification; the V1 and L1 pass details say so).
92
+ *
63
93
  * A helper that refuses the relay honestly is graded **tier
64
94
  * `read-only`** — a LEGAL grade, with the R cases skipped, never
65
95
  * failed. `nonconforming` means a dishonesty case failed.
66
96
  */
97
+ import { toolVisibleToModel, } from '@ggui-ai/protocol/integrations/mcp-apps';
67
98
  import { renderLocatorUri } from '../resource-read-conformance/index.js';
99
+ /**
100
+ * A served-equivalent shape the M1 grade feeds a host's filter (ggui#1414):
101
+ * agent tools as ggui declares them — `ggui_render` and `ggui_update` stamp
102
+ * `visibility: ['model']`, the others carry no marker (the spec's default)
103
+ * — and ggui's seven app-only `ggui_runtime_*` tools. The list is static and
104
+ * the names are the protocol's own, so it grades the FILTER a host applies,
105
+ * never the host's live `tools/list`: a host that filters by name rather
106
+ * than by visibility passes only as long as this list matches what its
107
+ * server lists, which is why the grade wants the filter and the SPEC
108
+ * points at the predicate.
109
+ */
110
+ export const MODEL_TOOL_SET_FIXTURE = [
111
+ { name: 'ggui_handshake' },
112
+ { name: 'ggui_render', _meta: { ui: { visibility: ['model'] } } },
113
+ { name: 'ggui_update', _meta: { ui: { visibility: ['model'] } } },
114
+ { name: 'ggui_consume' },
115
+ { name: 'ggui_amend' },
116
+ { name: 'ggui_runtime_submit_action', _meta: { ui: { visibility: ['app'] } } },
117
+ { name: 'ggui_runtime_pull', _meta: { ui: { visibility: ['app'] } } },
118
+ { name: 'ggui_runtime_sync_context', _meta: { ui: { visibility: ['app'] } } },
119
+ { name: 'ggui_runtime_refresh_ws_token', _meta: { ui: { visibility: ['app'] } } },
120
+ { name: 'ggui_runtime_telemetry', _meta: { ui: { visibility: ['app'] } } },
121
+ { name: 'ggui_runtime_report_render_failure', _meta: { ui: { visibility: ['app'] } } },
122
+ { name: 'ggui_runtime_declare_tool_catalog', _meta: { ui: { visibility: ['app'] } } },
123
+ ];
124
+ /**
125
+ * The render result V1 feeds a host's context builder (ggui#1415). Every
126
+ * string value in the `_meta["ai.ggui/render"]` slice is a unique marker
127
+ * ({@link VIEW_MATERIAL_CANARIES}); the model-visible parts carry values of
128
+ * their own, so a host that shows them cannot trip a canary, and one marker
129
+ * ({@link VIEW_MATERIAL_VISIBLE_MARKER}) the model must still see.
130
+ */
131
+ export const VIEW_MATERIAL_RESULT_FIXTURE = {
132
+ structuredContent: { sessionId: 'vmx-visible-session-2a7d', status: 'rendered', note: 'vmx-visible-marker-5e1b' },
133
+ content: [{ type: 'text', text: 'Rendered the card (vmx-visible-marker-5e1b).' }],
134
+ _meta: {
135
+ 'ai.ggui/render': {
136
+ sessionId: 'vmx-slice-session-3f9a1c',
137
+ appId: 'vmx-slice-app-8b2e',
138
+ runtimeUrl: 'https://vmx-slice-runtime.invalid/iframe-runtime.js',
139
+ wsUrl: 'wss://vmx-slice-wsurl-4c2d.invalid/ws',
140
+ wsToken: 'vmx-slice-wstoken-6d41e0a7',
141
+ viewKey: 'vmx-slice-viewkey-Q2FuYXJ5S2V5',
142
+ expiresAt: '2099-07-07T07:07:07.707Z',
143
+ propsJson: '{"vmxSliceProp":"vmx-slice-props-91c2"}',
144
+ },
145
+ },
146
+ };
147
+ /**
148
+ * The marker V1 requires in the model's context: the result itself is not
149
+ * withheld. Both model-visible parts carry it, so a host that shows either
150
+ * one passes.
151
+ */
152
+ export const VIEW_MATERIAL_VISIBLE_MARKER = 'vmx-visible-marker-5e1b';
153
+ /**
154
+ * Every string a value holds, walked deep: object values and keys, arrays,
155
+ * Map entries, Set members, and byte views read as UTF-8 text. Cycles are
156
+ * walked once.
157
+ */
158
+ function stringValuesIn(value, out = [], seen = new Set()) {
159
+ if (typeof value === 'string') {
160
+ out.push(value);
161
+ return out;
162
+ }
163
+ if (typeof value !== 'object' || value === null || seen.has(value))
164
+ return out;
165
+ seen.add(value);
166
+ if (ArrayBuffer.isView(value)) {
167
+ out.push(new TextDecoder().decode(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)));
168
+ }
169
+ else if (value instanceof ArrayBuffer) {
170
+ out.push(new TextDecoder().decode(new Uint8Array(value)));
171
+ }
172
+ else if (value instanceof Map) {
173
+ for (const [k, v] of value) {
174
+ stringValuesIn(k, out, seen);
175
+ stringValuesIn(v, out, seen);
176
+ }
177
+ }
178
+ else if (value instanceof Set || Array.isArray(value)) {
179
+ for (const item of value)
180
+ stringValuesIn(item, out, seen);
181
+ }
182
+ else {
183
+ for (const [k, v] of Object.entries(value)) {
184
+ out.push(k);
185
+ stringValuesIn(v, out, seen);
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+ /**
191
+ * One canary per string value of the fixture's `ai.ggui/render` slice: the
192
+ * value's unique `vmx-slice-…` mark, which survives JSON escaping and
193
+ * encoding, or the whole value when it carries none. None may reach the model.
194
+ */
195
+ export const VIEW_MATERIAL_CANARIES = Object.values(VIEW_MATERIAL_RESULT_FIXTURE._meta['ai.ggui/render'])
196
+ .filter((value) => typeof value === 'string')
197
+ .map((value) => /vmx-slice-[A-Za-z0-9-]+/.exec(value)?.[0] ?? value);
198
+ /** The resources V1 feeds a host's model-facing resource list: the render template and one ordinary resource. */
199
+ export const MODEL_RESOURCE_FIXTURE = [
200
+ { name: 'ggui render', uriTemplate: 'ui://ggui/render/{sessionId}' },
201
+ { name: 'notes', uri: 'file:///vmx-notes.md' },
202
+ ];
203
+ /**
204
+ * A render locator's `resources/read` result as a server answers it: the
205
+ * self-contained shell, with the view's slice inlined, canaries included.
206
+ */
207
+ export const VIEW_MATERIAL_READ_FIXTURE = {
208
+ contents: [
209
+ {
210
+ uri: 'ui://ggui/render/vmx-slice-session-3f9a1c',
211
+ mimeType: 'text/html;profile=mcp-app',
212
+ text: `<!doctype html><html><body><script>globalThis.__GGUI_META__ = ${JSON.stringify(VIEW_MATERIAL_RESULT_FIXTURE._meta)};</script></body></html>`,
213
+ },
214
+ ],
215
+ };
216
+ /** The view L1's requests come from. */
217
+ export const VIEW_BINDING_MOUNT = { locator: 'ui://ggui/render/vmx-s1/bk-vmx1', sessionId: 'vmx-s1' };
218
+ const OTHER_SESSION = 'vmx-s2';
219
+ /**
220
+ * Every tool a view may call (no `model`-only marker) that names a session in
221
+ * its arguments, with arguments its input schema accepts. Left out:
222
+ * `ggui_runtime_declare_tool_catalog` (app-scoped, names none),
223
+ * `ggui_runtime_refresh_ws_token` (its session is inside the envelope, and
224
+ * the server admits the caller to it), `ggui_handshake` and
225
+ * `ggui_list_sessions` (name none), and `ggui_render` / `ggui_update`
226
+ * (model-only).
227
+ */
228
+ export const VIEW_SESSION_BOUND_CALLS = [
229
+ {
230
+ name: 'ggui_runtime_submit_action',
231
+ args: (sessionId) => ({
232
+ kind: 'dispatch',
233
+ payload: { intent: 'confirm', actionData: null, uiContext: {} },
234
+ sessionId,
235
+ appId: 'vmx-app',
236
+ actionId: 'a3f2b1d4',
237
+ firedAt: '2026-09-29T00:00:00.000Z',
238
+ }),
239
+ },
240
+ { name: 'ggui_runtime_sync_context', args: (sessionId) => ({ sessionId, appId: 'vmx-app', snapshot: {} }) },
241
+ { name: 'ggui_runtime_pull', args: (sessionId) => ({ sessionId }) },
242
+ { name: 'ggui_runtime_telemetry', args: (sessionId) => ({ sessionId, events: [{ at: 0, kind: 'boot.path' }] }) },
243
+ {
244
+ name: 'ggui_runtime_report_render_failure',
245
+ args: (sessionId) => ({ sessionId, appId: 'vmx-app', phase: 'mount', errorName: 'TypeError', catches: 2 }),
246
+ },
247
+ { name: 'ggui_consume', args: (sessionId) => ({ sessionId }) },
248
+ { name: 'ggui_amend', args: (sessionId) => ({ sessionId, kind: 'merge', patch: { count: 1 } }) },
249
+ { name: 'ggui_emit', args: (sessionId) => ({ sessionId, channel: 'status', payload: { text: 'hi' } }) },
250
+ { name: 'ggui_get_session', args: (sessionId) => ({ sessionId }) },
251
+ { name: 'ggui_get_render_source', args: (sessionId) => ({ sessionId }) },
252
+ ];
253
+ /**
254
+ * L1's requests and the decision each must get (ggui#1415). A view's own
255
+ * session relays and another's refuses, for a read of either locator form
256
+ * (`{sessionId}`, `{sessionId}/{blueprintKey}`, and an epoch pin `#N`) and
257
+ * for every call in {@link VIEW_SESSION_BOUND_CALLS}.
258
+ */
259
+ export const VIEW_BINDING_CASES = [
260
+ { request: { kind: 'resources/read', uri: VIEW_BINDING_MOUNT.locator }, expect: 'relay' },
261
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${VIEW_BINDING_MOUNT.sessionId}` }, expect: 'relay' },
262
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${VIEW_BINDING_MOUNT.sessionId}#2` }, expect: 'relay' },
263
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}` }, expect: 'refuse' },
264
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}/bk-vmx2` }, expect: 'refuse' },
265
+ { request: { kind: 'resources/read', uri: `ui://ggui/render/${OTHER_SESSION}/bk-vmx2#1` }, expect: 'refuse' },
266
+ ...VIEW_SESSION_BOUND_CALLS.flatMap((call) => [
267
+ { request: { kind: 'tools/call', name: call.name, arguments: call.args(VIEW_BINDING_MOUNT.sessionId) }, expect: 'relay' },
268
+ { request: { kind: 'tools/call', name: call.name, arguments: call.args(OTHER_SESSION) }, expect: 'refuse' },
269
+ ]),
270
+ ];
271
+ /** The base64 / base64url runs inside a string, decoded; a run that does not decode yields nothing. */
272
+ function decodedBase64Runs(text) {
273
+ const runs = text.match(/[A-Za-z0-9+/_-]{16,}={0,2}/g) ?? [];
274
+ // A blob can start anywhere in a run (after a URL path, say), and base64
275
+ // decodes in 4-character groups, so each run is decoded from all four
276
+ // offsets: one of them lines up with the blob.
277
+ return runs.flatMap((run) => [0, 1, 2, 3].flatMap((offset) => {
278
+ const normal = run.slice(offset).replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '');
279
+ const usable = normal.slice(0, normal.length - (normal.length % 4 === 1 ? 1 : 0));
280
+ try {
281
+ const binary = atob(usable + '==='.slice((usable.length + 3) % 4));
282
+ return [new TextDecoder().decode(Uint8Array.from(binary, (ch) => ch.charCodeAt(0)))];
283
+ }
284
+ catch {
285
+ // Not base64 from this offset: the run is searched raw already.
286
+ return [];
287
+ }
288
+ }));
289
+ }
290
+ /**
291
+ * A string with its `%XX` escapes decoded, where it carries any: the form
292
+ * state takes in a URL's query string. A malformed escape is left as is,
293
+ * one escape at a time, so a stray `%` never hides the rest.
294
+ */
295
+ function percentDecoded(text) {
296
+ if (!/%[0-9A-Fa-f]{2}/.test(text))
297
+ return [];
298
+ try {
299
+ return [decodeURIComponent(text)];
300
+ }
301
+ catch {
302
+ // A malformed escape somewhere: decode the well-formed ones in place.
303
+ return [text.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
304
+ try {
305
+ return decodeURIComponent(run);
306
+ }
307
+ catch {
308
+ // Bytes that are not UTF-8 stay escaped; the rest still decode.
309
+ return run;
310
+ }
311
+ })];
312
+ }
313
+ }
314
+ /**
315
+ * Which canaries appear in a model context: raw (JSON-escaped values
316
+ * included, since every mark is escape-free), or inside a percent-encoded
317
+ * string or a base64 / base64url run, decoded (the first match per canary).
318
+ */
319
+ function leakedCanaries(context) {
320
+ const strings = stringValuesIn(context);
321
+ const unescaped = strings.flatMap(percentDecoded);
322
+ const decoded = [...unescaped, ...[...strings, ...unescaped].flatMap(decodedBase64Runs)];
323
+ const raw = VIEW_MATERIAL_CANARIES.filter((c) => strings.some((s) => s.includes(c)));
324
+ const encoded = VIEW_MATERIAL_CANARIES.filter((c) => !raw.includes(c) && decoded.some((s) => s.includes(c)));
325
+ return { raw, encoded };
326
+ }
327
+ /** The slice field a canary is the value of, for a failure detail. */
328
+ function canaryField(canary) {
329
+ const slice = VIEW_MATERIAL_RESULT_FIXTURE._meta['ai.ggui/render'];
330
+ const entry = typeof slice === 'object' && slice !== null
331
+ ? Object.entries(slice).find(([, v]) => typeof v === 'string' && /vmx-slice-[A-Za-z0-9-]+/.exec(v)?.[0] === canary) ??
332
+ Object.entries(slice).find(([, v]) => v === canary)
333
+ : undefined;
334
+ return entry !== undefined ? entry[0] : canary;
335
+ }
68
336
  const METHOD_NOT_SUPPORTED = -32601;
69
337
  const DEFAULT_REFUSAL_TIMEOUT_MS = 2_000;
70
338
  /** A method no ggui helper answers — the honest-refusal probe. */
@@ -72,8 +340,7 @@ const UNSUPPORTED_PROBE_METHOD = 'ggui-conformance/unsupported-probe';
72
340
  /**
73
341
  * The `resources/read` probe's URI: a well-formed render locator naming
74
342
  * a render that does not exist, built with the kit's own locator grammar
75
- * (one owner, `resource-read-conformance`; the kit never compiles against
76
- * live protocol types). It is a locator on purpose, so a helper that
343
+ * (one owner, `resource-read-conformance`; the kit compiles against the protocol's published integrations only where a predicate IS the contract (`toolVisibleToModel`), never against live wire types). It is a locator on purpose, so a helper that
77
344
  * forwards only `ui://ggui/render/` reads still forwards it; a relaying
78
345
  * helper then hands back the server's own classification of the miss,
79
346
  * which is an answer (ggui#1304).
@@ -438,6 +705,120 @@ export async function runHostHelperConformance(port, options = {}) {
438
705
  });
439
706
  }
440
707
  }
708
+ // ── M1: the model's tool set withholds app-only tools ─────────────
709
+ if (options.modelToolSet === undefined) {
710
+ cases.push({
711
+ id: 'M1-model-tool-set',
712
+ outcome: 'skip',
713
+ detail: 'no model-facing filter supplied — the visibility door is still the host\'s obligation (SEP-1865), just not graded here (supply modelToolSet to grade)',
714
+ });
715
+ }
716
+ else {
717
+ const offered = new Set(options.modelToolSet.offeredToModel(MODEL_TOOL_SET_FIXTURE));
718
+ const leaked = MODEL_TOOL_SET_FIXTURE.filter((t) => !toolVisibleToModel(t._meta?.ui?.visibility) && offered.has(t.name)).map((t) => t.name);
719
+ const withheld = MODEL_TOOL_SET_FIXTURE.filter((t) => toolVisibleToModel(t._meta?.ui?.visibility) && !offered.has(t.name)).map((t) => t.name);
720
+ const served = new Set(MODEL_TOOL_SET_FIXTURE.map((t) => t.name));
721
+ const unknown = [...offered].filter((name) => !served.has(name));
722
+ if (leaked.length === 0 && withheld.length === 0 && unknown.length === 0) {
723
+ cases.push({
724
+ id: 'M1-model-tool-set',
725
+ outcome: 'pass',
726
+ detail: `the model's tool set withholds every app-only tool and keeps every model-visible one (${offered.size} offered)`,
727
+ });
728
+ }
729
+ else {
730
+ const parts = [
731
+ leaked.length > 0
732
+ ? `app-only tools offered to the model (${leaked.length}): ${leaked.join(', ')} — a model holding the app credential can call them`
733
+ : undefined,
734
+ withheld.length > 0
735
+ ? `model-visible tools withheld (${withheld.length}): ${withheld.join(', ')} — the agent cannot run the loop without them`
736
+ : undefined,
737
+ unknown.length > 0
738
+ ? `names offered that the server never listed (${unknown.length}): ${unknown.join(', ')} — the list is not derived from tools/list`
739
+ : undefined,
740
+ ].filter((part) => part !== undefined);
741
+ cases.push({
742
+ id: 'M1-model-tool-set',
743
+ outcome: 'fail',
744
+ detail: parts.join('; '),
745
+ });
746
+ }
747
+ }
748
+ // ── V1: view-delivered material stays out of the model's context ──
749
+ if (options.modelContext === undefined) {
750
+ cases.push({
751
+ id: 'V1-view-material-withheld',
752
+ outcome: 'skip',
753
+ detail: "no model-context rule supplied: keeping view material from the model is still the host's obligation (SPEC §4.7), just not graded here (supply modelContext to grade)",
754
+ });
755
+ }
756
+ else {
757
+ const rules = options.modelContext;
758
+ try {
759
+ const context = await rules.modelContextOf(VIEW_MATERIAL_RESULT_FIXTURE);
760
+ const readContext = await rules.modelContextOfRead(VIEW_MATERIAL_READ_FIXTURE);
761
+ const fromResult = leakedCanaries(context);
762
+ const fromRead = leakedCanaries(readContext);
763
+ const seesResult = stringValuesIn(context).some((s) => s.includes(VIEW_MATERIAL_VISIBLE_MARKER));
764
+ const offered = rules.resourcesOfferedToModel?.(MODEL_RESOURCE_FIXTURE);
765
+ const offersRender = (offered ?? []).filter((u) => u.startsWith('ui://ggui/render'));
766
+ const keepsOrdinary = offered === undefined || offered.includes('file:///vmx-notes.md');
767
+ const parts = [
768
+ fromResult.raw.length > 0 ? `view material from a tool result in the model's context: ${fromResult.raw.map(canaryField).join(', ')}` : undefined,
769
+ fromResult.encoded.length > 0 ? `view material from a tool result inside a percent-encoded or base64 run: ${fromResult.encoded.map(canaryField).join(', ')}` : undefined,
770
+ fromRead.raw.length > 0 ? `a render read body in the model's context: ${fromRead.raw.map(canaryField).join(', ')}` : undefined,
771
+ fromRead.encoded.length > 0 ? `a render read body inside a percent-encoded or base64 run: ${fromRead.encoded.map(canaryField).join(', ')}` : undefined,
772
+ seesResult ? undefined : `the model does not see the result's own marker ${VIEW_MATERIAL_VISIBLE_MARKER}: withholding the result is not withholding the view material`,
773
+ offersRender.length > 0 ? `a render read offered to the model: ${offersRender.join(', ')}` : undefined,
774
+ keepsOrdinary ? undefined : 'the ordinary resource was not offered: a list that hides everything is not a rule (omit resourcesOfferedToModel when the model reads no resources)',
775
+ ].filter((part) => part !== undefined);
776
+ cases.push(parts.length === 0
777
+ ? {
778
+ id: 'V1-view-material-withheld',
779
+ outcome: 'pass',
780
+ detail: `none of the slice's ${VIEW_MATERIAL_CANARIES.length} values reaches the model from a tool result or a render read body, raw, JSON-escaped, percent-encoded, base64 or base64url; the result itself does; ${offered === undefined ? 'the host offers the model no resource reads' : 'no render read is offered'} (the host's rule, self-certified)`,
781
+ }
782
+ : { id: 'V1-view-material-withheld', outcome: 'fail', detail: parts.join('; ') });
783
+ }
784
+ catch (err) {
785
+ cases.push({ id: 'V1-view-material-withheld', outcome: 'fail', detail: `the host's rule threw: ${err instanceof Error ? err.message : String(err)}` });
786
+ }
787
+ }
788
+ // ── L1: a view's calls and reads are bound to its own locator ─────
789
+ if (options.viewBinding === undefined) {
790
+ cases.push({
791
+ id: 'L1-view-locator-binding',
792
+ outcome: 'skip',
793
+ detail: "no view-binding rule supplied: binding a view to its own locator is still the host's obligation (SPEC §4.7), just not graded here (supply viewBinding to grade)",
794
+ });
795
+ }
796
+ else {
797
+ const describe = (r) => r.kind === 'resources/read' ? `resources/read ${r.uri}` : `${r.name} for session ${String(r.arguments['sessionId'])}`;
798
+ const decide = options.viewBinding.decide;
799
+ const decided = VIEW_BINDING_CASES.map((c) => {
800
+ try {
801
+ return { ...c, got: decide(VIEW_BINDING_MOUNT, c.request) };
802
+ }
803
+ catch (err) {
804
+ // A rule that throws decides nothing: the request is neither relayed nor refused, and the case says so.
805
+ return { ...c, got: `threw: ${err instanceof Error ? err.message : String(err)}` };
806
+ }
807
+ });
808
+ const relayedForeign = decided.filter((c) => c.expect === 'refuse' && c.got !== 'refuse').map((c) => describe(c.request));
809
+ const refusedOwn = decided.filter((c) => c.expect === 'relay' && c.got !== 'relay').map((c) => describe(c.request));
810
+ const parts = [
811
+ relayedForeign.length > 0 ? `relayed another view's request (${relayedForeign.length}): ${relayedForeign.join('; ')}` : undefined,
812
+ refusedOwn.length > 0 ? `refused its own view's request (${refusedOwn.length}): ${refusedOwn.join('; ')}` : undefined,
813
+ ].filter((part) => part !== undefined);
814
+ cases.push(parts.length === 0
815
+ ? {
816
+ id: 'L1-view-locator-binding',
817
+ outcome: 'pass',
818
+ detail: `the view's own reads and session-bound calls relay and another session's refuse, ${VIEW_BINDING_CASES.length} requests (the host's rule, self-certified)`,
819
+ }
820
+ : { id: 'L1-view-locator-binding', outcome: 'fail', detail: parts.join('; ') });
821
+ }
441
822
  const failures = cases
442
823
  .filter((c) => c.outcome === 'fail')
443
824
  .map((c) => c.id);
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
  export type { TransportConfig, WebSocketTransportConfig, UnknownTransportConfig, AuthConfig, TestCase, SubscribeFrameShaping, SetupStep, CreateGguiSessionStep, ActionSpecEntryDecl, JsonSchemaDecl, RendererUrlOverrideStep, ServerVersionOverrideStep, UiInitializeResponseOverrideStep, EmitEnvelopeStep, TeardownStep, ExpectedBehavior, ActionAckBehavior, ErrorFrameBehavior, StreamUpdateBehavior, BootstrapFailureBehavior, BootstrapSuccessBehavior, VersionMismatchBehavior, PropsUpdateBehavior, SessionStateBehavior, NoOpBehavior, UnknownBehavior, ProtocolError, BootstrapFailureReason, } from './types.js';
14
14
  export type { ConformanceHost, SetupStep as HostSetupStep, CreateGguiSessionSetup, RendererUrlOverrideSetup, ServerVersionOverrideSetup, UiInitializeResponseOverrideSetup, EmitEnvelopeSetup, TeardownStep as HostTeardownStep, } from './conformance-host.js';
15
- export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
15
+ export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, reservedChannelReplayFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
16
16
  export type { ContractSlug } from './fixtures/index.js';
17
17
  export { gadgetWireSchemaCases, runSchemaConformance, } from './schema-conformance/index.js';
18
18
  export type { SchemaConformanceCase, SchemaConformanceMismatch, SchemaConformanceResult, } from './schema-conformance/index.js';
@@ -37,10 +37,12 @@ export type { PreparedReadPlaneOnlyScenario, ReadPlaneOnlyConformanceCase, ReadP
37
37
  export { loadFixture, listFixtures, loadAllFixtures } from './loader.js';
38
38
  export { runConformance } from './run-conformance.js';
39
39
  export type { RunConformanceConfig, ConformanceResult, ConformanceFailure, SkippedFixture, ConformanceReporter, InputEnvelopeDispatch, HostContextObservedInputEnvelope, } from './run-conformance.js';
40
- export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatSkips, } from './reporter.js';
40
+ export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatWarnings, formatSkips, } from './reporter.js';
41
41
  export type { DefaultReporterOptions } from './reporter.js';
42
42
  export { domainErrorCases, isRawToolCallResult, runDomainErrorConformance, } from './domain-error-conformance/index.js';
43
43
  export type { DomainErrorConformanceCase, DomainErrorConformanceResult, DomainErrorCriterion, DomainErrorMismatch, DomainErrorSkip, RawToolCallResult, RunDomainErrorConformanceOptions, ToolCallArgs, ToolCallContent, ToolCallDriver, ToolCallScenario, } from './domain-error-conformance/index.js';
44
+ export { DISPATCH_IDEMPOTENCY_ACTION, DISPATCH_IDEMPOTENCY_ACTION_SPEC, runDispatchIdempotencyConformance, } from './dispatch-idempotency-conformance/index.js';
45
+ export type { DispatchIdempotencyCaseName, DispatchIdempotencyCaseResult, DispatchIdempotencyHost, DispatchIdempotencyResult, DispatchIdempotencySession, } from './dispatch-idempotency-conformance/index.js';
44
46
  export { APP_THEME_CASES, OVERLAY_HASH_CASES, THEME_MODE_CASES, runThemeBindingConformance, } from './theme-binding-conformance/index.js';
45
47
  export type { AppThemeCase, OverlayHashCase, ThemeBindingResult, ThemeModeCase, } from './theme-binding-conformance/index.js';
46
48
  export { N1_COMPAT_CASES, N1_COMPAT_DIRECTIONS, N1_COMPAT_WIRES, runN1CompatConformance } from './n1-compat-conformance/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // Fixture catalog — every authored conformance case, classified by
14
14
  // contract. Third-party runners + the kit's own `runConformance()`
15
15
  // consume this surface.
16
- export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
16
+ export { allFixtures, fixturesByContract, bootstrapProtocolFixtures, consumeBufferFixtures, hostContextFixtures, reservedChannelAuthorityFixtures, reservedChannelReplayFixtures, schemaVersionHandshakeFixtures, subscribeAppScopeFixtures, } from './fixtures/index.js';
17
17
  // Pure-function conformance catalogs — SPEC §7.7.2's gadget
18
18
  // obligations are deterministic validation functions, not transport-
19
19
  // observable behaviors. Each catalog grades a caller-supplied function
@@ -78,9 +78,11 @@ export { runConformance } from './run-conformance.js';
78
78
  // Default reporter — stdout-based bar-scorecard implementation +
79
79
  // formatters programmatic consumers can reuse when building their
80
80
  // own reporter (CI annotations, vitest integration, etc.).
81
- export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatSkips, } from './reporter.js';
81
+ export { createDefaultReporter, formatScorecard, formatSummary, formatFailures, formatWarnings, formatSkips, } from './reporter.js';
82
82
  // ── domain-error (SPEC §7.9 Plane 2 on the tools/call wire, ggui#880) ──
83
83
  export { domainErrorCases, isRawToolCallResult, runDomainErrorConformance, } from './domain-error-conformance/index.js';
84
+ // ── dispatch-idempotency (SPEC §11.1 "A retried dispatch is one gesture", ggui#1519; SHOULD level) ──
85
+ export { DISPATCH_IDEMPOTENCY_ACTION, DISPATCH_IDEMPOTENCY_ACTION_SPEC, runDispatchIdempotencyConformance, } from './dispatch-idempotency-conformance/index.js';
84
86
  // ── theme binding (ggui#987 — the theming wave's arbiter; guuey 1.3 judges against it) ──
85
87
  export { APP_THEME_CASES, OVERLAY_HASH_CASES, THEME_MODE_CASES, runThemeBindingConformance, } from './theme-binding-conformance/index.js';
86
88
  // ── n1-compat (ggui#1014 §3.6 — previous-release payloads against today's parsers; ggui#1093 — a later release's payload against today's READ door) ──
@@ -1 +1 @@
1
- {"version":3,"file":"match-behavior.d.ts","sourceRoot":"","sources":["../src/match-behavior.ts"],"names":[],"mappings":"AA+FA,OAAO,KAAK,EAIV,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,EAC3D,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,CAAC,GACN,QAAQ,IAAI,OAAO,CAAC,gBAAgB,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAE7D;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,WAAW,CAkDb;AAqUD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAgBzD"}
1
+ {"version":3,"file":"match-behavior.d.ts","sourceRoot":"","sources":["../src/match-behavior.ts"],"names":[],"mappings":"AA+FA,OAAO,KAAK,EAIV,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,gBAAgB,CAAC,MAAM,CAAC,EAC3D,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,CAAC,GACN,QAAQ,IAAI,OAAO,CAAC,gBAAgB,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAE7D;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,WAAW,CAkDb;AAkXD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAgBzD"}
@@ -340,19 +340,21 @@ function matchContractViolationPayload(assertions, parsed, frames) {
340
340
  * non-conformant envelope must not satisfy the assertion.
341
341
  */
342
342
  function matchStreamUpdate(behavior, frames) {
343
- const envelopes = frames.flatMap((f) => {
344
- if (f.kind !== 'frame' || f.parsed['type'] !== 'data')
345
- return [];
346
- const body = f.parsed['payload'];
347
- return isStreamEnvelope(body) ? [body] : [];
348
- });
349
343
  // `valueMatch: 'subset'` relaxes the value comparison to "every
350
344
  // declared key is present + matching; extra observed keys ignored".
351
345
  // Default ('exact' / absent) is the exact deep-equal.
352
346
  const subset = behavior.valueMatch === 'subset';
353
347
  const valueMatches = (observed) => subset ? deepMatchSubset(behavior.value, observed) : deepEqual(observed, behavior.value);
354
- const match = envelopes.find((envelope) => envelope.channel === behavior.channel && valueMatches(envelope.payload));
355
- if (match === undefined) {
348
+ // Every matching envelope, with where it arrived among the frames.
349
+ const matches = frames.flatMap((f, index) => {
350
+ if (f.kind !== 'frame' || f.parsed['type'] !== 'data')
351
+ return [];
352
+ const body = f.parsed['payload'];
353
+ return isStreamEnvelope(body) && body.channel === behavior.channel && valueMatches(body.payload)
354
+ ? [{ index, envelope: body }]
355
+ : [];
356
+ });
357
+ if (matches.length === 0) {
356
358
  return {
357
359
  kind: 'fail',
358
360
  expected: {
@@ -366,6 +368,43 @@ function matchStreamUpdate(behavior, frames) {
366
368
  message: `expected a \`data\` frame whose StreamEnvelope names channel '${behavior.channel}' and carries the declared value as its payload (${subset ? 'subset match — every declared key present + matching' : 'exact match'}); none matched.`,
367
369
  };
368
370
  }
371
+ if (behavior.replayAfterAck !== true)
372
+ return { kind: 'pass' };
373
+ return matchReplayPlacement(behavior.channel, frames, matches);
374
+ }
375
+ /**
376
+ * The replay placement a `stream-update` with `replayAfterAck` grades
377
+ * (SPEC §12.2.1, ggui#1526): a matching frame that arrives after the
378
+ * subscribe's `ack` and carries a `seq` at or below that ack's
379
+ * `streamSeq`. The first placement failure found names what is wrong.
380
+ */
381
+ function matchReplayPlacement(channel, frames, matches) {
382
+ const ackIndex = frames.findIndex((f) => f.kind === 'frame' && f.parsed['type'] === 'ack');
383
+ const expected = { type: 'data', channel, after: 'the subscribe ack', seq: '<= ack.payload.streamSeq' };
384
+ const fail = (message) => ({
385
+ kind: 'fail',
386
+ expected,
387
+ received: frames.filter((f) => f.kind === 'frame').map((f) => (f.kind === 'frame' ? f.parsed : f)),
388
+ message,
389
+ });
390
+ if (ackIndex === -1) {
391
+ return fail(`a matching frame on '${channel}' was observed, but no subscribe \`ack\` was, so it cannot be shown to be the replay.`);
392
+ }
393
+ const ackFrame = frames[ackIndex];
394
+ const ackPayload = ackFrame !== undefined && ackFrame.kind === 'frame' ? ackFrame.parsed['payload'] : undefined;
395
+ const streamSeq = isRecord(ackPayload) ? ackPayload['streamSeq'] : undefined;
396
+ if (typeof streamSeq !== 'number') {
397
+ return fail(`the subscribe \`ack\` carries no numeric \`streamSeq\`, so a frame on '${channel}' cannot be shown to be at or below it.`);
398
+ }
399
+ const afterAck = matches.filter((m) => m.index > ackIndex);
400
+ if (afterAck.length === 0) {
401
+ return fail(`the only matching frames on '${channel}' arrived before the subscribe's \`ack\`; the replay belongs after it.`);
402
+ }
403
+ const replayed = afterAck.find((m) => typeof m.envelope.seq === 'number' && m.envelope.seq <= streamSeq);
404
+ if (replayed === undefined) {
405
+ const seqs = afterAck.map((m) => (typeof m.envelope.seq === 'number' ? m.envelope.seq : 'none')).join(', ');
406
+ return fail(`the matching frames on '${channel}' after the \`ack\` carry seq ${seqs}, none at or below the ack's \`streamSeq\` (${streamSeq}): each is above the ack's \`streamSeq\` (${streamSeq}) or unnumbered, so none is the replay.`);
407
+ }
369
408
  return { kind: 'pass' };
370
409
  }
371
410
  function matchNoOp(behavior, frames) {
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "forward-app-theme-carry-uninterpretable",
3
+ "description": "FORWARD tolerance on the theme CARRY read (ggui#1175, VERSION-POLICY §3.6): a theme GET response for a stored document that fails the carry shape, as a door that names it returns it — `theme: null` beside `uninterpretable`, whose issues carry a path and a protocol-owned code and never a stored value. Today's response schema must accept it, and must read it as a document that is not a theme, never as \"no theme\".",
4
+ "direction": "forward",
5
+ "release": {
6
+ "label": "a later ggui release (synthetic): the theme GET naming an uninterpretable stored document",
7
+ "sha": "synthetic"
8
+ },
9
+ "wire": "app-theme-carry",
10
+ "payload": {
11
+ "theme": null,
12
+ "uninterpretable": {
13
+ "issueCount": 2,
14
+ "issues": [
15
+ { "path": ["overlays", "light", "not-a-ggui-var"], "code": "unknown_key" },
16
+ { "path": ["mode"], "code": "value_not_allowed" }
17
+ ]
18
+ }
19
+ },
20
+ "expect": "accepted"
21
+ }