@ggui-ai/protocol 0.1.0-rc.1

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 (222) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +46 -0
  3. package/dist/bridge/invoke-agent.d.ts +65 -0
  4. package/dist/bridge/invoke-agent.d.ts.map +1 -0
  5. package/dist/bridge/invoke-agent.js +113 -0
  6. package/dist/envelope-adapters.d.ts +24 -0
  7. package/dist/envelope-adapters.d.ts.map +1 -0
  8. package/dist/envelope-adapters.js +14 -0
  9. package/dist/envelopes/builders.d.ts +145 -0
  10. package/dist/envelopes/builders.d.ts.map +1 -0
  11. package/dist/envelopes/builders.js +113 -0
  12. package/dist/errors/unknown-permission-name.d.ts +12 -0
  13. package/dist/errors/unknown-permission-name.d.ts.map +1 -0
  14. package/dist/errors/unknown-permission-name.js +29 -0
  15. package/dist/errors/version-mismatch.d.ts +55 -0
  16. package/dist/errors/version-mismatch.d.ts.map +1 -0
  17. package/dist/errors/version-mismatch.js +52 -0
  18. package/dist/gadgets/resolve-contract-gadgets.d.ts +93 -0
  19. package/dist/gadgets/resolve-contract-gadgets.d.ts.map +1 -0
  20. package/dist/gadgets/resolve-contract-gadgets.js +119 -0
  21. package/dist/gadgets/stdlib-gadgets.d.ts +43 -0
  22. package/dist/gadgets/stdlib-gadgets.d.ts.map +1 -0
  23. package/dist/gadgets/stdlib-gadgets.js +161 -0
  24. package/dist/iframe-bridge.d.ts +63 -0
  25. package/dist/iframe-bridge.d.ts.map +1 -0
  26. package/dist/iframe-bridge.js +166 -0
  27. package/dist/index.d.ts +62 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +79 -0
  30. package/dist/integrations/mcp-apps.d.ts +1218 -0
  31. package/dist/integrations/mcp-apps.d.ts.map +1 -0
  32. package/dist/integrations/mcp-apps.js +427 -0
  33. package/dist/navigation/index.d.ts +3 -0
  34. package/dist/navigation/index.d.ts.map +1 -0
  35. package/dist/navigation/index.js +1 -0
  36. package/dist/navigation/stack-navigation.d.ts +55 -0
  37. package/dist/navigation/stack-navigation.d.ts.map +1 -0
  38. package/dist/navigation/stack-navigation.js +80 -0
  39. package/dist/recommended-prompts.d.ts +56 -0
  40. package/dist/recommended-prompts.d.ts.map +1 -0
  41. package/dist/recommended-prompts.js +55 -0
  42. package/dist/registry/blueprint-key.d.ts +9 -0
  43. package/dist/registry/blueprint-key.d.ts.map +1 -0
  44. package/dist/registry/blueprint-key.js +28 -0
  45. package/dist/registry/canonicalize-contract.d.ts +35 -0
  46. package/dist/registry/canonicalize-contract.d.ts.map +1 -0
  47. package/dist/registry/canonicalize-contract.js +166 -0
  48. package/dist/registry/summarize-contract.d.ts +46 -0
  49. package/dist/registry/summarize-contract.d.ts.map +1 -0
  50. package/dist/registry/summarize-contract.js +63 -0
  51. package/dist/schema-learning/derive-contract.d.ts +67 -0
  52. package/dist/schema-learning/derive-contract.d.ts.map +1 -0
  53. package/dist/schema-learning/derive-contract.js +117 -0
  54. package/dist/schema-learning/merge.d.ts +32 -0
  55. package/dist/schema-learning/merge.d.ts.map +1 -0
  56. package/dist/schema-learning/merge.js +146 -0
  57. package/dist/schemas/blueprint.d.ts +32 -0
  58. package/dist/schemas/blueprint.d.ts.map +1 -0
  59. package/dist/schemas/blueprint.js +92 -0
  60. package/dist/schemas/data-contract.d.ts +750 -0
  61. package/dist/schemas/data-contract.d.ts.map +1 -0
  62. package/dist/schemas/data-contract.js +663 -0
  63. package/dist/schemas/gadget-name-grammar.d.ts +29 -0
  64. package/dist/schemas/gadget-name-grammar.d.ts.map +1 -0
  65. package/dist/schemas/gadget-name-grammar.js +28 -0
  66. package/dist/schemas/handshake-suggestion.d.ts +46 -0
  67. package/dist/schemas/handshake-suggestion.d.ts.map +1 -0
  68. package/dist/schemas/handshake-suggestion.js +107 -0
  69. package/dist/schemas/invoke.d.ts +337 -0
  70. package/dist/schemas/invoke.d.ts.map +1 -0
  71. package/dist/schemas/invoke.js +169 -0
  72. package/dist/schemas/mcp.d.ts +301 -0
  73. package/dist/schemas/mcp.d.ts.map +1 -0
  74. package/dist/schemas/mcp.js +373 -0
  75. package/dist/schemas/ops-blueprint.d.ts +176 -0
  76. package/dist/schemas/ops-blueprint.d.ts.map +1 -0
  77. package/dist/schemas/ops-blueprint.js +259 -0
  78. package/dist/schemas/sync-check.d.ts +11 -0
  79. package/dist/schemas/sync-check.d.ts.map +1 -0
  80. package/dist/schemas/sync-check.js +60 -0
  81. package/dist/screen-blueprints/define.d.ts +22 -0
  82. package/dist/screen-blueprints/define.d.ts.map +1 -0
  83. package/dist/screen-blueprints/define.js +3 -0
  84. package/dist/screen-blueprints/index.d.ts +4 -0
  85. package/dist/screen-blueprints/index.d.ts.map +1 -0
  86. package/dist/screen-blueprints/index.js +3 -0
  87. package/dist/screen-blueprints/match.d.ts +35 -0
  88. package/dist/screen-blueprints/match.d.ts.map +1 -0
  89. package/dist/screen-blueprints/match.js +51 -0
  90. package/dist/screen-blueprints/types.d.ts +164 -0
  91. package/dist/screen-blueprints/types.d.ts.map +1 -0
  92. package/dist/screen-blueprints/types.js +1 -0
  93. package/dist/stream/stream-parser.d.ts +62 -0
  94. package/dist/stream/stream-parser.d.ts.map +1 -0
  95. package/dist/stream/stream-parser.js +199 -0
  96. package/dist/transport/websocket.d.ts +178 -0
  97. package/dist/transport/websocket.d.ts.map +1 -0
  98. package/dist/transport/websocket.js +1 -0
  99. package/dist/types/app-config.d.ts +61 -0
  100. package/dist/types/app-config.d.ts.map +1 -0
  101. package/dist/types/app-config.js +1 -0
  102. package/dist/types/auth.d.ts +61 -0
  103. package/dist/types/auth.d.ts.map +1 -0
  104. package/dist/types/auth.js +1 -0
  105. package/dist/types/blueprint.d.ts +206 -0
  106. package/dist/types/blueprint.d.ts.map +1 -0
  107. package/dist/types/blueprint.js +1 -0
  108. package/dist/types/canvas-lifecycle.d.ts +105 -0
  109. package/dist/types/canvas-lifecycle.d.ts.map +1 -0
  110. package/dist/types/canvas-lifecycle.js +38 -0
  111. package/dist/types/capabilities.d.ts +40 -0
  112. package/dist/types/capabilities.d.ts.map +1 -0
  113. package/dist/types/capabilities.js +19 -0
  114. package/dist/types/contract-inference.d.ts +401 -0
  115. package/dist/types/contract-inference.d.ts.map +1 -0
  116. package/dist/types/contract-inference.js +44 -0
  117. package/dist/types/credential.d.ts +41 -0
  118. package/dist/types/credential.d.ts.map +1 -0
  119. package/dist/types/credential.js +32 -0
  120. package/dist/types/data-bindings.d.ts +322 -0
  121. package/dist/types/data-bindings.d.ts.map +1 -0
  122. package/dist/types/data-bindings.js +29 -0
  123. package/dist/types/data-contract.d.ts +1296 -0
  124. package/dist/types/data-contract.d.ts.map +1 -0
  125. package/dist/types/data-contract.js +111 -0
  126. package/dist/types/events.d.ts +182 -0
  127. package/dist/types/events.d.ts.map +1 -0
  128. package/dist/types/events.js +8 -0
  129. package/dist/types/feedback.d.ts +24 -0
  130. package/dist/types/feedback.d.ts.map +1 -0
  131. package/dist/types/feedback.js +7 -0
  132. package/dist/types/gadget.d.ts +121 -0
  133. package/dist/types/gadget.d.ts.map +1 -0
  134. package/dist/types/gadget.js +24 -0
  135. package/dist/types/handshake-suggestion.d.ts +264 -0
  136. package/dist/types/handshake-suggestion.d.ts.map +1 -0
  137. package/dist/types/handshake-suggestion.js +70 -0
  138. package/dist/types/host-context.d.ts +163 -0
  139. package/dist/types/host-context.d.ts.map +1 -0
  140. package/dist/types/host-context.js +142 -0
  141. package/dist/types/interface-context.d.ts +105 -0
  142. package/dist/types/interface-context.d.ts.map +1 -0
  143. package/dist/types/interface-context.js +115 -0
  144. package/dist/types/invoke.d.ts +28 -0
  145. package/dist/types/invoke.d.ts.map +1 -0
  146. package/dist/types/invoke.js +7 -0
  147. package/dist/types/live-channel.d.ts +613 -0
  148. package/dist/types/live-channel.d.ts.map +1 -0
  149. package/dist/types/live-channel.js +1 -0
  150. package/dist/types/llm.d.ts +61 -0
  151. package/dist/types/llm.d.ts.map +1 -0
  152. package/dist/types/llm.js +186 -0
  153. package/dist/types/mcp-proxy.d.ts +67 -0
  154. package/dist/types/mcp-proxy.d.ts.map +1 -0
  155. package/dist/types/mcp-proxy.js +46 -0
  156. package/dist/types/mcp.d.ts +637 -0
  157. package/dist/types/mcp.d.ts.map +1 -0
  158. package/dist/types/mcp.js +30 -0
  159. package/dist/types/openrouter-models.d.ts +22 -0
  160. package/dist/types/openrouter-models.d.ts.map +1 -0
  161. package/dist/types/openrouter-models.js +4843 -0
  162. package/dist/types/region.d.ts +26 -0
  163. package/dist/types/region.d.ts.map +1 -0
  164. package/dist/types/region.js +36 -0
  165. package/dist/types/session.d.ts +419 -0
  166. package/dist/types/session.d.ts.map +1 -0
  167. package/dist/types/session.js +1 -0
  168. package/dist/types/thread.d.ts +207 -0
  169. package/dist/types/thread.d.ts.map +1 -0
  170. package/dist/types/thread.js +57 -0
  171. package/dist/types/ui-generator.d.ts +100 -0
  172. package/dist/types/ui-generator.d.ts.map +1 -0
  173. package/dist/types/ui-generator.js +53 -0
  174. package/dist/validation/ajv-runtime.d.ts +140 -0
  175. package/dist/validation/ajv-runtime.d.ts.map +1 -0
  176. package/dist/validation/ajv-runtime.js +452 -0
  177. package/dist/validation/content-hash.d.ts +3 -0
  178. package/dist/validation/content-hash.d.ts.map +1 -0
  179. package/dist/validation/content-hash.js +21 -0
  180. package/dist/validation/contract-validator.d.ts +244 -0
  181. package/dist/validation/contract-validator.d.ts.map +1 -0
  182. package/dist/validation/contract-validator.js +711 -0
  183. package/dist/validation/cross-references.d.ts +105 -0
  184. package/dist/validation/cross-references.d.ts.map +1 -0
  185. package/dist/validation/cross-references.js +164 -0
  186. package/dist/validation/hygiene-rules.d.ts +250 -0
  187. package/dist/validation/hygiene-rules.d.ts.map +1 -0
  188. package/dist/validation/hygiene-rules.js +564 -0
  189. package/dist/validation/lint-contract.d.ts +130 -0
  190. package/dist/validation/lint-contract.d.ts.map +1 -0
  191. package/dist/validation/lint-contract.js +225 -0
  192. package/dist/validation/name-invariants.d.ts +117 -0
  193. package/dist/validation/name-invariants.d.ts.map +1 -0
  194. package/dist/validation/name-invariants.js +172 -0
  195. package/dist/validation/reserved-channels.d.ts +156 -0
  196. package/dist/validation/reserved-channels.d.ts.map +1 -0
  197. package/dist/validation/reserved-channels.js +356 -0
  198. package/dist/validation/resolve-stream-channel.d.ts +78 -0
  199. package/dist/validation/resolve-stream-channel.d.ts.map +1 -0
  200. package/dist/validation/resolve-stream-channel.js +64 -0
  201. package/dist/validation/sanitize-error.d.ts +46 -0
  202. package/dist/validation/sanitize-error.d.ts.map +1 -0
  203. package/dist/validation/sanitize-error.js +88 -0
  204. package/dist/validation/schema-compat-invariants.d.ts +140 -0
  205. package/dist/validation/schema-compat-invariants.d.ts.map +1 -0
  206. package/dist/validation/schema-compat-invariants.js +220 -0
  207. package/dist/validation/schema-meta-validation.d.ts +60 -0
  208. package/dist/validation/schema-meta-validation.d.ts.map +1 -0
  209. package/dist/validation/schema-meta-validation.js +131 -0
  210. package/dist/validation/schema-subset.d.ts +165 -0
  211. package/dist/validation/schema-subset.d.ts.map +1 -0
  212. package/dist/validation/schema-subset.js +295 -0
  213. package/dist/validation/ui-security.d.ts +54 -0
  214. package/dist/validation/ui-security.d.ts.map +1 -0
  215. package/dist/validation/ui-security.js +138 -0
  216. package/dist/validation/zod-to-json-schema.d.ts +63 -0
  217. package/dist/validation/zod-to-json-schema.d.ts.map +1 -0
  218. package/dist/validation/zod-to-json-schema.js +126 -0
  219. package/dist/version.d.ts +1458 -0
  220. package/dist/version.d.ts.map +1 -0
  221. package/dist/version.js +1459 -0
  222. package/package.json +113 -0
@@ -0,0 +1,711 @@
1
+ import { deriveContextDefault } from '../types/data-contract.js';
2
+ import { compileForValidation, compileValidatorModule, mapAjvErrorsToViolations, prefixViolations, } from './ajv-runtime.js';
3
+ import { checkCrossReferences } from './cross-references.js';
4
+ import { checkNameInvariants } from './name-invariants.js';
5
+ import { checkSchemaCompat } from './schema-compat-invariants.js';
6
+ import { BUILTIN_RESERVED_VALIDATORS, RESERVED_CHANNEL_PREFIX, isKnownReservedChannel, isReservedChannelName, } from './reserved-channels.js';
7
+ /**
8
+ * Synthesize a {@link PropsSpec} into the single object-node JSON
9
+ * Schema the runtime validates `props` against — `{type:'object',
10
+ * properties:{…entry.schema…}, required:[…entry.required…]}`.
11
+ * Closed-shape (`additionalProperties:false` at every depth) is NOT
12
+ * injected here — the Ajv compile step ({@link compileForValidation} /
13
+ * {@link compileValidatorModule}) does that, so this returns the raw
14
+ * pre-injection wrapper.
15
+ *
16
+ * Shared by {@link validatePropsData} (server-side runtime check) and
17
+ * {@link compileContractValidators} (push-time standalone emission) so
18
+ * the precompiled in-iframe validator enforces byte-identical
19
+ * semantics to the runtime validator — one synthesis, no drift.
20
+ */
21
+ export function buildPropsWrapperSchema(spec) {
22
+ // `properties` is required by the `PropsSpec` type, but a degenerate
23
+ // contract can carry `props: {}` on the wire — tolerate that as an
24
+ // empty wrapper rather than throwing on `Object.entries(undefined)`.
25
+ const properties = spec.properties ?? {};
26
+ return {
27
+ type: 'object',
28
+ properties: Object.fromEntries(Object.entries(properties).map(([name, entry]) => [name, entry.schema])),
29
+ required: Object.entries(properties)
30
+ .filter(([, entry]) => entry.required === true)
31
+ .map(([name]) => name),
32
+ };
33
+ }
34
+ /**
35
+ * Validate runtime props data against a PropsSpec contract.
36
+ *
37
+ * Synthesizes the propsSpec into a single JSON Schema object node
38
+ * — `{type:'object', properties: {…spec.properties[name].schema…},
39
+ * required: [...names where entry.required], additionalProperties:
40
+ * false}` — and validates `props` against it via the shared Ajv
41
+ * runtime. The closed-shape injector recurses into every nested
42
+ * object so the bidirectional contract (every declared key
43
+ * validated, every data key declared) holds at any depth.
44
+ *
45
+ * Load-bearing for `ggui_update kind:'merge'` (RFC 7396): a patch
46
+ * adding a key absent from `propsSpec.properties` would silently
47
+ * land on the stack item without this gate. Same rule applies to
48
+ * the `done`-vs-declared-`completed` class of bug inside array
49
+ * items — Ajv rejects with the exact path (`todos[0].done`).
50
+ */
51
+ export function validatePropsData(props, spec, precompiled) {
52
+ // `precompiled` — a server-emitted, eval-free validator — lets a
53
+ // CSP-sandboxed caller (the renderer iframe) skip the `ajv.compile()`
54
+ // codegen its CSP forbids. Same Ajv engine, compiled once server-side.
55
+ const validate = precompiled ?? compileForValidation(buildPropsWrapperSchema(spec));
56
+ const ok = validate(props);
57
+ if (ok)
58
+ return { valid: true, violations: [] };
59
+ return {
60
+ valid: false,
61
+ violations: mapAjvErrorsToViolations(validate.errors, props),
62
+ };
63
+ }
64
+ /**
65
+ * Validate a stream delivery's payload against the channel's declared
66
+ * schema on a {@link StreamSpec}.
67
+ *
68
+ * Signature takes the channel name + payload explicitly — matching
69
+ * the {@link StreamEnvelope} wire shape (where channel is a first-
70
+ * class envelope field, not a field nested inside the payload).
71
+ *
72
+ * Checks:
73
+ * - `channelName` is declared in `spec` (a flat `Record<channelName,
74
+ * StreamChannelEntry>` post-2026-04-22 flatten) — undeclared
75
+ * channels reject with `'Unknown stream channel'` in the
76
+ * violation message.
77
+ * - `payload` conforms to `spec[channelName].schema` when
78
+ * that schema declares a `type`.
79
+ *
80
+ * Reserved-channel handling (injection pattern):
81
+ *
82
+ * Known reserved channels (see {@link isKnownReservedChannel}) are
83
+ * server-owned and bypass the streamSpec path entirely — agents
84
+ * never declare them. Their payloads are validated through the
85
+ * TWO-TIER validator lookup:
86
+ *
87
+ * 1. `extraReservedValidators` — optional, caller-provided. Primary
88
+ * consumer: a hosting implementation composing the A2UI
89
+ * validator for `_ggui:preview`. Consulted FIRST so callers can
90
+ * override or extend built-ins.
91
+ * 2. `BUILTIN_RESERVED_VALIDATORS` — protocol-owned, always active.
92
+ * Ships the {@link validateContractErrorPayload} for
93
+ * `_ggui:contract-error`.
94
+ * 3. Fall-through: if no validator is registered for the known
95
+ * reserved channel, return `{valid: true}`. Preserves backward
96
+ * compatibility for any future reserved channel the runtime
97
+ * adds before its validator is authored.
98
+ *
99
+ * Without this structure, a `_ggui:preview` emission into a session
100
+ * whose active stack item carries ANY user streamSpec would
101
+ * synthesize a false "Unknown channel" violation, blocking the
102
+ * provisional preview runtime. Symmetric with the client-side
103
+ * handling in `GguiSession`.
104
+ *
105
+ * Crucially narrow by design — the known-reserved path is a CLOSED
106
+ * SET, not a prefix check. A typo inside the reserved namespace
107
+ * (e.g. `_ggui:preveiw`) is NOT recognized, falls through to the
108
+ * normal unknown-channel rejection, and surfaces the bug at its
109
+ * emission site instead of turning into a silent no-op delivery.
110
+ *
111
+ * Does NOT validate channel semantics (mode / replay / complete) —
112
+ * those are declarations, not shape constraints. See
113
+ * `resolveStreamChannel` for semantics lookup.
114
+ */
115
+ export function validateStreamData(channelName, payload, spec, extraReservedValidators, precompiledChannels) {
116
+ if (isKnownReservedChannel(channelName)) {
117
+ // Lookup order: extras first (so hosting implementations can
118
+ // override built-ins if they ever need to), then built-ins, then
119
+ // fall through to valid (unrecognized-but-known reserved channel
120
+ // without a registered validator).
121
+ const override = extraReservedValidators?.get(channelName);
122
+ if (override)
123
+ return override(payload);
124
+ const builtin = BUILTIN_RESERVED_VALIDATORS.get(channelName);
125
+ if (builtin)
126
+ return builtin(payload);
127
+ return { valid: true, violations: [] };
128
+ }
129
+ const violations = [];
130
+ const channelEntry = spec[channelName];
131
+ if (!channelEntry) {
132
+ violations.push({
133
+ field: 'channel',
134
+ message: `Unknown stream channel '${channelName}'. Declared channels: ${Object.keys(spec).join(', ')}`,
135
+ expected: Object.keys(spec).join(' | '),
136
+ received: channelName,
137
+ });
138
+ return { valid: false, violations };
139
+ }
140
+ if (channelEntry.schema && payload !== undefined) {
141
+ const validate = precompiledChannels?.get(channelName) ??
142
+ compileForValidation(channelEntry.schema);
143
+ const ok = validate(payload);
144
+ if (!ok) {
145
+ for (const v of prefixViolations(mapAjvErrorsToViolations(validate.errors, payload), `${channelName}.payload`)) {
146
+ violations.push(v);
147
+ }
148
+ }
149
+ }
150
+ return { valid: violations.length === 0, violations };
151
+ }
152
+ /**
153
+ * Validate a contextSpec slot value against the spec's declared
154
+ * schema. Symmetric with {@link validateStreamData} /
155
+ * {@link validateActionData}: the iframe-runtime observer uses this
156
+ * to gate Provider values BEFORE posting `ui/update-model-context`
157
+ * envelopes (per the contextSpec design-lock — Q4 schema check).
158
+ *
159
+ * Checks:
160
+ * - `slotName` is declared in `spec` — undeclared slots reject with
161
+ * `'Unknown context slot'`.
162
+ * - `value` conforms to `spec[slotName].schema` when that schema
163
+ * declares a `type`.
164
+ *
165
+ * Mirrors `validateActionData`'s posture: the runtime that calls this
166
+ * decides whether to surface the failure (dev-only `console.warn`,
167
+ * drop silently in production) — the validator is a pure shape gate.
168
+ */
169
+ export function validateContextData(slotName, value, spec, precompiledSlots) {
170
+ const violations = [];
171
+ const entry = spec[slotName];
172
+ if (!entry) {
173
+ violations.push({
174
+ field: 'contextSpec',
175
+ message: `Unknown context slot '${slotName}'. Declared slots: ${Object.keys(spec).join(', ') || '(none)'}`,
176
+ expected: Object.keys(spec).join(' | ') || '(none)',
177
+ received: slotName,
178
+ });
179
+ return { valid: false, violations };
180
+ }
181
+ if (entry.schema && value !== undefined) {
182
+ const validate = precompiledSlots?.get(slotName) ?? compileForValidation(entry.schema);
183
+ const ok = validate(value);
184
+ if (!ok) {
185
+ for (const v of prefixViolations(mapAjvErrorsToViolations(validate.errors, value), `${slotName}.value`)) {
186
+ violations.push(v);
187
+ }
188
+ }
189
+ }
190
+ return { valid: violations.length === 0, violations };
191
+ }
192
+ /**
193
+ * Validate an inbound user-action payload against the session's ActionSpec.
194
+ *
195
+ * Symmetric with {@link validatePropsData} / {@link validateStreamData}, but for
196
+ * live-channel INBOUND user → core traffic. Enforces the action contract at the
197
+ * wire boundary BEFORE the event is buffered or forwarded to an agent.
198
+ *
199
+ * Input shape mirrors `ActionEventValue` from `events.ts`:
200
+ * `{ action: string, data?: JsonValue, tool?: string }`
201
+ *
202
+ * Checks:
203
+ * - `action` is a non-empty string
204
+ * - `action` is declared in `spec` (a flat `Record<actionName,
205
+ * ActionEntry>` post-2026-04-22 flatten)
206
+ * - If the declared action has a `schema`, `data` matches it
207
+ *
208
+ * Actions without a declared schema are void-payload (fire-and-forget) — a
209
+ * present-but-unexpected `data` is tolerated to stay forward-compatible with
210
+ * clients that attach UI metadata the contract doesn't model. Contracts that
211
+ * want strict emptiness should declare `schema: { type: 'null' }`.
212
+ */
213
+ export function validateActionData(value, spec, precompiledActions) {
214
+ const violations = [];
215
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
216
+ violations.push({
217
+ field: 'value',
218
+ message: 'Action payload must be an object with an `action` field',
219
+ expected: 'object',
220
+ received: getJsonType(value),
221
+ });
222
+ return { valid: false, violations };
223
+ }
224
+ const record = value;
225
+ const actionId = record.action;
226
+ if (typeof actionId !== 'string' || actionId.length === 0) {
227
+ violations.push({
228
+ field: 'action',
229
+ message: 'Missing or empty `action` identifier',
230
+ expected: 'string',
231
+ received: getJsonType(actionId),
232
+ });
233
+ return { valid: false, violations };
234
+ }
235
+ const entry = spec[actionId];
236
+ if (!entry) {
237
+ violations.push({
238
+ field: 'action',
239
+ message: `Unknown action '${actionId}'. Declared actions: ${Object.keys(spec).join(', ') || '(none)'}`,
240
+ expected: Object.keys(spec).join(' | ') || '(none)',
241
+ received: actionId,
242
+ });
243
+ return { valid: false, violations };
244
+ }
245
+ if (entry.schema) {
246
+ const validate = precompiledActions?.get(actionId) ?? compileForValidation(entry.schema);
247
+ const ok = validate(record.data);
248
+ if (!ok) {
249
+ for (const v of prefixViolations(mapAjvErrorsToViolations(validate.errors, record.data), `${actionId}.data`)) {
250
+ violations.push(v);
251
+ }
252
+ }
253
+ }
254
+ return { valid: violations.length === 0, violations };
255
+ }
256
+ function getJsonType(value) {
257
+ if (value === null)
258
+ return 'null';
259
+ if (Array.isArray(value))
260
+ return 'array';
261
+ return typeof value;
262
+ }
263
+ /**
264
+ * Validate an inbound {@link ActionEnvelope} against the target stack
265
+ * item's {@link ActionSpec}. Payload-contract layer of live-channel
266
+ * inbound enforcement — the allowlist gate (`assertEventAllowed` in
267
+ * `@ggui-ai/mcp-server-handlers`) is a separate concern that runs
268
+ * first.
269
+ *
270
+ * Semantics:
271
+ * - `envelope.type !== 'data:submit'` → `{valid: true, violations: []}`.
272
+ * Only action submissions carry payload contract today; other
273
+ * event types (lifecycle, interaction, error) have no schema
274
+ * enforcement on this layer.
275
+ * - `spec === undefined` → `{valid: true, violations: []}`. Stack
276
+ * items without an actionSpec have no contract; legacy pushes keep
277
+ * flowing.
278
+ * - Otherwise `envelope.payload` is validated against `spec` via
279
+ * {@link validateActionData}. Same rules, same output shape.
280
+ *
281
+ * This helper does NOT enforce allowlist, session binding, or stack
282
+ * routing — those are ingress-plumbing concerns. Pure payload-shape
283
+ * check; returns `ValidationResult` rather than throwing so callers
284
+ * can decide whether to surface as a wire error, log, etc.
285
+ */
286
+ export function validateActionEnvelope(envelope, spec, precompiledActions) {
287
+ if (envelope.type !== 'data:submit')
288
+ return { valid: true, violations: [] };
289
+ if (!spec)
290
+ return { valid: true, violations: [] };
291
+ return validateActionData(envelope.payload, spec, precompiledActions);
292
+ }
293
+ /**
294
+ * Compile a contract's runtime-validated sub-schemas into standalone,
295
+ * eval-free ESM validator modules — the producer half of the
296
+ * precompiled-validator channel
297
+ * ({@link CompiledContractValidators} on `GguiBootstrapMeta`).
298
+ *
299
+ * The renderer iframe runs under a strict CSP with no `'unsafe-eval'`,
300
+ * so it cannot call `ajv.compile()` (which builds validators via
301
+ * `new Function`). Compilation therefore happens server-side at push
302
+ * time — where the contract schema is fixed and codegen is legal — and
303
+ * the iframe loads each emitted module via a `blob:` dynamic import.
304
+ *
305
+ * One module per runtime-validated surface, matching the four runtime
306
+ * validators in this file exactly (no second contract model):
307
+ *
308
+ * - `props` — the synthesized object wrapper from
309
+ * {@link buildPropsWrapperSchema}, as {@link validatePropsData}
310
+ * validates `props`.
311
+ * - `actions` — per-action `entry.schema`, as {@link validateActionData}
312
+ * validates `data`. Void actions (no `schema`) contribute no entry.
313
+ * - `streams` — per-channel `entry.schema`, as {@link validateStreamData}
314
+ * validates `payload`.
315
+ * - `context` — per-slot `entry.schema`, as {@link validateContextData}
316
+ * validates `value`.
317
+ *
318
+ * Returns `undefined` when the contract declares no runtime-validated
319
+ * schema at all — the bootstrap projection then omits the field.
320
+ */
321
+ export function compileContractValidators(specs) {
322
+ const compilePerEntry = (spec) => {
323
+ if (!spec)
324
+ return undefined;
325
+ const collected = {};
326
+ for (const [name, entry] of Object.entries(spec)) {
327
+ if (entry && entry.schema) {
328
+ collected[name] = compileValidatorModule(entry.schema);
329
+ }
330
+ }
331
+ return Object.keys(collected).length > 0 ? collected : undefined;
332
+ };
333
+ const out = {};
334
+ // A degenerate contract may carry `props: {}` (a propsSpec with no
335
+ // `properties`) — guard before `Object.keys` so the projection never
336
+ // throws on it; an empty propsSpec simply contributes no validator.
337
+ if (specs.propsSpec &&
338
+ specs.propsSpec.properties &&
339
+ Object.keys(specs.propsSpec.properties).length > 0) {
340
+ out.props = compileValidatorModule(buildPropsWrapperSchema(specs.propsSpec));
341
+ }
342
+ const actions = compilePerEntry(specs.actionSpec);
343
+ if (actions)
344
+ out.actions = actions;
345
+ const streams = compilePerEntry(specs.streamSpec);
346
+ if (streams)
347
+ out.streams = streams;
348
+ const context = compilePerEntry(specs.contextSpec);
349
+ if (context)
350
+ out.context = context;
351
+ return Object.keys(out).length > 0 ? out : undefined;
352
+ }
353
+ /**
354
+ * Format violations into a human-readable error message for the target agent.
355
+ */
356
+ export function formatViolations(violations) {
357
+ return violations
358
+ .map(v => `- ${v.field}: ${v.message}${v.expected ? ` (expected: ${v.expected}, got: ${v.received})` : ''}`)
359
+ .join('\n');
360
+ }
361
+ // =============================================================================
362
+ // Contract Structure Validation (Scenario 1: Bad Contract)
363
+ // =============================================================================
364
+ /**
365
+ * Validate the contract structure itself — catches malformed contract
366
+ * before they're persisted and used to validate runtime data.
367
+ *
368
+ * Checks:
369
+ * - PropsSpec properties have valid schemas (type or oneOf/anyOf defined)
370
+ * - Array schemas have items defined (otherwise element validation is impossible)
371
+ * - Object schemas with required fields reference existing properties
372
+ * - StreamSpec channels have schemas defined (reserved-prefix names rejected)
373
+ * - ActionSpec actions have schemas defined
374
+ * - ContextSpec slots: identifier-shape keys, no reserved keys
375
+ * (`__proto__`/`constructor`/`prototype`), schema present, default
376
+ * satisfies schema, debounceMs is a non-negative integer, no key
377
+ * collision with propsSpec, and `deriveContextDefault` yields a
378
+ * non-undefined initial value (see `validateContextStructure`).
379
+ * - Cross-reference invariants (`actionSpec.nextStep`,
380
+ * `streamSpec.source.tool` resolve to `agentCapabilities.tools[*]`).
381
+ * - Name invariants (no collision across actionSpec / streamSpec /
382
+ * contextSpec; no `_ggui:` reserved-prefix keys).
383
+ * - Schema-compat invariants (`actionSpec[*].schema` ⊆
384
+ * `tool.inputSchema`; `streamSpec[*].schema` ⊇ `tool.outputSchema`).
385
+ */
386
+ export function validateContractStructure(contract) {
387
+ const violations = [];
388
+ if (contract.propsSpec) {
389
+ const entries = Object.entries(contract.propsSpec.properties);
390
+ if (entries.length === 0) {
391
+ violations.push({
392
+ field: 'propsSpec',
393
+ message: 'PropsSpec has no properties — contract must define at least one prop',
394
+ expected: 'non-empty properties map',
395
+ received: 'empty',
396
+ });
397
+ }
398
+ for (const [name, entry] of entries) {
399
+ if (!entry.schema) {
400
+ violations.push({
401
+ field: `propsSpec.${name}`,
402
+ message: `Prop '${name}' has no schema — cannot validate data without a type definition`,
403
+ expected: 'schema with type',
404
+ received: 'undefined',
405
+ });
406
+ continue;
407
+ }
408
+ validateSchemaStructure(entry.schema, `propsSpec.${name}`, violations);
409
+ }
410
+ }
411
+ if (contract.streamSpec) {
412
+ for (const [channelName, entry] of Object.entries(contract.streamSpec)) {
413
+ if (isReservedChannelName(channelName)) {
414
+ // Server-owned namespace (see `./reserved-channels`). Agents
415
+ // can't declare here — these channels are emitted by the
416
+ // runtime, not the agent's contract. Skip further structural
417
+ // checks on this entry; the rejection is the authoritative
418
+ // violation.
419
+ violations.push({
420
+ field: `streamSpec.${channelName}`,
421
+ message: `Stream channel '${channelName}' is in the reserved '${RESERVED_CHANNEL_PREFIX}' namespace — server-owned channels cannot be declared in agent streamSpec`,
422
+ expected: `name not starting with '${RESERVED_CHANNEL_PREFIX}'`,
423
+ received: channelName,
424
+ });
425
+ continue;
426
+ }
427
+ if (!entry.schema) {
428
+ violations.push({
429
+ field: `streamSpec.${channelName}`,
430
+ message: `Stream channel '${channelName}' has no schema — cannot validate payloads`,
431
+ expected: 'schema with type',
432
+ received: 'undefined',
433
+ });
434
+ }
435
+ else {
436
+ validateSchemaStructure(entry.schema, `streamSpec.${channelName}`, violations);
437
+ }
438
+ }
439
+ }
440
+ if (contract.actionSpec) {
441
+ for (const [actionId, entry] of Object.entries(contract.actionSpec)) {
442
+ if (entry.schema) {
443
+ validateSchemaStructure(entry.schema, `actionSpec.${actionId}`, violations);
444
+ }
445
+ // ActionSpec schema is optional — actions can be void (e.g., button click with no payload)
446
+ }
447
+ }
448
+ if (contract.contextSpec) {
449
+ validateContextStructure(contract.contextSpec, violations, contract.propsSpec);
450
+ }
451
+ // Cross-reference invariants: actionSpec[*].nextStep and
452
+ // streamSpec[*].source.tool both resolve to agentCapabilities.tools[*]
453
+ // keys on the same contract. Folded into the same violations list so a
454
+ // single call surfaces both structural AND reference bugs.
455
+ for (const violation of checkCrossReferences(contract)) {
456
+ violations.push(violation);
457
+ }
458
+ // Name-invariant rules: no collisions across actionSpec / streamSpec
459
+ // / contextSpec keys; no `_ggui:` reserved-prefix keys on actionSpec
460
+ // or contextSpec (streamSpec reserved-prefix rejection is the
461
+ // dedicated `streamSpec.*` clause above). Same fold-into-violations
462
+ // posture as cross-references.
463
+ for (const violation of checkNameInvariants(contract)) {
464
+ violations.push(violation);
465
+ }
466
+ // Schema-compat invariants: action.schema ⊆ tool.inputSchema and
467
+ // channel.schema ⊇ tool.outputSchema, validated against the
468
+ // contract's OWN agentTools catalog. Distinct scope from the
469
+ // server-level F4 check (which compares against the live tool
470
+ // registry's zod schemas).
471
+ for (const violation of checkSchemaCompat(contract)) {
472
+ violations.push(violation);
473
+ }
474
+ return { valid: violations.length === 0, violations };
475
+ }
476
+ /**
477
+ * Slot-key identifier check for contextSpec. Slot keys are surfaced
478
+ * as React Context names by the boilerplate generator (`currentStep`
479
+ * → `CurrentStepContext`); a non-identifier here would break the
480
+ * generation step at runtime, so we reject at push-time instead.
481
+ */
482
+ const CONTEXT_SLOT_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
483
+ /**
484
+ * Reserved slot keys for contextSpec. Forbidden because they would
485
+ * shadow Object prototype slots when the runtime materializes the
486
+ * context map (e.g., `obj['__proto__'] = …` in pre-frozen
487
+ * environments would mutate the prototype chain rather than set a
488
+ * slot). Defensive — the runtime uses a frozen spec lookup, but the
489
+ * push-time gate keeps the invariant load-bearing for any future
490
+ * implementation that materializes the spec into a plain object.
491
+ */
492
+ const CONTEXT_RESERVED_KEYS = new Set([
493
+ '__proto__',
494
+ 'constructor',
495
+ 'prototype',
496
+ ]);
497
+ /**
498
+ * Per-slot structural validation for {@link ContextSpec}.
499
+ * Surfaces every violation alongside the surrounding actionSpec /
500
+ * streamSpec / props checks so authors see the full contract status
501
+ * in one pass instead of fix-and-retry per-field.
502
+ *
503
+ * Rules enforced:
504
+ * 1. Slot keys MUST match {@link CONTEXT_SLOT_KEY_PATTERN} (camelCase
505
+ * JS identifier) — boilerplate generates a React Context name
506
+ * from the key.
507
+ * 2. Slot keys MUST NOT be in {@link CONTEXT_RESERVED_KEYS}
508
+ * (`__proto__` / `constructor` / `prototype`).
509
+ * 3. `entry.schema` MUST be present and structurally valid (same
510
+ * structural check as actionSpec / streamSpec schemas).
511
+ * 4. If `entry.default` is present, it MUST satisfy `entry.schema`
512
+ * (same posture as `validatePropsData` for `default`).
513
+ * 5. If `entry.debounceMs` is present, it MUST be a non-negative
514
+ * integer (`0` allowed = immediate).
515
+ * 6. Slot keys MUST NOT collide with any key in
516
+ * {@link PropsSpec.properties} — the boilerplate generator emits
517
+ * `const [<slotKey>, set<PascalSlotKey>] = useState(...)` inside
518
+ * the same function scope as the destructured `props.<key>`, so a
519
+ * collision would shadow the prop binding.
520
+ * 7. {@link deriveContextDefault} MUST yield a non-undefined value
521
+ * for every slot — otherwise the boilerplate's
522
+ * `useState(<defaultExpr>)` call would emit `useState(undefined)`,
523
+ * defeating the typed initial-value contract. Schemas without a
524
+ * derivable primitive type (e.g., bare `oneOf`/`anyOf`) MUST set
525
+ * `entry.default` explicitly.
526
+ */
527
+ function validateContextStructure(spec, violations, props) {
528
+ const propKeys = props
529
+ ? new Set(Object.keys(props.properties))
530
+ : new Set();
531
+ for (const [slotKey, entry] of Object.entries(spec)) {
532
+ const path = `contextSpec.${slotKey}`;
533
+ if (CONTEXT_RESERVED_KEYS.has(slotKey)) {
534
+ violations.push({
535
+ field: path,
536
+ message: `context slot key '${slotKey}' is reserved (forbidden: ${[
537
+ ...CONTEXT_RESERVED_KEYS,
538
+ ]
539
+ .map(k => `'${k}'`)
540
+ .join(', ')})`,
541
+ expected: 'non-reserved identifier',
542
+ received: slotKey,
543
+ });
544
+ continue;
545
+ }
546
+ if (!CONTEXT_SLOT_KEY_PATTERN.test(slotKey)) {
547
+ violations.push({
548
+ field: path,
549
+ message: `context slot key '${slotKey}' is not a valid JS identifier (must match ${CONTEXT_SLOT_KEY_PATTERN})`,
550
+ expected: 'camelCase JS identifier',
551
+ received: slotKey,
552
+ });
553
+ continue;
554
+ }
555
+ if (propKeys.has(slotKey)) {
556
+ violations.push({
557
+ field: path,
558
+ message: `context slot key '${slotKey}' collides with propsSpec.properties.${slotKey} (would shadow the prop binding in generated boilerplate)`,
559
+ expected: 'slot key not present in propsSpec.properties',
560
+ received: slotKey,
561
+ });
562
+ continue;
563
+ }
564
+ if (!entry.schema) {
565
+ violations.push({
566
+ field: path,
567
+ message: `context slot '${slotKey}' has no schema — cannot validate slot values`,
568
+ expected: 'schema with type',
569
+ received: 'undefined',
570
+ });
571
+ continue;
572
+ }
573
+ validateSchemaStructure(entry.schema, path, violations);
574
+ if (entry.default !== undefined) {
575
+ try {
576
+ const validate = compileForValidation(entry.schema);
577
+ const ok = validate(entry.default);
578
+ if (!ok) {
579
+ for (const v of prefixViolations(mapAjvErrorsToViolations(validate.errors, entry.default), `${path}.default`)) {
580
+ violations.push(v);
581
+ }
582
+ }
583
+ }
584
+ catch {
585
+ // Schema malformed under Ajv strict mode. The dedicated
586
+ // `validateSchemaStructure` call earlier in this loop has
587
+ // already pushed a violation describing the schema problem;
588
+ // skipping the default check here avoids a noisy duplicate.
589
+ }
590
+ }
591
+ // Default-derivability — boilerplate emits `useState(<defaultExpr>)`,
592
+ // and `defaultExpr` must NOT be `undefined`. Resolves via
593
+ // `deriveContextDefault`: explicit `entry.default` first, then the
594
+ // schema-typed fallback. Schemas with no resolvable primitive type
595
+ // (e.g., bare `oneOf`/`anyOf`) require the author to set `default`
596
+ // explicitly.
597
+ if (deriveContextDefault(entry) === undefined) {
598
+ const t = entry.schema?.type;
599
+ violations.push({
600
+ field: path,
601
+ message: `context slot '${slotKey}' has no derivable default${t ? ` (schema type ${t})` : ''}; explicitly set entry.default`,
602
+ expected: 'derivable default via deriveContextDefault',
603
+ received: 'undefined',
604
+ });
605
+ }
606
+ if (entry.debounceMs !== undefined) {
607
+ if (typeof entry.debounceMs !== 'number' ||
608
+ !Number.isInteger(entry.debounceMs) ||
609
+ entry.debounceMs < 0) {
610
+ violations.push({
611
+ field: `${path}.debounceMs`,
612
+ message: `contextSpec[${slotKey}].debounceMs must be a non-negative integer (got ${String(entry.debounceMs)})`,
613
+ expected: 'non-negative integer',
614
+ received: getJsonType(entry.debounceMs),
615
+ });
616
+ }
617
+ }
618
+ }
619
+ }
620
+ /**
621
+ * Recursively validate a JSON Schema node for structural completeness.
622
+ */
623
+ function validateSchemaStructure(schema, path, violations) {
624
+ // Must have type, oneOf, or anyOf
625
+ if (!schema.type && !schema.oneOf && !schema.anyOf) {
626
+ violations.push({
627
+ field: path,
628
+ message: `Schema at '${path}' has no type, oneOf, or anyOf — cannot determine data shape`,
629
+ expected: 'type | oneOf | anyOf',
630
+ received: 'none',
631
+ });
632
+ return;
633
+ }
634
+ if (schema.type === 'array' && !schema.items) {
635
+ violations.push({
636
+ field: path,
637
+ message: `Array schema at '${path}' has no items — cannot validate array elements`,
638
+ expected: 'items schema',
639
+ received: 'undefined',
640
+ });
641
+ }
642
+ if (schema.type === 'object' && schema.required?.length) {
643
+ const definedProps = Object.keys(schema.properties ?? {});
644
+ for (const req of schema.required) {
645
+ if (!definedProps.includes(req)) {
646
+ violations.push({
647
+ field: `${path}.${req}`,
648
+ message: `Required field '${req}' is not defined in properties at '${path}'`,
649
+ expected: `'${req}' in properties`,
650
+ received: `properties: [${definedProps.join(', ')}]`,
651
+ });
652
+ }
653
+ }
654
+ }
655
+ // Recurse into nested schemas
656
+ if (schema.items) {
657
+ validateSchemaStructure(schema.items, `${path}.items`, violations);
658
+ }
659
+ if (schema.properties) {
660
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
661
+ validateSchemaStructure(propSchema, `${path}.${key}`, violations);
662
+ }
663
+ }
664
+ // Recurse into union branches
665
+ if (schema.oneOf) {
666
+ for (let i = 0; i < schema.oneOf.length; i++) {
667
+ validateSchemaStructure(schema.oneOf[i], `${path}.oneOf[${i}]`, violations);
668
+ }
669
+ }
670
+ if (schema.anyOf) {
671
+ for (let i = 0; i < schema.anyOf.length; i++) {
672
+ validateSchemaStructure(schema.anyOf[i], `${path}.anyOf[${i}]`, violations);
673
+ }
674
+ }
675
+ }
676
+ /**
677
+ * Typed error for contract violations.
678
+ *
679
+ * Thrown by commit/update/stream handlers when agent data doesn't match
680
+ * the negotiated contract. The MCP handler catches this and returns a
681
+ * structured error response with violations + hint so the agent can
682
+ * self-correct (fix data or propose a new contract via ggui_push).
683
+ */
684
+ function defaultHintFor(tool) {
685
+ if (tool === 'ggui_event') {
686
+ return 'The user-action payload did not match the session\'s actionSpec. Re-check the client-side action wiring, or have the agent push a new UI whose actionSpec covers this payload shape.';
687
+ }
688
+ return 'Fix your data to match the contract, or call ggui_push to create a new UI for this data shape.';
689
+ }
690
+ export class ContractViolationError extends Error {
691
+ violations;
692
+ tool;
693
+ hint;
694
+ constructor(opts) {
695
+ const formattedViolations = formatViolations(opts.violations);
696
+ super(`Contract violation in ${opts.tool}:\n${formattedViolations}`);
697
+ this.name = 'ContractViolationError';
698
+ this.violations = opts.violations;
699
+ this.tool = opts.tool;
700
+ this.hint = opts.hint ?? defaultHintFor(opts.tool);
701
+ }
702
+ /** Structured payload for MCP error response `data` field. */
703
+ toErrorData() {
704
+ return {
705
+ error: 'contract_violation',
706
+ tool: this.tool,
707
+ violations: this.violations,
708
+ hint: this.hint,
709
+ };
710
+ }
711
+ }