@ggui-ai/protocol 0.13.0 → 0.15.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 (85) hide show
  1. package/dist/envelope-adapters.d.ts +1 -14
  2. package/dist/envelope-adapters.d.ts.map +1 -1
  3. package/dist/envelope-adapters.js +17 -1
  4. package/dist/envelopes/render-refusal.d.ts +49 -0
  5. package/dist/envelopes/render-refusal.d.ts.map +1 -0
  6. package/dist/envelopes/render-refusal.js +12 -0
  7. package/dist/gadgets/resolve-app-gadgets.d.ts +1 -1
  8. package/dist/gadgets/stdlib-gadgets.d.ts +2 -2
  9. package/dist/gadgets/stdlib-gadgets.js +1 -1
  10. package/dist/index.d.ts +67 -60
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +10 -0
  13. package/dist/integrations/mcp-apps.d.ts +4 -10
  14. package/dist/integrations/mcp-apps.d.ts.map +1 -1
  15. package/dist/schemas/app-theme.d.ts +5 -0
  16. package/dist/schemas/app-theme.d.ts.map +1 -1
  17. package/dist/schemas/app-theme.js +23 -0
  18. package/dist/schemas/blueprint.d.ts +2 -2
  19. package/dist/schemas/data-contract.d.ts +12 -30
  20. package/dist/schemas/data-contract.d.ts.map +1 -1
  21. package/dist/schemas/data-contract.js +6 -27
  22. package/dist/schemas/interface-context.d.ts +44 -0
  23. package/dist/schemas/interface-context.d.ts.map +1 -0
  24. package/dist/schemas/interface-context.js +22 -0
  25. package/dist/schemas/invoke.d.ts +1 -1
  26. package/dist/schemas/invoke.js +1 -1
  27. package/dist/schemas/mcp.d.ts +636 -69
  28. package/dist/schemas/mcp.d.ts.map +1 -1
  29. package/dist/schemas/mcp.js +554 -47
  30. package/dist/schemas/public-env-key.d.ts +26 -0
  31. package/dist/schemas/public-env-key.d.ts.map +1 -0
  32. package/dist/schemas/public-env-key.js +25 -0
  33. package/dist/schemas/render-input-envelope.d.ts +20 -0
  34. package/dist/schemas/render-input-envelope.d.ts.map +1 -0
  35. package/dist/schemas/render-input-envelope.js +56 -0
  36. package/dist/schemas/runtime-telemetry-limits.d.ts +11 -0
  37. package/dist/schemas/runtime-telemetry-limits.d.ts.map +1 -0
  38. package/dist/schemas/runtime-telemetry-limits.js +10 -0
  39. package/dist/transport/websocket.d.ts +5 -5
  40. package/dist/types/auth.d.ts +1 -1
  41. package/dist/types/contract-inference.d.ts +1 -1
  42. package/dist/types/events.d.ts +1 -1
  43. package/dist/types/ggui-session-event.d.ts +4 -5
  44. package/dist/types/ggui-session-event.d.ts.map +1 -1
  45. package/dist/types/ggui-session-event.js +0 -37
  46. package/dist/types/host-context.d.ts +18 -17
  47. package/dist/types/host-context.d.ts.map +1 -1
  48. package/dist/types/host-context.js +7 -0
  49. package/dist/types/interface-context.d.ts +1 -1
  50. package/dist/types/invoke.d.ts +1 -1
  51. package/dist/types/lifecycle.d.ts +1 -1
  52. package/dist/types/live-channel.d.ts +2 -2
  53. package/dist/types/llm-route.d.ts +2 -2
  54. package/dist/types/llm-route.d.ts.map +1 -1
  55. package/dist/types/llm-route.js +11 -3
  56. package/dist/types/llm.d.ts +397 -7
  57. package/dist/types/llm.d.ts.map +1 -1
  58. package/dist/types/llm.js +111 -10
  59. package/dist/types/mcp.d.ts +31 -111
  60. package/dist/types/mcp.d.ts.map +1 -1
  61. package/dist/types/mcp.js +7 -0
  62. package/dist/types/readonly.d.ts +11 -0
  63. package/dist/types/readonly.d.ts.map +1 -0
  64. package/dist/types/readonly.js +1 -0
  65. package/dist/types/refusal-codes.d.ts +340 -0
  66. package/dist/types/refusal-codes.d.ts.map +1 -0
  67. package/dist/types/refusal-codes.js +346 -0
  68. package/dist/types/render.d.ts +9 -8
  69. package/dist/types/render.d.ts.map +1 -1
  70. package/dist/types/ui-generator.d.ts +1 -1
  71. package/dist/validation/ajv-runtime.d.ts +2 -2
  72. package/dist/validation/contract-validator.d.ts +6 -6
  73. package/dist/validation/cross-references.d.ts +2 -2
  74. package/dist/validation/hygiene-rules.d.ts +1 -1
  75. package/dist/validation/name-invariants.d.ts +2 -2
  76. package/dist/validation/reserved-channels.d.ts +1 -1
  77. package/dist/validation/schema-compat-invariants.d.ts +2 -2
  78. package/dist/validation/schema-meta-validation.d.ts +1 -1
  79. package/dist/version.d.ts +201 -5
  80. package/dist/version.d.ts.map +1 -1
  81. package/dist/version.js +200 -4
  82. package/dist/wire.d.ts +45 -0
  83. package/dist/wire.d.ts.map +1 -0
  84. package/dist/wire.js +44 -0
  85. package/package.json +13 -3
@@ -25,24 +25,10 @@
25
25
  */
26
26
  import { z } from 'zod';
27
27
  import { blueprintDraftSchema, handshakeSuggestionSchema, } from './handshake-suggestion.js';
28
- import { dataContractSchema } from './data-contract.js';
29
- import { blueprintVarianceSchema } from './blueprint.js';
30
- // ── Shared Sub-Schemas ──
31
- export const viewportSchema = z.object({
32
- width: z.number(),
33
- height: z.number(),
34
- });
35
- export const interfaceContextSchema = z.object({
36
- viewport: viewportSchema,
37
- platform: z.enum(['web', 'mobile', 'desktop']),
38
- deviceType: z.enum(['phone', 'tablet', 'desktop']),
39
- orientation: z.enum(['portrait', 'landscape']),
40
- devicePixelRatio: z.number().optional(),
41
- touchPrimary: z.boolean().optional(),
42
- shellType: z.enum(['chat', 'fullscreen', 'spatial']).optional(),
43
- colorScheme: z.enum(['light', 'dark']).optional(),
44
- reducedMotion: z.boolean().optional(),
45
- }).passthrough();
28
+ import { dataContractSchema, jsonObjectSchema, jsonValueSchema } from './data-contract.js';
29
+ import { blueprintVarianceSchema, blueprintSourceSchema } from './blueprint.js';
30
+ import { MCP_ENDPOINT_REFUSAL_CODES, REFUSAL_RETRIES, RENDER_GATE_REFUSAL_CODES, } from '../types/refusal-codes.js';
31
+ import { RUNTIME_TELEMETRY_MAX_EVENTS } from './runtime-telemetry-limits.js';
46
32
  // ── Wired Tool Input Shapes ──
47
33
  //
48
34
  // Raw zod shapes for the non-lifecycle tools. The SHAPE is the canonical
@@ -325,9 +311,14 @@ export const renderInputShape = {
325
311
  /**
326
312
  * Typed `infra` envelope. Today carries one field (`model`); future
327
313
  * expansion (temperature, max_tokens, provider hints) lands here
328
- * additively. `model` MUST be a provider-prefixed id
329
- * (`provider/model-name`); a bound generator may also accept
330
- * generator-specific prefixes for alternate transports.
314
+ * additively. `model` MUST parse as a model route in either wire form —
315
+ * canonical `provider:model` or LiteLLM `provider/model` (aliases resolve
316
+ * in both). The mechanism is `renderInputEnvelopeSchema`
317
+ * (`schemas/render-input-envelope.ts`), which the render handler parses
318
+ * BEFORE its pre-generation gate; this registered shape stays
319
+ * parser-free by design so a browser bundle never carries the route
320
+ * tables (ggui#818). A bound generator may also accept generator-specific
321
+ * prefixes for alternate transports.
331
322
  *
332
323
  * Strict — extra keys at `infra.*` are not silently dropped, so a
333
324
  * typo (`infra.modelId`) surfaces as a clear zod path instead of a
@@ -339,7 +330,7 @@ export const renderInputShape = {
339
330
  .string()
340
331
  .min(1)
341
332
  .optional()
342
- .describe('Provider-prefixed model id (e.g., `anthropic/claude-haiku-4-5`, `openai/gpt-5`). Generator-specific prefixes (e.g., `bedrock/...` for AWS Bedrock routing) supported when the bound generator handles them.'),
333
+ .describe('Model route in either wire form — canonical `anthropic:claude-haiku-4-5-20251001` or LiteLLM `anthropic/claude-haiku-4-5` (aliases resolve in both); generator-specific prefixes (e.g. `bedrock/...`) route to that transport. A value that parses in neither form fails the handler input parse at `infra.model`, before any pre-generation gate; `model_not_in_tier` is only ever a well-formed route with no rate row on the effective tier.'),
343
334
  })
344
335
  .strict()
345
336
  .optional(),
@@ -445,6 +436,177 @@ export const renderErrorSchema = z.object({
445
436
  .string()
446
437
  .describe('Human-readable failure detail — fold into the next attempt or surface to the operator.'),
447
438
  });
439
+ /**
440
+ * The three outcomes a `ggui_render` result can report (SPEC §7.1,
441
+ * ggui#786). A reader branches on this rather than guessing from which
442
+ * fields happen to be present:
443
+ *
444
+ * - `rendered` — the render ran and produced an interface. The
445
+ * identity fields (`sessionId`, `action`, `contractHash`,
446
+ * `blueprintId`, `variantKey`, `cache`) are all present.
447
+ * - `failed` — generation RAN and did not produce a component. The
448
+ * error session IS committed, so the identity fields are present
449
+ * and `error` carries the classification. The handshake is
450
+ * consumed.
451
+ * - `refused` — the deployment declined the call BEFORE it did any
452
+ * work: no state read, nothing committed, no spend. (The SDK has
453
+ * already checked the call against the declared `inputSchema` —
454
+ * the claim is nothing READ, not nothing validated.) The identity
455
+ * fields are structurally ABSENT and `refusal` carries the whole
456
+ * story; the handshake is INTACT, so the same id is valid on a
457
+ * retry.
458
+ *
459
+ * Declared HERE rather than in `types/mcp.ts` (which re-exports the
460
+ * inferred type) so `types/render.ts` can reference it without a
461
+ * type-only import cycle through `types/mcp.ts` — same convention as
462
+ * {@link renderErrorCodeSchema}.
463
+ */
464
+ export const renderOutcomeSchema = z.enum(['rendered', 'failed', 'refused']);
465
+ /**
466
+ * In-result PRE-GENERATION REFUSAL marker (SPEC §7.1's refused arm,
467
+ * ggui#786). Present iff `outcome: 'refused'`.
468
+ *
469
+ * `code` draws from the closed render-gate subset of
470
+ * `PRE_GENERATION_REFUSAL_CODES` — a code that is not registered fails
471
+ * this enum at the transport, loudly. It is a bug, never a wire state.
472
+ *
473
+ * An agent MUST NOT auto-retry an `after-fix` refusal whose registry
474
+ * row names a `fixBy` other than `caller`: the fix belongs to the app's
475
+ * owner, the tenant, or the operator, and retrying does not perform it.
476
+ */
477
+ /**
478
+ * The refusal projection every surface shares — what a client acts on:
479
+ * the diagnostic, the one recovery step, and how the call becomes
480
+ * possible again. Defined ONCE and spread into each surface's envelope
481
+ * (the render gate's {@link renderRefusalSchema}, the per-app endpoint's
482
+ * {@link transportRefusalSchema}); the `code` enum is per surface,
483
+ * derived from the registry. `fixBy` never travels — it is a registry
484
+ * attribute a client reads by `code`.
485
+ */
486
+ const refusalProjectionFields = {
487
+ message: z
488
+ .string()
489
+ .describe('Precise diagnostic — what was checked, and against what. Surface it to the operator; do not parse it.'),
490
+ fix: z
491
+ .string()
492
+ .describe('The one recovery step, addressed to the party that can take it. Retry the same call only when that party is the caller.'),
493
+ retry: z
494
+ .enum(REFUSAL_RETRIES)
495
+ .describe("How the call becomes possible again. 'after-fix': a named party acts and the same call then succeeds. 'next-period': time restores it at the next period boundary. 'later': transient — retry after a short delay. 'never': no caller action restores it under this identity."),
496
+ };
497
+ export const renderRefusalSchema = z.object({
498
+ code: z
499
+ .enum(RENDER_GATE_REFUSAL_CODES)
500
+ .describe("Registered refusal state. Look the code up in the protocol's refusal registry for its retry class and which party can act; the accompanying `fix` names the one recovery step."),
501
+ ...refusalProjectionFields,
502
+ handshake: z
503
+ .literal('intact')
504
+ .describe('The handshake was NOT consumed — nothing was read. The same handshakeId is valid on a retry.'),
505
+ balanceCentsAtCheck: z
506
+ .number()
507
+ .int()
508
+ .optional()
509
+ .describe('Present only when the refusing check read a balance: its value at the moment of the check.'),
510
+ });
511
+ /**
512
+ * A refusal typed on the per-app MCP endpoint's authorization
513
+ * (ggui#825) — the registry projection WITHOUT the render-only fields:
514
+ * no `handshake` (nothing was handed), no `balanceCentsAtCheck`. Strict:
515
+ * a render-only field here is a bug, never a wire state. `code` draws
516
+ * from {@link MCP_ENDPOINT_REFUSAL_CODES} — today exactly
517
+ * `app_deprovisioned`, the one refusal with a tenant-side fix and
518
+ * therefore the one that MUST be legible where a deleted app and a bad
519
+ * credential would otherwise look alike.
520
+ */
521
+ export const transportRefusalSchema = z.strictObject({
522
+ code: z
523
+ .enum(MCP_ENDPOINT_REFUSAL_CODES)
524
+ .describe("Registered refusal state on the per-app endpoint. Look the code up in the protocol's refusal registry for its retry class and which party can act."),
525
+ ...refusalProjectionFields,
526
+ });
527
+ /**
528
+ * The JSON-RPC error object a per-app MCP endpoint answers with when it
529
+ * refuses a request for a typed reason (ggui#825, codes ruled in
530
+ * ggui#836): HTTP 403, `code` `-32003` (`APP_NOT_FOUND` — the endpoint
531
+ * no longer serves this app, the same reading ggui's embed host gives a
532
+ * proxy 403) and `message` `App not found`, plus `data.refusal`, which
533
+ * makes it legible. `data` is strict: it carries the refusal and nothing
534
+ * else. An authorization failure that is not a registry state answers
535
+ * HTTP 403 with `-32001` (`UNAUTHORIZED`) and NO `data` — the three
536
+ * untyped arms stay indistinguishable among themselves by contract:
537
+ * naming any of them would say which is true. A first-party server
538
+ * never chooses `-32000`: it is the SDK client's `ConnectionClosed`, so
539
+ * a bare 403 and a dropped socket would share a number.
540
+ */
541
+ export const transportRefusalErrorSchema = z.strictObject({
542
+ code: z.literal(-32003),
543
+ message: z.literal('App not found'),
544
+ data: z.strictObject({ refusal: transportRefusalSchema }),
545
+ });
546
+ /**
547
+ * The COMPLETE structuredContent of a refused tool result — the whole
548
+ * payload, not a slice of it. Strict on purpose: a refusal commits
549
+ * nothing, so ANY other key (a `sessionId`, a `resourceUri`, an
550
+ * `error`) means the projection leaked state that does not exist.
551
+ *
552
+ * Today `ggui_render` is the only tool that carries a refusing gate;
553
+ * {@link renderOutputSchema} delegates its refused arm here rather than
554
+ * restating the rules, so there is exactly ONE declaration of them.
555
+ *
556
+ * NOT reusable verbatim by a second refusing tool, despite the strict
557
+ * shape reading as generic: {@link renderRefusalSchema} REQUIRES
558
+ * `handshake: 'intact'`, and that field is render-only by
559
+ * construction — a mutation consumes no handshake, so it has nothing
560
+ * to report intact. A mutation arm therefore lands WITH its first
561
+ * emitter, sharing the facts this envelope carries (`code`, `message`,
562
+ * `fix`, `retry`, one closed registry) and carrying no `handshake`
563
+ * field at all (ggui#798).
564
+ */
565
+ export const refusedOutputSchema = z.strictObject({
566
+ outcome: z.literal('refused'),
567
+ refusal: renderRefusalSchema,
568
+ });
569
+ /**
570
+ * The refused arm's presence rule: the WHOLE payload must be
571
+ * {@link refusedOutputSchema}. Delegating here rather than restating
572
+ * the rule inline keeps one declaration of the RENDER refused
573
+ * envelope. A mutation arm cannot delegate to it — that schema
574
+ * REQUIRES `handshake: 'intact'` — so it mints its own with its first
575
+ * emitter, sharing the facts and carrying no `handshake` (ggui#798).
576
+ */
577
+ function refineRefusedArm(value, ctx) {
578
+ if (refusedOutputSchema.safeParse(value).success)
579
+ return;
580
+ ctx.addIssue({
581
+ code: 'custom',
582
+ message: "outcome 'refused' MUST carry a registered `refusal` and nothing else — a refusal commits nothing, so no identity field and no `error` may appear beside it.",
583
+ });
584
+ }
585
+ /**
586
+ * Report a field the committed arms (`rendered` / `failed`) require but
587
+ * the payload omits. The fields are optional at the schema level only
588
+ * to make room for the refused arm; demoting them must not weaken the
589
+ * committed arms, which is what this restores.
590
+ */
591
+ function requireOnCommittedArm(present, field, outcome, ctx) {
592
+ if (present)
593
+ return;
594
+ ctx.addIssue({
595
+ code: 'custom',
596
+ path: [field],
597
+ message: `outcome '${outcome}' MUST carry \`${field}\` — a committed render reports its full identity.`,
598
+ });
599
+ }
600
+ /** `refusal` rides the refused arm only. */
601
+ function rejectRefusalOnCommittedArm(refusal, outcome, ctx) {
602
+ if (refusal === undefined)
603
+ return;
604
+ ctx.addIssue({
605
+ code: 'custom',
606
+ path: ['refusal'],
607
+ message: `\`refusal\` is present only on outcome 'refused', not '${outcome}'.`,
608
+ });
609
+ }
448
610
  /**
449
611
  * Canonical failure codes for a `resources/read` on a render locator
450
612
  * (`ui://ggui/render/{sessionId}/{blueprintKey}`). Closed enum.
@@ -498,9 +660,10 @@ export const resourceReadErrorSchema = z.object({
498
660
  .describe('Extra diagnostic context for operators. Dropped on NOT_FOUND so a denied read cannot be told apart from a miss.'),
499
661
  });
500
662
  /**
501
- * Wire-output shape — `{sessionId, resourceUri?, action, contractHash,
502
- * cache, error?, nextStep?}`. `contractHash` (data-contract identity)
503
- * and `cache` (reuse outcome) are required wire fields on this schema.
663
+ * Wire-output shape. `outcome` is the discriminant and the only
664
+ * unconditionally required field; which of the others exist follows
665
+ * from it, per the THREE OUTCOMES section below — that section is the
666
+ * single summary of this shape, so do not restate it here.
504
667
  * The handler carries `shortCode`, `codeReady`, `handshakeId`,
505
668
  * `decision`, `contract`, `codeUrl`, `codeHash`
506
669
  * on its internal `RenderOutput` TS shape for telemetry / post-classify
@@ -514,17 +677,38 @@ export const resourceReadErrorSchema = z.object({
514
677
  * `render-resource/...`). Leaving a dead URL on the wire had the model
515
678
  * hallucinating links that resolve nowhere.
516
679
  *
517
- * Failure envelope (SPEC §7.1): a failed/rejected generation returns
518
- * this same schema-conformant shape on an `isError: true` tool result —
519
- * `error` present, `resourceUri` absent (nothing mountable), no
520
- * `_meta` on the result. The error GguiSession is still committed, so
521
- * `sessionId` remains a live handle into the session channel.
680
+ * THREE OUTCOMES (SPEC §7.1, ggui#786). Every result carries
681
+ * {@link renderOutcomeSchema} on `outcome`, and the identity fields are
682
+ * present IFF something was committed — which is why they are optional
683
+ * at the schema level and pinned by the presence refinement below:
684
+ *
685
+ * - `rendered` — identity fields present; `resourceUri` present iff
686
+ * mountable; no `error`, no `refusal`.
687
+ * - `failed` — generation ran and produced nothing. Identity fields
688
+ * present (the error GguiSession IS committed, so `sessionId`
689
+ * remains a live handle into the session channel), `error`
690
+ * present, `resourceUri` absent, no `_meta` on the result. The
691
+ * handshake is consumed.
692
+ * - `refused` — the deployment declined before doing any work.
693
+ * Identity fields ABSENT, `refusal` present, no `error`, no
694
+ * `nextStep`, no `_meta`. Nothing was committed and the handshake
695
+ * is intact. The refused arm's whole envelope is
696
+ * {@link refusedOutputSchema}.
697
+ *
698
+ * The root stays ONE object with a discriminant field rather than a
699
+ * discriminated union: the MCP spec's `Tool.outputSchema` root MUST be
700
+ * a JSON Schema of type `object`, and the SDK registers zod raw shapes.
701
+ * TypeScript narrowing is via the guards at the bottom of this file
702
+ * ({@link isRenderedOutput} / {@link isFailedRenderOutput} /
703
+ * {@link isRefusedRenderOutput}), never a parallel union type.
522
704
  *
523
705
  * Post-Phase-B the `'compose'` action enum value is gone — there is no
524
706
  * stack of N renders to compose against.
525
707
  */
526
708
  export const renderOutputSchema = z.object({
527
- sessionId: z.string(),
709
+ outcome: renderOutcomeSchema.describe("Which of the three outcomes this result reports. 'rendered': an interface was produced. 'failed': generation ran and produced none — the session is committed and `error` classifies it. 'refused': the deployment declined before doing any work — `refusal` carries the state, nothing was committed, and the handshake is still valid."),
710
+ /** Present iff something was committed — absent on a refusal. */
711
+ sessionId: z.string().optional(),
528
712
  /**
529
713
  * Spec-canonical MCP-Apps entry-point — same `ui://ggui/render/{id}`
530
714
  * URI surfaced on `_meta.ui.resourceUri`. Surfacing it on the LLM-
@@ -541,17 +725,24 @@ export const renderOutputSchema = z.object({
541
725
  .string()
542
726
  .optional()
543
727
  .describe('MCP-Apps mount URI (ui://ggui/render/{id}). Present iff the render is mountable; absent on a failed render.'),
544
- action: z.enum(['create', 'reuse', 'update', 'replace', 'declined']),
728
+ action: z
729
+ .enum(['create', 'reuse', 'update', 'replace', 'declined'])
730
+ .optional(),
545
731
  contractHash: z
546
732
  .string()
733
+ .optional()
547
734
  .describe('Canonical hash of the rendered data contract (shape only — fields, types, specs). Same hash ⟺ same data flow.'),
548
735
  blueprintId: z
549
736
  .string()
737
+ .optional()
550
738
  .describe('Opaque id of the materialised component for this render. On the handshake-decided reuse paths (accept a cache-origin proposal, or a variance re-aim that resolves to an existing variant) it is the stored id — equal ids across renders mean the same stored component. override.contract always generates cold and mints a fresh id, even for an identical contract.'),
551
739
  variantKey: z
552
740
  .string()
741
+ .optional()
553
742
  .describe('Canonical hash of the design-time variance (persona, aesthetic, seed prompt, context). With contractHash it forms the reuse key: the same pair reuses one component; a different variant of the same contract gets its own.'),
554
- cache: renderCacheMarkerSchema.describe('Reuse outcome for this render: whether a stored component was served, its similarity, the matched component id, and how many generation calls that avoided.'),
743
+ cache: renderCacheMarkerSchema
744
+ .optional()
745
+ .describe('Reuse outcome for this render: whether a stored component was served, its similarity, the matched component id, and how many generation calls that avoided.'),
555
746
  /**
556
747
  * In-result failure marker — present iff the tool result is
557
748
  * `isError: true`. The structuredContent stays schema-conformant on
@@ -561,6 +752,14 @@ export const renderOutputSchema = z.object({
561
752
  error: renderErrorSchema
562
753
  .optional()
563
754
  .describe('Present iff the tool result is isError — canonical {code, message} for a failed/rejected generation. Absent on success.'),
755
+ /**
756
+ * In-result PRE-GENERATION REFUSAL marker — present iff
757
+ * `outcome: 'refused'`, and then it is the ONLY field besides
758
+ * `outcome` (see {@link refusedOutputSchema}).
759
+ */
760
+ refusal: renderRefusalSchema
761
+ .optional()
762
+ .describe('Present iff outcome is refused — the registered state the deployment declined on, plus the one recovery step. Nothing was committed and the handshake is still valid.'),
564
763
  /**
565
764
  * Wire-shape recovery hint for the next call. Emitted ONLY when the
566
765
  * rendered contract has a non-empty `actionSpec` — i.e. the agent will
@@ -586,6 +785,56 @@ export const renderOutputSchema = z.object({
586
785
  timeout: z.number(),
587
786
  }),
588
787
  }).optional().describe('Required-next-call hint — when the rendered contract has actions, points the agent at ggui_consume({sessionId, timeout}) for the inbound action loop. Absent for pure-display renders.'),
788
+ }).superRefine((value, ctx) => {
789
+ // Present-iff-committed. NOTE for implementors: this refinement is
790
+ // attached to the COMPOSED schema. A consumer that decomposes the
791
+ // schema to its raw shape and rebuilds it (`z.object(schema.shape)`,
792
+ // which is how the MCP SDK registers a tool's outputSchema) loses
793
+ // these rules — which is why the transport validates a refused
794
+ // payload against `refusedOutputSchema` directly. See SPEC §7.1.
795
+ if (value.outcome === 'refused') {
796
+ refineRefusedArm(value, ctx);
797
+ return;
798
+ }
799
+ requireOnCommittedArm(value.sessionId !== undefined, 'sessionId', value.outcome, ctx);
800
+ requireOnCommittedArm(value.action !== undefined, 'action', value.outcome, ctx);
801
+ requireOnCommittedArm(value.contractHash !== undefined, 'contractHash', value.outcome, ctx);
802
+ requireOnCommittedArm(value.blueprintId !== undefined, 'blueprintId', value.outcome, ctx);
803
+ requireOnCommittedArm(value.variantKey !== undefined, 'variantKey', value.outcome, ctx);
804
+ requireOnCommittedArm(value.cache !== undefined, 'cache', value.outcome, ctx);
805
+ rejectRefusalOnCommittedArm(value.refusal, value.outcome, ctx);
806
+ // `resourceUri` present IFF `rendered` — the mountability rule, the
807
+ // one presence rule the six identity fields do not cover. A `failed`
808
+ // render commits an error GguiSession but exposes no mount, so a URI
809
+ // beside it points a host at a render that does not exist.
810
+ if (value.outcome === 'rendered' && value.resourceUri === undefined) {
811
+ ctx.addIssue({
812
+ code: 'custom',
813
+ path: ['resourceUri'],
814
+ message: "outcome 'rendered' MUST carry `resourceUri` — a rendered result is mountable, and the URI is how a host mounts it.",
815
+ });
816
+ }
817
+ if (value.outcome === 'failed' && value.resourceUri !== undefined) {
818
+ ctx.addIssue({
819
+ code: 'custom',
820
+ path: ['resourceUri'],
821
+ message: "`resourceUri` is present only on outcome 'rendered' — a failed render exposes no mount affordance.",
822
+ });
823
+ }
824
+ if (value.outcome === 'failed' && value.error === undefined) {
825
+ ctx.addIssue({
826
+ code: 'custom',
827
+ path: ['error'],
828
+ message: "outcome 'failed' MUST carry `error` — the classification is what distinguishes it from a render that succeeded.",
829
+ });
830
+ }
831
+ if (value.outcome === 'rendered' && value.error !== undefined) {
832
+ ctx.addIssue({
833
+ code: 'custom',
834
+ path: ['error'],
835
+ message: "`error` is present only on outcome 'failed', not 'rendered'.",
836
+ });
837
+ }
589
838
  });
590
839
  /**
591
840
  * `ggui_update` — refresh the rendered UI with new state.
@@ -654,9 +903,22 @@ export const amendInputSchema = z.discriminatedUnion('kind', [
654
903
  }).strict(),
655
904
  ]);
656
905
  /**
657
- * Wire-output shape — minimal acknowledgement. The handler carries
658
- * `decision`, `contract`, `contractHash` on its internal `UpdateOutput`
659
- * TS shape — zod strips them before structuredContent serialization.
906
+ * Agent-facing description of the schema-attestation hash, shared by
907
+ * both mutation tools (ggui#560). It is not a comment: `.describe()`
908
+ * ships as JSON-Schema `description` in the tool declaration, so this
909
+ * string is what an agent reads out of `tools/list` when it has to
910
+ * decide whether a hash mismatch means the contract moved under it.
911
+ * Declared once so the two tools cannot say different things about the
912
+ * same field.
913
+ */
914
+ const PROPS_SCHEMA_HASH_DESCRIPTION = 'sha256 (lowercase hex) over the RFC 8785 canonical form of the enforced props schema this mutation was validated against — the same schema the paired handshake disclosed. Present when the session declares a propsSpec. Equal to the handshake propsSchemaHash by the session-continuity guarantee; a mismatch means the contract changed under you.';
915
+ /** Agent-facing description of the grammar-profile classifier. Same rationale as {@link PROPS_SCHEMA_HASH_DESCRIPTION}. */
916
+ const PROPS_SCHEMA_PROFILE_DESCRIPTION = "Grammar profile of the enforced props schema: 'grammar-safe' or 'full'. Present with propsSchemaHash; treat unrecognized values as 'full'.";
917
+ /**
918
+ * Wire-output shape — minimal acknowledgement. This schema IS the
919
+ * handler's declared output: `ggui_update` registers `.shape` and its
920
+ * return type is `GguiUpdateOutput` (`z.infer` of this schema), so
921
+ * there is no second declaration to keep in step (ggui#798).
660
922
  *
661
923
  * Every REAL update (`updated: true`) mints a new history record and
662
924
  * its result carries the `ai.ggui/render` slice as a FULL bootable
@@ -666,6 +928,15 @@ export const amendInputSchema = z.discriminatedUnion('kind', [
666
928
  * history when its higher-epoch `props_update` frame lands; the
667
929
  * in-place repaint over the live-channel ladder (WS / SSE / polling /
668
930
  * bridge-pull) is `ggui_amend`'s job.
931
+ *
932
+ * NO refusal arm (ggui#786): the pre-generation refusal envelope rides
933
+ * `ggui_render` only. There is no pre-state POLICY gate on the
934
+ * mutation tools today — nothing on this path can decline a call for
935
+ * deployment-policy reasons (a contract error still rejects a
936
+ * malformed mutation before any store read; that is a parse result,
937
+ * not a refusal) — and `handshake: 'intact'` would assert something
938
+ * meaningless on a tool that consumes no handshake. The arm lands with its first
939
+ * emitter (ggui#798), not as a mechanical port of the render one.
669
940
  */
670
941
  export const updateOutputSchema = z.object({
671
942
  sessionId: z.string(),
@@ -676,8 +947,7 @@ export const updateOutputSchema = z.object({
676
947
  * no-op (`updated: false`) no record is minted and this is the bare
677
948
  * live-head URI. Mirrored on the LLM-visible structuredContent so
678
949
  * SDKs that strip `_meta` from tool_results can still reach the
679
- * mount URI. Kept in sync with the update handler's wire shape —
680
- * this export and the handler's inline schema must not drift.
950
+ * mount URI.
681
951
  */
682
952
  resourceUri: z.string(),
683
953
  /**
@@ -707,9 +977,15 @@ export const updateOutputSchema = z.object({
707
977
  * session-continuity guarantee, so a mismatch is the observable form
708
978
  * of a contract changing mid-session.
709
979
  */
710
- propsSchemaHash: z.string().optional(),
980
+ propsSchemaHash: z
981
+ .string()
982
+ .optional()
983
+ .describe(PROPS_SCHEMA_HASH_DESCRIPTION),
711
984
  /** Present with `propsSchemaHash`; same profile classifier as the handshake's. */
712
- propsSchemaProfile: z.string().optional(),
985
+ propsSchemaProfile: z
986
+ .string()
987
+ .optional()
988
+ .describe(PROPS_SCHEMA_PROFILE_DESCRIPTION),
713
989
  });
714
990
  /**
715
991
  * `ggui_amend` wire output (#483) — acknowledgement only. `resourceUri`
@@ -717,17 +993,49 @@ export const updateOutputSchema = z.object({
717
993
  * mints a record, so there is no pinned URI to return and no epoch
718
994
  * field — the history number is untouched by construction). The
719
995
  * mounted card receives the new props over the live channels.
996
+ *
997
+ * As with {@link updateOutputSchema}, this schema IS the handler's
998
+ * declared output — `ggui_amend` registers `.shape` and returns
999
+ * `GguiAmendOutput` (ggui#798).
720
1000
  */
721
1001
  export const amendOutputSchema = z.object({
722
1002
  sessionId: z.string(),
723
1003
  updated: z.boolean(),
1004
+ /**
1005
+ * The BARE live-head URI — amend targets the mounted card and never
1006
+ * mints a record, so there is no pinned URI to return and no epoch
1007
+ * field (the history number is untouched by construction).
1008
+ *
1009
+ * NORMATIVE re-anchor reference (SPEC §7.1.2.1, ggui#652 /
1010
+ * guuey#535): together with `sessionId` this is the durable record
1011
+ * that an in-place repaint touched this session at this turn. A
1012
+ * host's persistence layer MAY consume it as a locator-only
1013
+ * re-anchor — `resources/read`-resolvable, stable for the session's
1014
+ * lifetime — so a restored transcript re-positions the card at its
1015
+ * latest referencing turn and rehydrates CURRENT state instead of a
1016
+ * stale earlier snapshot. It rides `structuredContent` (LLM-visible,
1017
+ * same rationale as ggui_update's resourceUri: consumers that strip
1018
+ * `_meta` still reach it) and MUST NOT move to a result `_meta`
1019
+ * slice — any result `_meta` on this tool makes view-minting hosts
1020
+ * break the in-place semantics.
1021
+ *
1022
+ * Both this field and `sessionId` are REQUIRED, which is what makes
1023
+ * SPEC §7.1.2.1's "structurally guaranteed" true rather than a
1024
+ * convention.
1025
+ */
724
1026
  resourceUri: z.string(),
725
1027
  /** Same no-op feedback channel as ggui_update's `warning`. */
726
1028
  warning: z.string().optional(),
727
1029
  /** Schema attestation (ggui#560) — same semantics as ggui_update's. */
728
- propsSchemaHash: z.string().optional(),
1030
+ propsSchemaHash: z
1031
+ .string()
1032
+ .optional()
1033
+ .describe(PROPS_SCHEMA_HASH_DESCRIPTION),
729
1034
  /** Present with `propsSchemaHash`. */
730
- propsSchemaProfile: z.string().optional(),
1035
+ propsSchemaProfile: z
1036
+ .string()
1037
+ .optional()
1038
+ .describe(PROPS_SCHEMA_PROFILE_DESCRIPTION),
731
1039
  });
732
1040
  /**
733
1041
  * `ggui_runtime_declare_tool_catalog` — the host runtime declares its
@@ -898,11 +1206,6 @@ export const runtimePullOutputSchema = z.union([
898
1206
  runtimePullEventsPageSchema,
899
1207
  runtimePullHorizonSchema,
900
1208
  ]);
901
- /**
902
- * Per-batch event cap on `ggui_runtime_telemetry` — a bounded
903
- * fire-and-forget diagnostic channel, never a data plane.
904
- */
905
- export const RUNTIME_TELEMETRY_MAX_EVENTS = 40;
906
1209
  /**
907
1210
  * `ggui_runtime_telemetry` input — the iframe runtime's transport
908
1211
  * self-report (`_meta.ui.visibility: ['app']`, view-callable only).
@@ -951,3 +1254,207 @@ export const runtimeTelemetryInputShape = {
951
1254
  export const runtimeTelemetryInputSchema = z.object(runtimeTelemetryInputShape);
952
1255
  /** `ggui_runtime_telemetry` output — bare acknowledgement. */
953
1256
  export const runtimeTelemetryOutputSchema = z.object({ ok: z.literal(true) });
1257
+ /** A render that produced an interface — identity fields present. */
1258
+ export function isRenderedOutput(output) {
1259
+ return output.outcome === 'rendered';
1260
+ }
1261
+ /**
1262
+ * A generation that RAN and produced nothing. The error session is
1263
+ * committed, so `sessionId` is a live handle and `error` classifies it.
1264
+ */
1265
+ export function isFailedRenderOutput(output) {
1266
+ return output.outcome === 'failed';
1267
+ }
1268
+ /**
1269
+ * A PRE-GENERATION refusal — nothing read and nothing committed, so
1270
+ * every identity field is absent and `refusal` carries the state. (Not
1271
+ * "nothing parsed": the SDK has already checked the call against the
1272
+ * tool's declared `inputSchema` by the time a gate can refuse it.)
1273
+ */
1274
+ export function isRefusedRenderOutput(output) {
1275
+ return output.outcome === 'refused';
1276
+ }
1277
+ // ============================================================================
1278
+ // Tool output schemas the protocol owns (#817 part C). A handler registers
1279
+ // `<schema>.shape` as its MCP `outputSchema` and derives its output type from
1280
+ // the schema — never a parallel interface. No `.readonly()` here: zod 4
1281
+ // projects it as `readOnly` into the advertised JSON Schema, and the wire is
1282
+ // mutable JSON; readonly is applied at the seam (`DeepReadonly` on the
1283
+ // derived types). Every shape is closed: the transport strip-parses against
1284
+ // `.shape`, so the shape IS the wire.
1285
+ // ============================================================================
1286
+ /** Display modes an MCP Apps host can render a view in (ext-apps vocabulary). */
1287
+ export const mcpUiDisplayModeSchema = z.enum(['inline', 'fullscreen', 'pip']);
1288
+ /**
1289
+ * The host-context projection the iframe-runtime observes and `ggui_consume`
1290
+ * echoes (`client.hostContext`). Every field optional: a host that never
1291
+ * reports one leaves it absent — absent ⇒ the documented default.
1292
+ */
1293
+ export const hostContextProjectionSchema = z.object({
1294
+ availableDisplayModes: z.array(mcpUiDisplayModeSchema).optional(),
1295
+ currentDisplayMode: mcpUiDisplayModeSchema.optional(),
1296
+ containerDimensions: z
1297
+ .object({
1298
+ width: z.number().optional(),
1299
+ maxWidth: z.number().optional(),
1300
+ height: z.number().optional(),
1301
+ maxHeight: z.number().optional(),
1302
+ })
1303
+ .optional(),
1304
+ platform: z.enum(['web', 'desktop', 'mobile']).optional(),
1305
+ deviceCapabilities: z
1306
+ .object({ touch: z.boolean().optional(), hover: z.boolean().optional() })
1307
+ .optional(),
1308
+ locale: z.string().optional(),
1309
+ timeZone: z.string().optional(),
1310
+ });
1311
+ /** `ggui_consume`'s `client` slice — what the runtime observed about its host. */
1312
+ export const clientObservationsSchema = z.object({
1313
+ hostContext: hostContextProjectionSchema.optional(),
1314
+ });
1315
+ /** One row of `ggui_list_sessions` — eight closed keys; nothing passes through. */
1316
+ export const gguiSessionSummaryWireSchema = z.object({
1317
+ sessionId: z.string(),
1318
+ hostName: z.string().optional(),
1319
+ hostSessionId: z.string().optional(),
1320
+ createdAt: z.string(),
1321
+ lastActivityAt: z.string(),
1322
+ status: z.string(),
1323
+ wsToken: z.string().optional(),
1324
+ wsTokenExpiresAt: z.string().optional(),
1325
+ });
1326
+ /**
1327
+ * The two states a GguiSession is in on the wire — the pair the consume
1328
+ * loop exits on (`expired`). Owned here (ggui#817 part C2); the type is
1329
+ * derived, never a second list.
1330
+ */
1331
+ export const gguiSessionStatusSchema = z.enum(['active', 'expired']);
1332
+ /**
1333
+ * One drained row of `ggui_consume` — a user action that reached the
1334
+ * pipe (ggui#817 part C2). Closed on the wire: an unknown key is
1335
+ * stripped at the transport, a missing key refuses the row at the seam
1336
+ * (`parsePendingEnvelope`), so a malformed pipe entry never ships to an
1337
+ * agent typed as a good one.
1338
+ */
1339
+ export const consumeEventEntrySchema = z.object({
1340
+ type: z.literal('action'),
1341
+ sessionId: z.string().min(1),
1342
+ intent: z.string(),
1343
+ actionData: jsonValueSchema.nullable(),
1344
+ uiContext: jsonObjectSchema,
1345
+ actionId: z.string(),
1346
+ firedAt: z.string(),
1347
+ });
1348
+ /**
1349
+ * `ggui_consume`'s output — the drained rows, the session's state, and the
1350
+ * client's observations when the host sent any (ggui#817 part C2). The
1351
+ * handler registers `.shape`; `tools/list` therefore advertises the entry
1352
+ * vocabulary and the status enum instead of a free-form record and a free
1353
+ * string.
1354
+ */
1355
+ export const gguiConsumeOutputSchema = z.object({
1356
+ events: z.array(consumeEventEntrySchema),
1357
+ status: gguiSessionStatusSchema,
1358
+ client: clientObservationsSchema.optional(),
1359
+ });
1360
+ /** `ggui_list_sessions`' output — the closed summary rows (ggui#817 part C2). */
1361
+ export const gguiListSessionsOutputSchema = z.object({
1362
+ sessions: z.array(gguiSessionSummaryWireSchema),
1363
+ });
1364
+ /**
1365
+ * `ggui_emit`'s output (ggui#817 part C2): `accepted` at the boundary, and
1366
+ * `seq` when the server keeps a stream buffer — seq-aware implementations
1367
+ * stamp and return it so replay cursors can be built from the ack.
1368
+ */
1369
+ export const gguiEmitOutputSchema = z.object({
1370
+ accepted: z.boolean(),
1371
+ seq: z
1372
+ .number()
1373
+ .int()
1374
+ .nonnegative()
1375
+ .optional()
1376
+ .describe('Session-scoped monotonic outbound sequence assigned to this delivery. Present when the server keeps a stream buffer.'),
1377
+ });
1378
+ /**
1379
+ * `ggui_get_session`'s wire: the store row's six base fields plus the mount
1380
+ * variant — for EVERY session. An MCP-Apps mount is locator-only on the
1381
+ * render object, but its store row carries the base fields, so the
1382
+ * projection reads them from the row and the wire never fails on that
1383
+ * variant. The locator itself is not on this wire (MCP-Apps resources have
1384
+ * their own paths).
1385
+ * `contextSnapshot` rides when a component (`render`) mount's row has one —
1386
+ * never on an mcpApps mount.
1387
+ */
1388
+ export const gguiGetSessionOutputSchema = z.object({
1389
+ variant: z.enum(['render', 'mcpApps']),
1390
+ id: z.string(),
1391
+ appId: z.string(),
1392
+ eventSequence: z.number().int().nonnegative(),
1393
+ createdAt: z.number().int().nonnegative(),
1394
+ lastActivityAt: z.number().int().nonnegative(),
1395
+ expiresAt: z.number().int().nonnegative(),
1396
+ /**
1397
+ * The last-known value of every declared contextSpec slot, as
1398
+ * `ggui_runtime_sync_context` wrote it onto the row — the read path a
1399
+ * raw MCP client (no widget-context mirror) has for contextSpec values.
1400
+ * Present iff the row carries one — component (`render`) mounts only; an
1401
+ * mcpApps mount never carries one; never an empty placeholder.
1402
+ */
1403
+ contextSnapshot: jsonObjectSchema.optional(),
1404
+ });
1405
+ /** The three sequential gates of `ggui_protocol_validate_blueprint`. */
1406
+ export const blueprintValidationTierSchema = z.enum(['compile', 'selfCheck', 'runtime']);
1407
+ export const blueprintValidationIssueSchema = z.object({
1408
+ tier: blueprintValidationTierSchema,
1409
+ code: z.string(),
1410
+ message: z.string(),
1411
+ fix: z.string().optional(),
1412
+ });
1413
+ /** `ggui_protocol_validate_blueprint`'s result envelope: `failedAt` names the tier that stopped, or null. */
1414
+ export const blueprintValidationResultSchema = z.object({
1415
+ valid: z.boolean(),
1416
+ failedAt: blueprintValidationTierSchema.nullable(),
1417
+ errors: z.array(blueprintValidationIssueSchema),
1418
+ warnings: z.array(blueprintValidationIssueSchema),
1419
+ });
1420
+ /** A provider row — what `ggui_list_featured_blueprints` returns per blueprint. */
1421
+ export const blueprintEntryWireSchema = z.object({
1422
+ id: z.string(),
1423
+ name: z.string(),
1424
+ description: z.string().optional(),
1425
+ source: blueprintSourceSchema,
1426
+ updatedAt: z.string(),
1427
+ tags: z.array(z.string()).optional(),
1428
+ });
1429
+ export const gguiListFeaturedBlueprintsOutputSchema = z.object({
1430
+ blueprints: z.array(blueprintEntryWireSchema),
1431
+ total: z.number().int().nonnegative(),
1432
+ });
1433
+ /** One `ggui_search_blueprints` hit — a scored row plus the registry-only keys. */
1434
+ export const gguiSearchBlueprintsResultSchema = z.object({
1435
+ id: z.string(),
1436
+ name: z.string(),
1437
+ description: z.string(),
1438
+ category: z.string(),
1439
+ props: z.array(z.object({ name: z.string(), type: z.string(), required: z.boolean(), description: z.string() })),
1440
+ callbacks: z.array(z.string()),
1441
+ featured: z.boolean(),
1442
+ relevance: z.literal('match'),
1443
+ score: z.number(),
1444
+ origin: z.literal('registry').optional(),
1445
+ artifactId: z.string().optional(),
1446
+ version: z.string().optional(),
1447
+ mcpTools: z.array(z.object({ server: z.string().optional(), tool: z.string() })).optional(),
1448
+ scopeVerification: z.enum(['verified', 'unverified']).optional(),
1449
+ });
1450
+ export const gguiSearchBlueprintsOutputSchema = z.object({
1451
+ results: z.array(gguiSearchBlueprintsResultSchema),
1452
+ total: z.number(),
1453
+ query: z.string(),
1454
+ degradedSources: z
1455
+ .array(z.object({
1456
+ source: z.literal('registry'),
1457
+ reason: z.enum(['unreachable', 'timeout', 'invalid_response']),
1458
+ }))
1459
+ .optional(),
1460
+ });