@ggui-ai/protocol 0.12.0 → 0.14.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 (45) hide show
  1. package/dist/gadgets/stdlib-gadgets.d.ts +1 -1
  2. package/dist/gadgets/stdlib-gadgets.js +1 -1
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +6 -0
  6. package/dist/integrations/mcp-apps.d.ts +63 -11
  7. package/dist/integrations/mcp-apps.d.ts.map +1 -1
  8. package/dist/integrations/mcp-apps.js +73 -3
  9. package/dist/schemas/app-theme.d.ts +5 -0
  10. package/dist/schemas/app-theme.d.ts.map +1 -1
  11. package/dist/schemas/app-theme.js +23 -0
  12. package/dist/schemas/data-contract.d.ts +3 -1
  13. package/dist/schemas/data-contract.d.ts.map +1 -1
  14. package/dist/schemas/data-contract.js +2 -0
  15. package/dist/schemas/invoke.d.ts +2 -2
  16. package/dist/schemas/mcp.d.ts +482 -24
  17. package/dist/schemas/mcp.d.ts.map +1 -1
  18. package/dist/schemas/mcp.js +455 -26
  19. package/dist/schemas/render-input-envelope.d.ts +20 -0
  20. package/dist/schemas/render-input-envelope.d.ts.map +1 -0
  21. package/dist/schemas/render-input-envelope.js +56 -0
  22. package/dist/types/ggui-session-event.d.ts +4 -5
  23. package/dist/types/ggui-session-event.d.ts.map +1 -1
  24. package/dist/types/ggui-session-event.js +0 -37
  25. package/dist/types/host-context.d.ts +17 -16
  26. package/dist/types/host-context.d.ts.map +1 -1
  27. package/dist/types/host-context.js +7 -0
  28. package/dist/types/llm-route.d.ts +2 -2
  29. package/dist/types/llm-route.d.ts.map +1 -1
  30. package/dist/types/llm-route.js +11 -3
  31. package/dist/types/llm.d.ts +397 -7
  32. package/dist/types/llm.d.ts.map +1 -1
  33. package/dist/types/llm.js +111 -10
  34. package/dist/types/mcp.d.ts +16 -53
  35. package/dist/types/mcp.d.ts.map +1 -1
  36. package/dist/types/readonly.d.ts +11 -0
  37. package/dist/types/readonly.d.ts.map +1 -0
  38. package/dist/types/readonly.js +1 -0
  39. package/dist/types/refusal-codes.d.ts +319 -0
  40. package/dist/types/refusal-codes.d.ts.map +1 -0
  41. package/dist/types/refusal-codes.js +329 -0
  42. package/dist/version.d.ts +79 -5
  43. package/dist/version.d.ts.map +1 -1
  44. package/dist/version.js +78 -4
  45. package/package.json +7 -3
@@ -25,8 +25,9 @@
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';
28
+ import { dataContractSchema, jsonObjectSchema } from './data-contract.js';
29
+ import { blueprintVarianceSchema, blueprintSourceSchema } from './blueprint.js';
30
+ import { REFUSAL_RETRIES, RENDER_GATE_REFUSAL_CODES, } from '../types/refusal-codes.js';
30
31
  // ── Shared Sub-Schemas ──
31
32
  export const viewportSchema = z.object({
32
33
  width: z.number(),
@@ -325,9 +326,14 @@ export const renderInputShape = {
325
326
  /**
326
327
  * Typed `infra` envelope. Today carries one field (`model`); future
327
328
  * 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.
329
+ * additively. `model` MUST parse as a model route in either wire form —
330
+ * canonical `provider:model` or LiteLLM `provider/model` (aliases resolve
331
+ * in both). The mechanism is `renderInputEnvelopeSchema`
332
+ * (`schemas/render-input-envelope.ts`), which the render handler parses
333
+ * BEFORE its pre-generation gate; this registered shape stays
334
+ * parser-free by design so a browser bundle never carries the route
335
+ * tables (ggui#818). A bound generator may also accept generator-specific
336
+ * prefixes for alternate transports.
331
337
  *
332
338
  * Strict — extra keys at `infra.*` are not silently dropped, so a
333
339
  * typo (`infra.modelId`) surfaces as a clear zod path instead of a
@@ -339,7 +345,7 @@ export const renderInputShape = {
339
345
  .string()
340
346
  .min(1)
341
347
  .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.'),
348
+ .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
349
  })
344
350
  .strict()
345
351
  .optional(),
@@ -445,6 +451,130 @@ export const renderErrorSchema = z.object({
445
451
  .string()
446
452
  .describe('Human-readable failure detail — fold into the next attempt or surface to the operator.'),
447
453
  });
454
+ /**
455
+ * The three outcomes a `ggui_render` result can report (SPEC §7.1,
456
+ * ggui#786). A reader branches on this rather than guessing from which
457
+ * fields happen to be present:
458
+ *
459
+ * - `rendered` — the render ran and produced an interface. The
460
+ * identity fields (`sessionId`, `action`, `contractHash`,
461
+ * `blueprintId`, `variantKey`, `cache`) are all present.
462
+ * - `failed` — generation RAN and did not produce a component. The
463
+ * error session IS committed, so the identity fields are present
464
+ * and `error` carries the classification. The handshake is
465
+ * consumed.
466
+ * - `refused` — the deployment declined the call BEFORE it did any
467
+ * work: no state read, nothing committed, no spend. (The SDK has
468
+ * already checked the call against the declared `inputSchema` —
469
+ * the claim is nothing READ, not nothing validated.) The identity
470
+ * fields are structurally ABSENT and `refusal` carries the whole
471
+ * story; the handshake is INTACT, so the same id is valid on a
472
+ * retry.
473
+ *
474
+ * Declared HERE rather than in `types/mcp.ts` (which re-exports the
475
+ * inferred type) so `types/render.ts` can reference it without a
476
+ * type-only import cycle through `types/mcp.ts` — same convention as
477
+ * {@link renderErrorCodeSchema}.
478
+ */
479
+ export const renderOutcomeSchema = z.enum(['rendered', 'failed', 'refused']);
480
+ /**
481
+ * In-result PRE-GENERATION REFUSAL marker (SPEC §7.1's refused arm,
482
+ * ggui#786). Present iff `outcome: 'refused'`.
483
+ *
484
+ * `code` draws from the closed render-gate subset of
485
+ * `PRE_GENERATION_REFUSAL_CODES` — a code that is not registered fails
486
+ * this enum at the transport, loudly. It is a bug, never a wire state.
487
+ *
488
+ * An agent MUST NOT auto-retry an `after-fix` refusal whose registry
489
+ * row names a `fixBy` other than `caller`: the fix belongs to the app's
490
+ * owner, the tenant, or the operator, and retrying does not perform it.
491
+ */
492
+ export const renderRefusalSchema = z.object({
493
+ code: z
494
+ .enum(RENDER_GATE_REFUSAL_CODES)
495
+ .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."),
496
+ message: z
497
+ .string()
498
+ .describe('Precise diagnostic — what was checked, and against what. Surface it to the operator; do not parse it.'),
499
+ fix: z
500
+ .string()
501
+ .describe('The one recovery step, addressed to the party that can take it. Retry the same call only when that party is the caller.'),
502
+ retry: z
503
+ .enum(REFUSAL_RETRIES)
504
+ .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."),
505
+ handshake: z
506
+ .literal('intact')
507
+ .describe('The handshake was NOT consumed — nothing was read. The same handshakeId is valid on a retry.'),
508
+ balanceCentsAtCheck: z
509
+ .number()
510
+ .int()
511
+ .optional()
512
+ .describe('Present only when the refusing check read a balance: its value at the moment of the check.'),
513
+ });
514
+ /**
515
+ * The COMPLETE structuredContent of a refused tool result — the whole
516
+ * payload, not a slice of it. Strict on purpose: a refusal commits
517
+ * nothing, so ANY other key (a `sessionId`, a `resourceUri`, an
518
+ * `error`) means the projection leaked state that does not exist.
519
+ *
520
+ * Today `ggui_render` is the only tool that carries a refusing gate;
521
+ * {@link renderOutputSchema} delegates its refused arm here rather than
522
+ * restating the rules, so there is exactly ONE declaration of them.
523
+ *
524
+ * NOT reusable verbatim by a second refusing tool, despite the strict
525
+ * shape reading as generic: {@link renderRefusalSchema} REQUIRES
526
+ * `handshake: 'intact'`, and that field is render-only by
527
+ * construction — a mutation consumes no handshake, so it has nothing
528
+ * to report intact. A mutation arm therefore lands WITH its first
529
+ * emitter, sharing the facts this envelope carries (`code`, `message`,
530
+ * `fix`, `retry`, one closed registry) and carrying no `handshake`
531
+ * field at all (ggui#798).
532
+ */
533
+ export const refusedOutputSchema = z.strictObject({
534
+ outcome: z.literal('refused'),
535
+ refusal: renderRefusalSchema,
536
+ });
537
+ /**
538
+ * The refused arm's presence rule: the WHOLE payload must be
539
+ * {@link refusedOutputSchema}. Delegating here rather than restating
540
+ * the rule inline keeps one declaration of the RENDER refused
541
+ * envelope. A mutation arm cannot delegate to it — that schema
542
+ * REQUIRES `handshake: 'intact'` — so it mints its own with its first
543
+ * emitter, sharing the facts and carrying no `handshake` (ggui#798).
544
+ */
545
+ function refineRefusedArm(value, ctx) {
546
+ if (refusedOutputSchema.safeParse(value).success)
547
+ return;
548
+ ctx.addIssue({
549
+ code: 'custom',
550
+ 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.",
551
+ });
552
+ }
553
+ /**
554
+ * Report a field the committed arms (`rendered` / `failed`) require but
555
+ * the payload omits. The fields are optional at the schema level only
556
+ * to make room for the refused arm; demoting them must not weaken the
557
+ * committed arms, which is what this restores.
558
+ */
559
+ function requireOnCommittedArm(present, field, outcome, ctx) {
560
+ if (present)
561
+ return;
562
+ ctx.addIssue({
563
+ code: 'custom',
564
+ path: [field],
565
+ message: `outcome '${outcome}' MUST carry \`${field}\` — a committed render reports its full identity.`,
566
+ });
567
+ }
568
+ /** `refusal` rides the refused arm only. */
569
+ function rejectRefusalOnCommittedArm(refusal, outcome, ctx) {
570
+ if (refusal === undefined)
571
+ return;
572
+ ctx.addIssue({
573
+ code: 'custom',
574
+ path: ['refusal'],
575
+ message: `\`refusal\` is present only on outcome 'refused', not '${outcome}'.`,
576
+ });
577
+ }
448
578
  /**
449
579
  * Canonical failure codes for a `resources/read` on a render locator
450
580
  * (`ui://ggui/render/{sessionId}/{blueprintKey}`). Closed enum.
@@ -498,9 +628,10 @@ export const resourceReadErrorSchema = z.object({
498
628
  .describe('Extra diagnostic context for operators. Dropped on NOT_FOUND so a denied read cannot be told apart from a miss.'),
499
629
  });
500
630
  /**
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.
631
+ * Wire-output shape. `outcome` is the discriminant and the only
632
+ * unconditionally required field; which of the others exist follows
633
+ * from it, per the THREE OUTCOMES section below — that section is the
634
+ * single summary of this shape, so do not restate it here.
504
635
  * The handler carries `shortCode`, `codeReady`, `handshakeId`,
505
636
  * `decision`, `contract`, `codeUrl`, `codeHash`
506
637
  * on its internal `RenderOutput` TS shape for telemetry / post-classify
@@ -514,17 +645,38 @@ export const resourceReadErrorSchema = z.object({
514
645
  * `render-resource/...`). Leaving a dead URL on the wire had the model
515
646
  * hallucinating links that resolve nowhere.
516
647
  *
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.
648
+ * THREE OUTCOMES (SPEC §7.1, ggui#786). Every result carries
649
+ * {@link renderOutcomeSchema} on `outcome`, and the identity fields are
650
+ * present IFF something was committed — which is why they are optional
651
+ * at the schema level and pinned by the presence refinement below:
652
+ *
653
+ * - `rendered` — identity fields present; `resourceUri` present iff
654
+ * mountable; no `error`, no `refusal`.
655
+ * - `failed` — generation ran and produced nothing. Identity fields
656
+ * present (the error GguiSession IS committed, so `sessionId`
657
+ * remains a live handle into the session channel), `error`
658
+ * present, `resourceUri` absent, no `_meta` on the result. The
659
+ * handshake is consumed.
660
+ * - `refused` — the deployment declined before doing any work.
661
+ * Identity fields ABSENT, `refusal` present, no `error`, no
662
+ * `nextStep`, no `_meta`. Nothing was committed and the handshake
663
+ * is intact. The refused arm's whole envelope is
664
+ * {@link refusedOutputSchema}.
665
+ *
666
+ * The root stays ONE object with a discriminant field rather than a
667
+ * discriminated union: the MCP spec's `Tool.outputSchema` root MUST be
668
+ * a JSON Schema of type `object`, and the SDK registers zod raw shapes.
669
+ * TypeScript narrowing is via the guards at the bottom of this file
670
+ * ({@link isRenderedOutput} / {@link isFailedRenderOutput} /
671
+ * {@link isRefusedRenderOutput}), never a parallel union type.
522
672
  *
523
673
  * Post-Phase-B the `'compose'` action enum value is gone — there is no
524
674
  * stack of N renders to compose against.
525
675
  */
526
676
  export const renderOutputSchema = z.object({
527
- sessionId: z.string(),
677
+ 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."),
678
+ /** Present iff something was committed — absent on a refusal. */
679
+ sessionId: z.string().optional(),
528
680
  /**
529
681
  * Spec-canonical MCP-Apps entry-point — same `ui://ggui/render/{id}`
530
682
  * URI surfaced on `_meta.ui.resourceUri`. Surfacing it on the LLM-
@@ -541,17 +693,24 @@ export const renderOutputSchema = z.object({
541
693
  .string()
542
694
  .optional()
543
695
  .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']),
696
+ action: z
697
+ .enum(['create', 'reuse', 'update', 'replace', 'declined'])
698
+ .optional(),
545
699
  contractHash: z
546
700
  .string()
701
+ .optional()
547
702
  .describe('Canonical hash of the rendered data contract (shape only — fields, types, specs). Same hash ⟺ same data flow.'),
548
703
  blueprintId: z
549
704
  .string()
705
+ .optional()
550
706
  .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
707
  variantKey: z
552
708
  .string()
709
+ .optional()
553
710
  .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.'),
711
+ cache: renderCacheMarkerSchema
712
+ .optional()
713
+ .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
714
  /**
556
715
  * In-result failure marker — present iff the tool result is
557
716
  * `isError: true`. The structuredContent stays schema-conformant on
@@ -561,6 +720,14 @@ export const renderOutputSchema = z.object({
561
720
  error: renderErrorSchema
562
721
  .optional()
563
722
  .describe('Present iff the tool result is isError — canonical {code, message} for a failed/rejected generation. Absent on success.'),
723
+ /**
724
+ * In-result PRE-GENERATION REFUSAL marker — present iff
725
+ * `outcome: 'refused'`, and then it is the ONLY field besides
726
+ * `outcome` (see {@link refusedOutputSchema}).
727
+ */
728
+ refusal: renderRefusalSchema
729
+ .optional()
730
+ .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
731
  /**
565
732
  * Wire-shape recovery hint for the next call. Emitted ONLY when the
566
733
  * rendered contract has a non-empty `actionSpec` — i.e. the agent will
@@ -586,6 +753,56 @@ export const renderOutputSchema = z.object({
586
753
  timeout: z.number(),
587
754
  }),
588
755
  }).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.'),
756
+ }).superRefine((value, ctx) => {
757
+ // Present-iff-committed. NOTE for implementors: this refinement is
758
+ // attached to the COMPOSED schema. A consumer that decomposes the
759
+ // schema to its raw shape and rebuilds it (`z.object(schema.shape)`,
760
+ // which is how the MCP SDK registers a tool's outputSchema) loses
761
+ // these rules — which is why the transport validates a refused
762
+ // payload against `refusedOutputSchema` directly. See SPEC §7.1.
763
+ if (value.outcome === 'refused') {
764
+ refineRefusedArm(value, ctx);
765
+ return;
766
+ }
767
+ requireOnCommittedArm(value.sessionId !== undefined, 'sessionId', value.outcome, ctx);
768
+ requireOnCommittedArm(value.action !== undefined, 'action', value.outcome, ctx);
769
+ requireOnCommittedArm(value.contractHash !== undefined, 'contractHash', value.outcome, ctx);
770
+ requireOnCommittedArm(value.blueprintId !== undefined, 'blueprintId', value.outcome, ctx);
771
+ requireOnCommittedArm(value.variantKey !== undefined, 'variantKey', value.outcome, ctx);
772
+ requireOnCommittedArm(value.cache !== undefined, 'cache', value.outcome, ctx);
773
+ rejectRefusalOnCommittedArm(value.refusal, value.outcome, ctx);
774
+ // `resourceUri` present IFF `rendered` — the mountability rule, the
775
+ // one presence rule the six identity fields do not cover. A `failed`
776
+ // render commits an error GguiSession but exposes no mount, so a URI
777
+ // beside it points a host at a render that does not exist.
778
+ if (value.outcome === 'rendered' && value.resourceUri === undefined) {
779
+ ctx.addIssue({
780
+ code: 'custom',
781
+ path: ['resourceUri'],
782
+ message: "outcome 'rendered' MUST carry `resourceUri` — a rendered result is mountable, and the URI is how a host mounts it.",
783
+ });
784
+ }
785
+ if (value.outcome === 'failed' && value.resourceUri !== undefined) {
786
+ ctx.addIssue({
787
+ code: 'custom',
788
+ path: ['resourceUri'],
789
+ message: "`resourceUri` is present only on outcome 'rendered' — a failed render exposes no mount affordance.",
790
+ });
791
+ }
792
+ if (value.outcome === 'failed' && value.error === undefined) {
793
+ ctx.addIssue({
794
+ code: 'custom',
795
+ path: ['error'],
796
+ message: "outcome 'failed' MUST carry `error` — the classification is what distinguishes it from a render that succeeded.",
797
+ });
798
+ }
799
+ if (value.outcome === 'rendered' && value.error !== undefined) {
800
+ ctx.addIssue({
801
+ code: 'custom',
802
+ path: ['error'],
803
+ message: "`error` is present only on outcome 'failed', not 'rendered'.",
804
+ });
805
+ }
589
806
  });
590
807
  /**
591
808
  * `ggui_update` — refresh the rendered UI with new state.
@@ -654,9 +871,22 @@ export const amendInputSchema = z.discriminatedUnion('kind', [
654
871
  }).strict(),
655
872
  ]);
656
873
  /**
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.
874
+ * Agent-facing description of the schema-attestation hash, shared by
875
+ * both mutation tools (ggui#560). It is not a comment: `.describe()`
876
+ * ships as JSON-Schema `description` in the tool declaration, so this
877
+ * string is what an agent reads out of `tools/list` when it has to
878
+ * decide whether a hash mismatch means the contract moved under it.
879
+ * Declared once so the two tools cannot say different things about the
880
+ * same field.
881
+ */
882
+ 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.';
883
+ /** Agent-facing description of the grammar-profile classifier. Same rationale as {@link PROPS_SCHEMA_HASH_DESCRIPTION}. */
884
+ const PROPS_SCHEMA_PROFILE_DESCRIPTION = "Grammar profile of the enforced props schema: 'grammar-safe' or 'full'. Present with propsSchemaHash; treat unrecognized values as 'full'.";
885
+ /**
886
+ * Wire-output shape — minimal acknowledgement. This schema IS the
887
+ * handler's declared output: `ggui_update` registers `.shape` and its
888
+ * return type is `GguiUpdateOutput` (`z.infer` of this schema), so
889
+ * there is no second declaration to keep in step (ggui#798).
660
890
  *
661
891
  * Every REAL update (`updated: true`) mints a new history record and
662
892
  * its result carries the `ai.ggui/render` slice as a FULL bootable
@@ -666,6 +896,15 @@ export const amendInputSchema = z.discriminatedUnion('kind', [
666
896
  * history when its higher-epoch `props_update` frame lands; the
667
897
  * in-place repaint over the live-channel ladder (WS / SSE / polling /
668
898
  * bridge-pull) is `ggui_amend`'s job.
899
+ *
900
+ * NO refusal arm (ggui#786): the pre-generation refusal envelope rides
901
+ * `ggui_render` only. There is no pre-state POLICY gate on the
902
+ * mutation tools today — nothing on this path can decline a call for
903
+ * deployment-policy reasons (a contract error still rejects a
904
+ * malformed mutation before any store read; that is a parse result,
905
+ * not a refusal) — and `handshake: 'intact'` would assert something
906
+ * meaningless on a tool that consumes no handshake. The arm lands with its first
907
+ * emitter (ggui#798), not as a mechanical port of the render one.
669
908
  */
670
909
  export const updateOutputSchema = z.object({
671
910
  sessionId: z.string(),
@@ -676,8 +915,7 @@ export const updateOutputSchema = z.object({
676
915
  * no-op (`updated: false`) no record is minted and this is the bare
677
916
  * live-head URI. Mirrored on the LLM-visible structuredContent so
678
917
  * 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.
918
+ * mount URI.
681
919
  */
682
920
  resourceUri: z.string(),
683
921
  /**
@@ -707,9 +945,15 @@ export const updateOutputSchema = z.object({
707
945
  * session-continuity guarantee, so a mismatch is the observable form
708
946
  * of a contract changing mid-session.
709
947
  */
710
- propsSchemaHash: z.string().optional(),
948
+ propsSchemaHash: z
949
+ .string()
950
+ .optional()
951
+ .describe(PROPS_SCHEMA_HASH_DESCRIPTION),
711
952
  /** Present with `propsSchemaHash`; same profile classifier as the handshake's. */
712
- propsSchemaProfile: z.string().optional(),
953
+ propsSchemaProfile: z
954
+ .string()
955
+ .optional()
956
+ .describe(PROPS_SCHEMA_PROFILE_DESCRIPTION),
713
957
  });
714
958
  /**
715
959
  * `ggui_amend` wire output (#483) — acknowledgement only. `resourceUri`
@@ -717,17 +961,49 @@ export const updateOutputSchema = z.object({
717
961
  * mints a record, so there is no pinned URI to return and no epoch
718
962
  * field — the history number is untouched by construction). The
719
963
  * mounted card receives the new props over the live channels.
964
+ *
965
+ * As with {@link updateOutputSchema}, this schema IS the handler's
966
+ * declared output — `ggui_amend` registers `.shape` and returns
967
+ * `GguiAmendOutput` (ggui#798).
720
968
  */
721
969
  export const amendOutputSchema = z.object({
722
970
  sessionId: z.string(),
723
971
  updated: z.boolean(),
972
+ /**
973
+ * The BARE live-head URI — amend targets the mounted card and never
974
+ * mints a record, so there is no pinned URI to return and no epoch
975
+ * field (the history number is untouched by construction).
976
+ *
977
+ * NORMATIVE re-anchor reference (SPEC §7.1.2.1, ggui#652 /
978
+ * guuey#535): together with `sessionId` this is the durable record
979
+ * that an in-place repaint touched this session at this turn. A
980
+ * host's persistence layer MAY consume it as a locator-only
981
+ * re-anchor — `resources/read`-resolvable, stable for the session's
982
+ * lifetime — so a restored transcript re-positions the card at its
983
+ * latest referencing turn and rehydrates CURRENT state instead of a
984
+ * stale earlier snapshot. It rides `structuredContent` (LLM-visible,
985
+ * same rationale as ggui_update's resourceUri: consumers that strip
986
+ * `_meta` still reach it) and MUST NOT move to a result `_meta`
987
+ * slice — any result `_meta` on this tool makes view-minting hosts
988
+ * break the in-place semantics.
989
+ *
990
+ * Both this field and `sessionId` are REQUIRED, which is what makes
991
+ * SPEC §7.1.2.1's "structurally guaranteed" true rather than a
992
+ * convention.
993
+ */
724
994
  resourceUri: z.string(),
725
995
  /** Same no-op feedback channel as ggui_update's `warning`. */
726
996
  warning: z.string().optional(),
727
997
  /** Schema attestation (ggui#560) — same semantics as ggui_update's. */
728
- propsSchemaHash: z.string().optional(),
998
+ propsSchemaHash: z
999
+ .string()
1000
+ .optional()
1001
+ .describe(PROPS_SCHEMA_HASH_DESCRIPTION),
729
1002
  /** Present with `propsSchemaHash`. */
730
- propsSchemaProfile: z.string().optional(),
1003
+ propsSchemaProfile: z
1004
+ .string()
1005
+ .optional()
1006
+ .describe(PROPS_SCHEMA_PROFILE_DESCRIPTION),
731
1007
  });
732
1008
  /**
733
1009
  * `ggui_runtime_declare_tool_catalog` — the host runtime declares its
@@ -951,3 +1227,156 @@ export const runtimeTelemetryInputShape = {
951
1227
  export const runtimeTelemetryInputSchema = z.object(runtimeTelemetryInputShape);
952
1228
  /** `ggui_runtime_telemetry` output — bare acknowledgement. */
953
1229
  export const runtimeTelemetryOutputSchema = z.object({ ok: z.literal(true) });
1230
+ /** A render that produced an interface — identity fields present. */
1231
+ export function isRenderedOutput(output) {
1232
+ return output.outcome === 'rendered';
1233
+ }
1234
+ /**
1235
+ * A generation that RAN and produced nothing. The error session is
1236
+ * committed, so `sessionId` is a live handle and `error` classifies it.
1237
+ */
1238
+ export function isFailedRenderOutput(output) {
1239
+ return output.outcome === 'failed';
1240
+ }
1241
+ /**
1242
+ * A PRE-GENERATION refusal — nothing read and nothing committed, so
1243
+ * every identity field is absent and `refusal` carries the state. (Not
1244
+ * "nothing parsed": the SDK has already checked the call against the
1245
+ * tool's declared `inputSchema` by the time a gate can refuse it.)
1246
+ */
1247
+ export function isRefusedRenderOutput(output) {
1248
+ return output.outcome === 'refused';
1249
+ }
1250
+ // ============================================================================
1251
+ // Tool output schemas the protocol owns (#817 part C). A handler registers
1252
+ // `<schema>.shape` as its MCP `outputSchema` and derives its output type from
1253
+ // the schema — never a parallel interface. No `.readonly()` here: zod 4
1254
+ // projects it as `readOnly` into the advertised JSON Schema, and the wire is
1255
+ // mutable JSON; readonly is applied at the seam (`DeepReadonly` on the
1256
+ // derived types). Every shape is closed: the transport strip-parses against
1257
+ // `.shape`, so the shape IS the wire.
1258
+ // Honest exception, pre-existing: three `.readonly()` calls in
1259
+ // `schemas/data-contract.ts` (gadget requires/connect, app public env) reach
1260
+ // the registered render + handshake inputs through `dataContractSchema` and
1261
+ // advertise `readOnly` on tools/list today — ggui#824 removes them.
1262
+ // ============================================================================
1263
+ /** Display modes an MCP Apps host can render a view in (ext-apps vocabulary). */
1264
+ export const mcpUiDisplayModeSchema = z.enum(['inline', 'fullscreen', 'pip']);
1265
+ /**
1266
+ * The host-context projection the iframe-runtime observes and `ggui_consume`
1267
+ * echoes (`client.hostContext`). Every field optional: a host that never
1268
+ * reports one leaves it absent — absent ⇒ the documented default.
1269
+ */
1270
+ export const hostContextProjectionSchema = z.object({
1271
+ availableDisplayModes: z.array(mcpUiDisplayModeSchema).optional(),
1272
+ currentDisplayMode: mcpUiDisplayModeSchema.optional(),
1273
+ containerDimensions: z
1274
+ .object({
1275
+ width: z.number().optional(),
1276
+ maxWidth: z.number().optional(),
1277
+ height: z.number().optional(),
1278
+ maxHeight: z.number().optional(),
1279
+ })
1280
+ .optional(),
1281
+ platform: z.enum(['web', 'desktop', 'mobile']).optional(),
1282
+ deviceCapabilities: z
1283
+ .object({ touch: z.boolean().optional(), hover: z.boolean().optional() })
1284
+ .optional(),
1285
+ locale: z.string().optional(),
1286
+ timeZone: z.string().optional(),
1287
+ });
1288
+ /** `ggui_consume`'s `client` slice — what the runtime observed about its host. */
1289
+ export const clientObservationsSchema = z.object({
1290
+ hostContext: hostContextProjectionSchema.optional(),
1291
+ });
1292
+ /** One row of `ggui_list_sessions` — eight closed keys; nothing passes through. */
1293
+ export const gguiSessionSummaryWireSchema = z.object({
1294
+ sessionId: z.string(),
1295
+ hostName: z.string().optional(),
1296
+ hostSessionId: z.string().optional(),
1297
+ createdAt: z.string(),
1298
+ lastActivityAt: z.string(),
1299
+ status: z.string(),
1300
+ wsToken: z.string().optional(),
1301
+ wsTokenExpiresAt: z.string().optional(),
1302
+ });
1303
+ /**
1304
+ * `ggui_get_session`'s wire: the store row's six base fields plus the mount
1305
+ * variant — for EVERY session. An MCP-Apps mount is locator-only on the
1306
+ * render object, but its store row carries the base fields, so the
1307
+ * projection reads them from the row and the wire never fails on that
1308
+ * variant. The locator itself is not on this wire (MCP-Apps resources have
1309
+ * their own paths).
1310
+ */
1311
+ export const gguiGetSessionOutputSchema = z.object({
1312
+ variant: z.enum(['render', 'mcpApps']),
1313
+ id: z.string(),
1314
+ appId: z.string(),
1315
+ eventSequence: z.number().int().nonnegative(),
1316
+ createdAt: z.number().int().nonnegative(),
1317
+ lastActivityAt: z.number().int().nonnegative(),
1318
+ expiresAt: z.number().int().nonnegative(),
1319
+ /**
1320
+ * The last-known value of every declared contextSpec slot, as
1321
+ * `ggui_runtime_sync_context` wrote it onto the row — the read path a
1322
+ * raw MCP client (no widget-context mirror) has for contextSpec values.
1323
+ * Present iff the row carries one; never an empty placeholder.
1324
+ */
1325
+ contextSnapshot: jsonObjectSchema.optional(),
1326
+ });
1327
+ /** The three sequential gates of `ggui_protocol_validate_blueprint`. */
1328
+ export const blueprintValidationTierSchema = z.enum(['compile', 'selfCheck', 'runtime']);
1329
+ export const blueprintValidationIssueSchema = z.object({
1330
+ tier: blueprintValidationTierSchema,
1331
+ code: z.string(),
1332
+ message: z.string(),
1333
+ fix: z.string().optional(),
1334
+ });
1335
+ /** `ggui_protocol_validate_blueprint`'s result envelope: `failedAt` names the tier that stopped, or null. */
1336
+ export const blueprintValidationResultSchema = z.object({
1337
+ valid: z.boolean(),
1338
+ failedAt: blueprintValidationTierSchema.nullable(),
1339
+ errors: z.array(blueprintValidationIssueSchema),
1340
+ warnings: z.array(blueprintValidationIssueSchema),
1341
+ });
1342
+ /** A provider row — what `ggui_list_featured_blueprints` returns per blueprint. */
1343
+ export const blueprintEntryWireSchema = z.object({
1344
+ id: z.string(),
1345
+ name: z.string(),
1346
+ description: z.string().optional(),
1347
+ source: blueprintSourceSchema,
1348
+ updatedAt: z.string(),
1349
+ tags: z.array(z.string()).optional(),
1350
+ });
1351
+ export const gguiListFeaturedBlueprintsOutputSchema = z.object({
1352
+ blueprints: z.array(blueprintEntryWireSchema),
1353
+ total: z.number().int().nonnegative(),
1354
+ });
1355
+ /** One `ggui_search_blueprints` hit — a scored row plus the registry-only keys. */
1356
+ export const gguiSearchBlueprintsResultSchema = z.object({
1357
+ id: z.string(),
1358
+ name: z.string(),
1359
+ description: z.string(),
1360
+ category: z.string(),
1361
+ props: z.array(z.object({ name: z.string(), type: z.string(), required: z.boolean(), description: z.string() })),
1362
+ callbacks: z.array(z.string()),
1363
+ featured: z.boolean(),
1364
+ relevance: z.literal('match'),
1365
+ score: z.number(),
1366
+ origin: z.literal('registry').optional(),
1367
+ artifactId: z.string().optional(),
1368
+ version: z.string().optional(),
1369
+ mcpTools: z.array(z.object({ server: z.string().optional(), tool: z.string() })).optional(),
1370
+ scopeVerification: z.enum(['verified', 'unverified']).optional(),
1371
+ });
1372
+ export const gguiSearchBlueprintsOutputSchema = z.object({
1373
+ results: z.array(gguiSearchBlueprintsResultSchema),
1374
+ total: z.number(),
1375
+ query: z.string(),
1376
+ degradedSources: z
1377
+ .array(z.object({
1378
+ source: z.literal('registry'),
1379
+ reason: z.enum(['unreachable', 'timeout', 'invalid_response']),
1380
+ }))
1381
+ .optional(),
1382
+ });
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ export declare const renderInputEnvelopeSchema: z.ZodObject<{
3
+ handshakeId: z.ZodString;
4
+ props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
5
+ themeId: z.ZodOptional<z.ZodString>;
6
+ infra: z.ZodOptional<z.ZodObject<{
7
+ model: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strict>>;
9
+ override: z.ZodOptional<z.ZodObject<{
10
+ contract: z.ZodOptional<z.ZodType<import("..").DataContract, unknown, z.core.$ZodTypeInternals<import("..").DataContract, unknown>>>;
11
+ variance: z.ZodOptional<z.ZodType<import("..").BlueprintVariance, unknown, z.core.$ZodTypeInternals<import("..").BlueprintVariance, unknown>>>;
12
+ }, z.core.$strict>>;
13
+ }, z.core.$strip>;
14
+ export declare const renderInputRouteGuardSchema: z.ZodObject<{
15
+ infra: z.ZodOptional<z.ZodObject<{
16
+ model: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$loose>>;
18
+ }, z.core.$loose>;
19
+ export type RenderInputEnvelope = z.infer<typeof renderInputEnvelopeSchema>;
20
+ //# sourceMappingURL=render-input-envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-input-envelope.d.ts","sourceRoot":"","sources":["../../src/schemas/render-input-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiCxB,eAAO,MAAM,yBAAyB;;;;;;;;;;;iBAA6C,CAAC;AA6BpF,eAAO,MAAM,2BAA2B;;;;iBAA+C,CAAC;AAExF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}