@ggui-ai/mcp-server-handlers 0.8.0 → 0.9.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.
- package/dist/renders/amend.d.ts +57 -0
- package/dist/renders/amend.d.ts.map +1 -0
- package/dist/renders/amend.js +64 -0
- package/dist/renders/apply-ggui-session-patch.d.ts +4 -0
- package/dist/renders/apply-ggui-session-patch.d.ts.map +1 -1
- package/dist/renders/apply-ggui-session-patch.js +1 -1
- package/dist/renders/assert-props-contract.d.ts +5 -4
- package/dist/renders/assert-props-contract.d.ts.map +1 -1
- package/dist/renders/assert-props-contract.js +6 -5
- package/dist/renders/consume.js +1 -1
- package/dist/renders/decide-handshake.d.ts +15 -0
- package/dist/renders/decide-handshake.d.ts.map +1 -1
- package/dist/renders/decide-handshake.js +18 -0
- package/dist/renders/declare-tool-catalog.js +1 -1
- package/dist/renders/handshake.d.ts.map +1 -1
- package/dist/renders/handshake.js +58 -12
- package/dist/renders/index.d.ts +5 -1
- package/dist/renders/index.d.ts.map +1 -1
- package/dist/renders/index.js +5 -1
- package/dist/renders/props-mutation-core.d.ts +60 -0
- package/dist/renders/props-mutation-core.d.ts.map +1 -0
- package/dist/renders/props-mutation-core.js +339 -0
- package/dist/renders/render.d.ts +2 -0
- package/dist/renders/render.d.ts.map +1 -1
- package/dist/renders/render.js +35 -30
- package/dist/renders/runtime-pull.d.ts +122 -0
- package/dist/renders/runtime-pull.d.ts.map +1 -0
- package/dist/renders/runtime-pull.js +178 -0
- package/dist/renders/runtime-telemetry.d.ts +60 -0
- package/dist/renders/runtime-telemetry.d.ts.map +1 -0
- package/dist/renders/runtime-telemetry.js +76 -0
- package/dist/renders/slice-meta-derivation.d.ts +57 -0
- package/dist/renders/slice-meta-derivation.d.ts.map +1 -1
- package/dist/renders/slice-meta-derivation.js +72 -1
- package/dist/renders/submit-action.d.ts +9 -0
- package/dist/renders/submit-action.d.ts.map +1 -1
- package/dist/renders/submit-action.js +70 -1
- package/dist/renders/update.d.ts +33 -42
- package/dist/renders/update.d.ts.map +1 -1
- package/dist/renders/update.js +98 -233
- package/package.json +7 -7
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROPS-MUTATION CORE — the single flow behind BOTH mutation tools
|
|
3
|
+
* (#483 tool split): `ggui_update` (advances the history epoch,
|
|
4
|
+
* mints a new record) and `ggui_amend` (in-place, epoch untouched).
|
|
5
|
+
* Extracted verbatim from the pre-split ggui_update handler; the
|
|
6
|
+
* per-tool deltas are parameterized on `tool`, everything else is
|
|
7
|
+
* byte-identical behavior.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ContractViolationError, } from '@ggui-ai/protocol';
|
|
11
|
+
import { GGUI_RENDER_UI_META } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
12
|
+
import { refreshRenderIdentity } from './render-identity.js';
|
|
13
|
+
import { applyGguiSessionPatch, } from './apply-ggui-session-patch.js';
|
|
14
|
+
import { GguiSessionNotFoundError } from './errors.js';
|
|
15
|
+
import { emitPayloadTraceEvent } from './payload-trace-sink.js';
|
|
16
|
+
/**
|
|
17
|
+
* Input raw-shape shared by BOTH mutation tools — discriminated on
|
|
18
|
+
* `kind` (`'replace'` + `props` | `'merge'` + `patch`); identical wire
|
|
19
|
+
* grammar for `ggui_update` and `ggui_amend` (#483). Both validate the
|
|
20
|
+
* FINAL props (post-merge for `merge`) against the render's
|
|
21
|
+
* `propsSpec`.
|
|
22
|
+
*/
|
|
23
|
+
export const mutationInputSchema = {
|
|
24
|
+
/**
|
|
25
|
+
* Globally-unique render id. Optional on the wire so an in-process
|
|
26
|
+
* dispatcher (live-channel dispatch / threaded mount) can populate it
|
|
27
|
+
* via `HandlerContext.sessionId` instead. Required at the core
|
|
28
|
+
* level — see the resolve step inside `runPropsMutation`.
|
|
29
|
+
*/
|
|
30
|
+
sessionId: z.string().optional(),
|
|
31
|
+
/**
|
|
32
|
+
* Mode discriminator. `'replace'` requires `props`; `'merge'`
|
|
33
|
+
* requires `patch`. The narrowing step inside the core enforces
|
|
34
|
+
* both presence + mutual exclusion.
|
|
35
|
+
*/
|
|
36
|
+
kind: z.enum(['replace', 'merge']),
|
|
37
|
+
/**
|
|
38
|
+
* Full new props map. Required when `kind === 'replace'`; rejected
|
|
39
|
+
* otherwise. Validated against the GguiSession's `propsSpec` after
|
|
40
|
+
* applying.
|
|
41
|
+
*/
|
|
42
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
43
|
+
/**
|
|
44
|
+
* RFC 7396 JSON Merge Patch. Required when `kind === 'merge'`;
|
|
45
|
+
* rejected otherwise. The core applies the patch to the existing
|
|
46
|
+
* props, then validates the merged result against `propsSpec`.
|
|
47
|
+
* `null` values in the patch DELETE the corresponding key.
|
|
48
|
+
*/
|
|
49
|
+
patch: z.record(z.string(), z.unknown()).optional(),
|
|
50
|
+
};
|
|
51
|
+
const inputSchema = mutationInputSchema;
|
|
52
|
+
/**
|
|
53
|
+
* Canonical JSON stringify — object keys sorted recursively so two
|
|
54
|
+
* semantically-equal JsonObjects compare equal regardless of key
|
|
55
|
+
* insertion order. Arrays keep positional order (order is meaning
|
|
56
|
+
* there). Used by the no-op gate; props are validated JsonObjects, so
|
|
57
|
+
* no cycles by construction.
|
|
58
|
+
*/
|
|
59
|
+
function stableStringify(value) {
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
62
|
+
}
|
|
63
|
+
if (value !== null && typeof value === 'object') {
|
|
64
|
+
const entries = Object.entries(value)
|
|
65
|
+
.filter(([, v]) => v !== undefined)
|
|
66
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
67
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
|
|
68
|
+
return `{${entries.join(',')}}`;
|
|
69
|
+
}
|
|
70
|
+
return JSON.stringify(value) ?? 'null';
|
|
71
|
+
}
|
|
72
|
+
export async function runPropsMutation(deps, tool, input, ctx) {
|
|
73
|
+
// Strict: in-process dispatchers face exactly the protocol
|
|
74
|
+
// contract (unknown keys reject, as `updateInputSchema` does). On
|
|
75
|
+
// the MCP wire path this is a no-op by construction — the SDK's
|
|
76
|
+
// own arg validation strips unknown keys before the handler runs.
|
|
77
|
+
const parsed = z.object(inputSchema).strict().parse(input);
|
|
78
|
+
// Narrow on kind FIRST. Each branch enforces required-field +
|
|
79
|
+
// mutual-exclusion semantics that the flat raw-shape can't
|
|
80
|
+
// express. Runs before the billing gate and before any store
|
|
81
|
+
// read, so a malformed call costs nothing: no gate evaluation,
|
|
82
|
+
// no DynamoDB read, no trace emission — and the caller gets the
|
|
83
|
+
// contract error rather than whatever the gate would have said.
|
|
84
|
+
const kind = parsed.kind;
|
|
85
|
+
let patchInput;
|
|
86
|
+
if (kind === 'replace') {
|
|
87
|
+
if (parsed.props === undefined) {
|
|
88
|
+
throw new ContractViolationError({
|
|
89
|
+
tool,
|
|
90
|
+
violations: [
|
|
91
|
+
{
|
|
92
|
+
code: 'CTR_UPDATE_MISSING_PROPS',
|
|
93
|
+
field: 'props',
|
|
94
|
+
message: 'kind:"replace" requires `props` — the full new props map.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (parsed.patch !== undefined) {
|
|
100
|
+
throw new ContractViolationError({
|
|
101
|
+
tool,
|
|
102
|
+
violations: [
|
|
103
|
+
{
|
|
104
|
+
code: 'CTR_UPDATE_MIXED_FIELDS',
|
|
105
|
+
field: 'patch',
|
|
106
|
+
message: 'kind:"replace" must not include `patch`. Pick one mode per call.',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
patchInput = { mode: 'replace', props: parsed.props };
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// kind === 'merge'
|
|
115
|
+
if (parsed.patch === undefined) {
|
|
116
|
+
throw new ContractViolationError({
|
|
117
|
+
tool,
|
|
118
|
+
violations: [
|
|
119
|
+
{
|
|
120
|
+
code: 'CTR_UPDATE_MISSING_PATCH',
|
|
121
|
+
field: 'patch',
|
|
122
|
+
message: 'kind:"merge" requires `patch` — the RFC 7396 JSON Merge Patch delta.',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (parsed.props !== undefined) {
|
|
128
|
+
throw new ContractViolationError({
|
|
129
|
+
tool,
|
|
130
|
+
violations: [
|
|
131
|
+
{
|
|
132
|
+
code: 'CTR_UPDATE_MIXED_FIELDS',
|
|
133
|
+
field: 'props',
|
|
134
|
+
message: 'kind:"merge" must not include `props`. Pick one mode per call.',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
patchInput = { mode: 'merge', patch: parsed.patch };
|
|
140
|
+
}
|
|
141
|
+
// Pre-mutation gate. Throws to abort BEFORE any state change.
|
|
142
|
+
// OSS default: no gate bound, no-op. Cloud binds a traffic-class
|
|
143
|
+
// gate here.
|
|
144
|
+
if (deps.billingGate) {
|
|
145
|
+
await deps.billingGate.preCheck({ ctx, tool });
|
|
146
|
+
}
|
|
147
|
+
// Resolve sessionId from wire OR threaded HandlerContext.
|
|
148
|
+
const sessionId = parsed.sessionId ?? ctx.sessionId;
|
|
149
|
+
if (!sessionId) {
|
|
150
|
+
throw new GguiSessionNotFoundError('', `${tool}: sessionId is required on the wire (or threaded via HandlerContext for in-process dispatchers).`);
|
|
151
|
+
}
|
|
152
|
+
// Tenancy gate. Cross-tenant + missing surface uniformly as
|
|
153
|
+
// GguiSessionNotFoundError so cross-tenant existence is not leaked.
|
|
154
|
+
const stored = await deps.renderStore.get(sessionId);
|
|
155
|
+
if (!stored || stored.appId !== ctx.appId) {
|
|
156
|
+
throw new GguiSessionNotFoundError(sessionId);
|
|
157
|
+
}
|
|
158
|
+
// Mount-URI identity (#471 round-4 live finding): `ggui_render`
|
|
159
|
+
// stamped `.../<sessionId>/<contractKey>` as the mounted iframe's
|
|
160
|
+
// resource URI. Hosts that key result→iframe routing on URI
|
|
161
|
+
// equality only deliver this update's result to the live card
|
|
162
|
+
// when the update reproduces that EXACT URI — a bare
|
|
163
|
+
// `.../<sessionId>` routes fine on OUR server but is a different
|
|
164
|
+
// string to the host, so the forwarded result misses the mount
|
|
165
|
+
// and the card never repaints. The contract key cannot be
|
|
166
|
+
// recomputed from the render row (it hashes the handshake-agreed
|
|
167
|
+
// contract — see render-identity.ts); read it from the durable
|
|
168
|
+
// identity record. Absent store/record ⇒ bare session URI
|
|
169
|
+
// (pre-identity renders keep the old shape).
|
|
170
|
+
let contractSegment = '';
|
|
171
|
+
if (deps.renderIdentityStore) {
|
|
172
|
+
try {
|
|
173
|
+
const identity = await deps.renderIdentityStore.get(sessionId);
|
|
174
|
+
if (identity?.contractKey) {
|
|
175
|
+
contractSegment = `/${identity.contractKey}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Best-effort, matching every identity-store touch in this
|
|
180
|
+
// handler: a failed read must not fail the tool call, and the
|
|
181
|
+
// bare URI below stays routable on our server.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const mountResourceUri = `${GGUI_RENDER_UI_META.resourceUri}/${sessionId}${contractSegment}`;
|
|
185
|
+
// Devtools payload trace. No-op when no sink is registered.
|
|
186
|
+
// Fires AFTER the tenancy gate so cross-tenant probes never leak
|
|
187
|
+
// into the trace. Payload is the validated wire shape.
|
|
188
|
+
emitPayloadTraceEvent({
|
|
189
|
+
direction: 'outbound-update',
|
|
190
|
+
sessionId,
|
|
191
|
+
appId: ctx.appId,
|
|
192
|
+
tool,
|
|
193
|
+
payload: parsed,
|
|
194
|
+
});
|
|
195
|
+
// applyGguiSessionPatch throws ContractViolationError{tool:'ggui_update'}
|
|
196
|
+
// on propsSpec fail (validated against the FINAL props — post-merge
|
|
197
|
+
// for `merge` mode). Propagates verbatim — transport layer maps.
|
|
198
|
+
//
|
|
199
|
+
// Pull renderTarget from `stored.render` — both ComponentGguiSession and
|
|
200
|
+
// SystemGguiSession satisfy `GguiSessionTarget` (id + optional propsSpec +
|
|
201
|
+
// optional props). McpAppsGguiSession has no propsSpec — the helper's
|
|
202
|
+
// assertPropsContract no-ops on absent spec, so MCP Apps renders
|
|
203
|
+
// accept any patch shape (the iframe owns its own validation).
|
|
204
|
+
const renderTarget = stored.render;
|
|
205
|
+
const { updatedSession, finalProps } = applyGguiSessionPatch({
|
|
206
|
+
render: renderTarget,
|
|
207
|
+
tool,
|
|
208
|
+
...patchInput,
|
|
209
|
+
});
|
|
210
|
+
// No-op gate (#471 round-3 live finding): a CONFORMING patch that
|
|
211
|
+
// leaves the final props semantically identical to the current
|
|
212
|
+
// state is almost always an agent error — the live failure mode
|
|
213
|
+
// was an LLM echoing the card's existing props back (believing it
|
|
214
|
+
// had "switched" the UI) and telling the user the card changed
|
|
215
|
+
// while nothing on screen could. The contract validated it, the
|
|
216
|
+
// commit would apply it, and no signal existed anywhere. Make the
|
|
217
|
+
// non-change OBSERVABLE instead of silently committing: skip the
|
|
218
|
+
// write + fan-out (there is nothing to deliver) and return an
|
|
219
|
+
// honest `updated: false` with a model-visible warning telling
|
|
220
|
+
// the agent what to do instead.
|
|
221
|
+
const priorProps = 'props' in renderTarget && renderTarget.props !== undefined
|
|
222
|
+
? renderTarget.props
|
|
223
|
+
: {};
|
|
224
|
+
if (stableStringify(finalProps) === stableStringify(priorProps)) {
|
|
225
|
+
return {
|
|
226
|
+
sessionId,
|
|
227
|
+
updated: false,
|
|
228
|
+
mountResourceUri,
|
|
229
|
+
// No record minted on a no-op — the head epoch is untouched.
|
|
230
|
+
epoch: stored.render.epoch ?? 0,
|
|
231
|
+
warning: 'NO-OP: this patch left the props identical to the current state — nothing changed on screen. If you meant to refresh the UI, send values that actually differ; if the user’s gesture asks for a DIFFERENT surface (menu selection, navigation), run ggui_handshake + ggui_render for the next UI instead of updating this one.',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Epoch (#483): the row is the AUTHORITY, advanced in the SAME
|
|
235
|
+
// commit as the props write — update mints a new history record
|
|
236
|
+
// (epoch + 1); amend leaves the head untouched. The reminted
|
|
237
|
+
// ledger event below is the wire SIGNAL frames latch on, not the
|
|
238
|
+
// counter (the event ring is horizon-bounded).
|
|
239
|
+
const priorEpoch = stored.render.epoch ?? 0;
|
|
240
|
+
const nextEpoch = tool === 'ggui_update' ? priorEpoch + 1 : priorEpoch;
|
|
241
|
+
const sessionToCommit = tool === 'ggui_update'
|
|
242
|
+
? { ...updatedSession, epoch: nextEpoch }
|
|
243
|
+
: updatedSession;
|
|
244
|
+
// Persist via the commit seam — first-write mints, re-write
|
|
245
|
+
// replaces visible-bits in place. Lifecycle fields owned by the
|
|
246
|
+
// store (createdAt, eventSequence, hostSession) preserved across
|
|
247
|
+
// the upsert.
|
|
248
|
+
const committed = await deps.renderStore.commit({
|
|
249
|
+
render: sessionToCommit,
|
|
250
|
+
appId: stored.appId,
|
|
251
|
+
...(stored.userId !== undefined ? { userId: stored.userId } : {}),
|
|
252
|
+
...(stored.endUserIdentity !== undefined
|
|
253
|
+
? { endUserIdentity: stored.endUserIdentity }
|
|
254
|
+
: {}),
|
|
255
|
+
...(stored.themeId !== undefined ? { themeId: stored.themeId } : {}),
|
|
256
|
+
...(stored.hostSession !== undefined
|
|
257
|
+
? { hostSession: stored.hostSession }
|
|
258
|
+
: {}),
|
|
259
|
+
});
|
|
260
|
+
// Ledger append — the delivery substrate for the PULL rungs of
|
|
261
|
+
// the failover ladder (HTTP `/events` polling and the
|
|
262
|
+
// `ggui_runtime_pull` bridge rung read ONLY the event ledger).
|
|
263
|
+
// Without this append a props update is invisible to every
|
|
264
|
+
// cursor-based reader: live evidence (#471 round-3 forensics)
|
|
265
|
+
// showed eventSequence stuck at 0 across real updates — pull
|
|
266
|
+
// clients would poll politely forever and receive nothing.
|
|
267
|
+
// Best-effort: persistence already succeeded, and the push
|
|
268
|
+
// planes (WS/SSE fan-out, forwarded tool-result slice) deliver
|
|
269
|
+
// independently of the ledger.
|
|
270
|
+
let committedForIdentity = committed;
|
|
271
|
+
try {
|
|
272
|
+
// `'ui.updated'` is the canonical LEDGER taxonomy name; the
|
|
273
|
+
// live-channel FRAME namespace calls the same thing
|
|
274
|
+
// `props_update`. The translation lives at the client parse
|
|
275
|
+
// core (events-polling), never here — the ledger speaks only
|
|
276
|
+
// taxonomy types.
|
|
277
|
+
// The props event carries the epoch it belongs to (#483) so
|
|
278
|
+
// frames can ignore props from a NEWER epoch than their own
|
|
279
|
+
// instead of flashing the new state before the freeze lands.
|
|
280
|
+
const seq = await deps.renderStore.appendEvent({
|
|
281
|
+
sessionId,
|
|
282
|
+
type: 'ui.updated',
|
|
283
|
+
data: { sessionId, props: finalProps, epoch: nextEpoch },
|
|
284
|
+
});
|
|
285
|
+
// The append advanced the row's high-water mark past the
|
|
286
|
+
// commit-time snapshot — thread the returned seq so the
|
|
287
|
+
// identity refresh records the ledger state INCLUDING this
|
|
288
|
+
// update's own event (record.seqAtLastCommit tracks the row).
|
|
289
|
+
committedForIdentity = { ...committed, eventSequence: seq };
|
|
290
|
+
// Epoch boundary AFTER the props event (#483): pinned #N
|
|
291
|
+
// reconstruction replays props UP TO AND INCLUDING the N-th
|
|
292
|
+
// remint boundary, so the boundary must follow its own props.
|
|
293
|
+
// This is also the freeze signal older-epoch frames latch on.
|
|
294
|
+
if (tool === 'ggui_update') {
|
|
295
|
+
const remintSeq = await deps.renderStore.appendEvent({
|
|
296
|
+
sessionId,
|
|
297
|
+
type: 'ui.reminted',
|
|
298
|
+
data: { epoch: nextEpoch },
|
|
299
|
+
});
|
|
300
|
+
committedForIdentity = {
|
|
301
|
+
...committedForIdentity,
|
|
302
|
+
eventSequence: remintSeq,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Silent — a ledger hiccup must not fail the tool call; the
|
|
308
|
+
// pull rungs degrade to the ack/mount snapshot on next
|
|
309
|
+
// (re)subscribe, same recovery every reader already has.
|
|
310
|
+
}
|
|
311
|
+
// Keep the durable identity record's view of this row current.
|
|
312
|
+
// Reads the row the commit RETURNED (seq-adjusted above) so the
|
|
313
|
+
// record can't disagree with what was persisted.
|
|
314
|
+
await refreshRenderIdentity(deps.renderIdentityStore, committedForIdentity);
|
|
315
|
+
// Best-effort live delivery. Persistence is the source of truth;
|
|
316
|
+
// the live-channel fan-out is a latency optimization. Errors are
|
|
317
|
+
// swallowed — a failed notify must not fail the tool call (the
|
|
318
|
+
// renderer reads canonical state via `ack.render` on next
|
|
319
|
+
// (re)subscribe).
|
|
320
|
+
if (deps.propsUpdateNotifier) {
|
|
321
|
+
try {
|
|
322
|
+
await deps.propsUpdateNotifier.sendPropsUpdate(sessionId, finalProps, nextEpoch);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// Silent: stay aligned with `safelyNotifyGguiSessionCommit`'s
|
|
326
|
+
// posture in render.ts. A throwing notifier is a host-side
|
|
327
|
+
// bug, not a tool-call failure.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Callers own presentation: ggui_update composes the pinned
|
|
331
|
+
// `#epoch` form from this bare mount URI; ggui_amend returns it
|
|
332
|
+
// bare (the live head).
|
|
333
|
+
return {
|
|
334
|
+
sessionId,
|
|
335
|
+
updated: true,
|
|
336
|
+
mountResourceUri,
|
|
337
|
+
epoch: nextEpoch,
|
|
338
|
+
};
|
|
339
|
+
}
|
package/dist/renders/render.d.ts
CHANGED
|
@@ -704,6 +704,7 @@ declare const outputSchema: {
|
|
|
704
704
|
VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
705
705
|
NO_PLATFORM_KEY: "NO_PLATFORM_KEY";
|
|
706
706
|
NO_CREDENTIALS: "NO_CREDENTIALS";
|
|
707
|
+
GENERATION_QUEUE_OVERLOADED: "GENERATION_QUEUE_OVERLOADED";
|
|
707
708
|
}>;
|
|
708
709
|
message: z.ZodString;
|
|
709
710
|
}, z.core.$strip>>;
|
|
@@ -713,6 +714,7 @@ declare const outputSchema: {
|
|
|
713
714
|
example: z.ZodString;
|
|
714
715
|
args: z.ZodObject<{
|
|
715
716
|
sessionId: z.ZodString;
|
|
717
|
+
timeout: z.ZodNumber;
|
|
716
718
|
}, z.core.$strip>;
|
|
717
719
|
}, z.core.$strip>>;
|
|
718
720
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/renders/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,iBAAiB,EAEtB,KAAK,YAAY,EAGjB,KAAK,WAAW,EAGjB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EAGd,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAOrB,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACV,mBAAmB,EAEpB,MAAM,uBAAuB,CAAC;AAO/B,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAY3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAQL,KAAK,gBAAgB,EAErB,KAAK,WAAW,EAEjB,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/renders/render.ts"],"names":[],"mappings":"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"}
|
package/dist/renders/render.js
CHANGED
|
@@ -64,7 +64,7 @@ import { validatePropsData, ContractViolationError, validateContract, renderInpu
|
|
|
64
64
|
import { emitCacheTraceEvent, newCacheTraceId, truncateCacheTraceIntent, } from './cache-trace-sink.js';
|
|
65
65
|
import { emitPayloadTraceEvent } from './payload-trace-sink.js';
|
|
66
66
|
import { isVisibleToCaller } from './tenancy.js';
|
|
67
|
-
import { assembleRenderSliceBase, deriveRenderMeta, derivePublicEnvProjection, deriveContractBundle, rewritePrivateBundleUrls, } from './slice-meta-derivation.js';
|
|
67
|
+
import { assembleRenderSliceBase, deriveRenderMeta, derivePublicEnvProjection, deriveContractBundle, rewritePrivateBundleUrls, spreadRenderMetaViewOntoSlice, } from './slice-meta-derivation.js';
|
|
68
68
|
/**
|
|
69
69
|
* Input raw-shape.
|
|
70
70
|
*
|
|
@@ -122,6 +122,7 @@ const RENDER_FAILURE_GUIDANCE = {
|
|
|
122
122
|
VALIDATION_ERROR: 'Fix the configuration or input named in the message',
|
|
123
123
|
NO_PLATFORM_KEY: 'Have the operator set a provider key for this app, or switch the app to a model whose key is configured',
|
|
124
124
|
NO_CREDENTIALS: 'Have the operator configure provider credentials on the server (environment variable or credentials file)',
|
|
125
|
+
GENERATION_QUEUE_OVERLOADED: 'No attempt was made — this is safe to retry immediately, or after a short backoff if it recurs',
|
|
125
126
|
};
|
|
126
127
|
/**
|
|
127
128
|
* Model-visible content text for the failure envelope. Pinned format:
|
|
@@ -138,7 +139,7 @@ function buildRenderFailureText(failure) {
|
|
|
138
139
|
* Project a generator-result {@link GenerationError} code onto the
|
|
139
140
|
* canonical render-failure enum. `COMPILATION_ERROR` is a
|
|
140
141
|
* finer-grained production failure, not a distinct wire class — it
|
|
141
|
-
* folds into `PRODUCTION_FAILED`; the other
|
|
142
|
+
* folds into `PRODUCTION_FAILED`; the other five members map 1:1.
|
|
142
143
|
*/
|
|
143
144
|
function toCanonicalRenderErrorCode(code) {
|
|
144
145
|
switch (code) {
|
|
@@ -148,6 +149,8 @@ function toCanonicalRenderErrorCode(code) {
|
|
|
148
149
|
return 'NO_PLATFORM_KEY';
|
|
149
150
|
case 'NO_CREDENTIALS':
|
|
150
151
|
return 'NO_CREDENTIALS';
|
|
152
|
+
case 'GENERATION_QUEUE_OVERLOADED':
|
|
153
|
+
return 'GENERATION_QUEUE_OVERLOADED';
|
|
151
154
|
case 'PRODUCTION_FAILED':
|
|
152
155
|
case 'COMPILATION_ERROR':
|
|
153
156
|
return 'PRODUCTION_FAILED';
|
|
@@ -210,10 +213,15 @@ export function createGguiRenderHandler(deps) {
|
|
|
210
213
|
inputSchema,
|
|
211
214
|
outputSchema,
|
|
212
215
|
_meta: {
|
|
213
|
-
// §2.4.1 entry-point lock: `_meta.ui.resourceUri`
|
|
214
|
-
// `_meta.ui.visibility` per the MCP Apps spec. Exactly
|
|
215
|
-
//
|
|
216
|
-
//
|
|
216
|
+
// §2.4.1 entry-point lock, REVISED 2026-08-11: `_meta.ui.resourceUri`
|
|
217
|
+
// + `_meta.ui.visibility` per the MCP Apps spec. Exactly TWO ggui
|
|
218
|
+
// tools carry these — ggui_render (the entry point) and
|
|
219
|
+
// ggui_update (same template). The lock originally said one, but
|
|
220
|
+
// live claude.ai evidence (#471 round-2 retest) showed hosts
|
|
221
|
+
// forward a tool's RESULTS to the mounted iframe only when the
|
|
222
|
+
// tool's declaration carries the binding — without it every
|
|
223
|
+
// ggui_update landed server-side and the iframe never repainted.
|
|
224
|
+
// Expanding beyond these two still requires revisiting this note.
|
|
217
225
|
//
|
|
218
226
|
// Legacy flat-key (`_meta["ui/resourceUri"]`) is stamped
|
|
219
227
|
// automatically by `registerAppTool` in `build-mcp.ts` — we
|
|
@@ -1225,9 +1233,15 @@ export function createGguiRenderHandler(deps) {
|
|
|
1225
1233
|
const nextStep = hasActions
|
|
1226
1234
|
? {
|
|
1227
1235
|
tool: 'ggui_consume',
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1236
|
+
// Imperative + honest: `timeout` MUST ride the hint —
|
|
1237
|
+
// consume's own default is 0 (single non-blocking drain),
|
|
1238
|
+
// so an agent copying a timeout-less example gets an
|
|
1239
|
+
// instant empty result and reasonably ends its turn (the
|
|
1240
|
+
// first live claude.ai test died exactly this way). 25 is
|
|
1241
|
+
// the server-enforced per-call cap (SPEC §7.3).
|
|
1242
|
+
description: "The UI has interactive actions. After this turn's other tool calls, you may long-poll once (waits up to 25s) to catch an immediate gesture; if `events` comes back empty, end your turn — later gestures arrive as new user messages carrying their own consume directive.",
|
|
1243
|
+
example: `ggui_consume({ sessionId: "${sessionId}", timeout: 25 })`,
|
|
1244
|
+
args: { sessionId, timeout: 25 },
|
|
1231
1245
|
}
|
|
1232
1246
|
: undefined;
|
|
1233
1247
|
// Render response architecture (2026-05-13):
|
|
@@ -1385,7 +1399,7 @@ export function createGguiRenderHandler(deps) {
|
|
|
1385
1399
|
// resolution (operator-debug-wins: live console pick beats the
|
|
1386
1400
|
// per-render `ggui_render.themeId` beats static deps) all live
|
|
1387
1401
|
// in ONE helper shared with `ggui_update`.
|
|
1388
|
-
const { runtimeUrl, authFields, themeId: resolvedThemeId, themeMode: resolvedThemeMode, } = assembleRenderSliceBase(deps, {
|
|
1402
|
+
const { runtimeUrl, authFields, channelUrls, themeId: resolvedThemeId, themeMode: resolvedThemeMode, } = assembleRenderSliceBase(deps, {
|
|
1389
1403
|
sessionId: output.sessionId,
|
|
1390
1404
|
appId: ctx.appId,
|
|
1391
1405
|
renderThemeId,
|
|
@@ -1436,17 +1450,14 @@ export function createGguiRenderHandler(deps) {
|
|
|
1436
1450
|
appId: ctx.appId,
|
|
1437
1451
|
runtimeUrl,
|
|
1438
1452
|
...authFields,
|
|
1453
|
+
// Token-bearing HTTP fallback rungs (pollingUrl + sseUrl),
|
|
1454
|
+
// composed by the ONE shared helper — present exactly when
|
|
1455
|
+
// the live trio above was minted and a session-API base
|
|
1456
|
+
// resolved. See `assembleRenderSliceBase`.
|
|
1457
|
+
...channelUrls,
|
|
1439
1458
|
...(streamWebSocketLocalTools !== undefined
|
|
1440
1459
|
? { streamWebSocketLocalTools }
|
|
1441
1460
|
: {}),
|
|
1442
|
-
// Operator-registered wrappers ride the render slice so the
|
|
1443
|
-
// runtime can dynamic-import each before mounting. Projected
|
|
1444
|
-
// by `deriveRenderMeta` from the (enriched) render contract;
|
|
1445
|
-
// only emitted when wrappers are actually declared so
|
|
1446
|
-
// pure-STDLIB apps stay byte-identical.
|
|
1447
|
-
...(view.gadgets !== undefined && view.gadgets.length > 0
|
|
1448
|
-
? { gadgets: view.gadgets }
|
|
1449
|
-
: {}),
|
|
1450
1461
|
// Minimum-disclosure subset of App.publicEnv (union of
|
|
1451
1462
|
// declared wrappers' `requires`). Filtered above by
|
|
1452
1463
|
// `derivePublicEnvProjection`.
|
|
@@ -1460,19 +1471,13 @@ export function createGguiRenderHandler(deps) {
|
|
|
1460
1471
|
...(resolvedThemeMode !== undefined
|
|
1461
1472
|
? { themeMode: resolvedThemeMode }
|
|
1462
1473
|
: {}),
|
|
1463
|
-
// Resolved per-app theme overlay (mode + `--ggui-*` variable map)
|
|
1464
|
-
// projected by `deriveRenderMeta` from the render's `theme`
|
|
1465
|
-
// sidecar. Only emitted when the App declared a theme so
|
|
1466
|
-
// theme-less apps stay byte-identical.
|
|
1467
|
-
...(view.theme !== undefined ? { theme: view.theme } : {}),
|
|
1468
|
-
...(view.permissionsPolicy !== undefined
|
|
1469
|
-
? { permissionsPolicy: [...view.permissionsPolicy] }
|
|
1470
|
-
: {}),
|
|
1471
1474
|
...(lastSequence !== undefined ? { lastSequence } : {}),
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1475
|
+
// State + policy view fields (gadgets, theme overlay,
|
|
1476
|
+
// permissionsPolicy, propsJson, contextSlots, and the #483
|
|
1477
|
+
// freeze-latch self-epoch — a fresh render is epoch 0) — ONE
|
|
1478
|
+
// shared spread so this emitter cannot drift from ggui_update /
|
|
1479
|
+
// the shell / the /state route.
|
|
1480
|
+
...spreadRenderMetaViewOntoSlice(view),
|
|
1476
1481
|
// Content-addressable contract validators. Both fields present
|
|
1477
1482
|
// together or absent together — iframe-runtime treats absence
|
|
1478
1483
|
// as "no validators".
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ggui_runtime_pull` — terminal bridge-pull rung of the live-channel
|
|
3
|
+
* failover ladder (WS → SSE → HTTP polling → bridge-pull).
|
|
4
|
+
*
|
|
5
|
+
* In a CSP-jailed MCP Apps host (claude.ai) the iframe can reach NO
|
|
6
|
+
* network origin — no WS, no SSE, no HTTP polling. The one channel it
|
|
7
|
+
* always has is the host's `tools/call` postMessage bridge. This tool
|
|
8
|
+
* serves the GguiSessionEvent ledger over that bridge: the
|
|
9
|
+
* `@ggui-ai/iframe-runtime` bridge rung pulls it on a flat interval,
|
|
10
|
+
* reusing the SAME cursor-walk algorithm (and the same parse core) as
|
|
11
|
+
* its HTTP `/events` polling rung, with `tools/call` as the carrier.
|
|
12
|
+
*
|
|
13
|
+
* **Wire shape** (what iframe-runtime postMessages via `tools/call`
|
|
14
|
+
* and the host relays to the MCP server):
|
|
15
|
+
*
|
|
16
|
+
* ```jsonc
|
|
17
|
+
* {
|
|
18
|
+
* "method": "tools/call",
|
|
19
|
+
* "params": {
|
|
20
|
+
* "name": "ggui_runtime_pull",
|
|
21
|
+
* "arguments": {
|
|
22
|
+
* "sessionId": "rnd_…", // bootstrap.sessionId
|
|
23
|
+
* "sinceSequence": 12, // replay cursor; omit = 0
|
|
24
|
+
* "limit": 100 // page size; clamped to 100
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* **Output = EXACT `EventsResponse` parity** with
|
|
31
|
+
* `GET /api/sessions/:sessionId/events` (`@ggui-ai/mcp-server`'s
|
|
32
|
+
* `api-renders-routes.ts`) — both read the ledger through the same
|
|
33
|
+
* `GguiSessionStore.listEventsSince` and answer with the same two
|
|
34
|
+
* shapes, so the client reuses one parse core across carriers:
|
|
35
|
+
*
|
|
36
|
+
* - Normal page: `{events, lastSequence, hasMore}` — events strictly
|
|
37
|
+
* ascending by seq with `seq > sinceSequence`, `lastSequence` = the
|
|
38
|
+
* render's high-water mark (advances the cursor even on an empty
|
|
39
|
+
* page), `hasMore` = the page was truncated by `limit` (client
|
|
40
|
+
* SHOULD immediately re-pull with the advanced cursor).
|
|
41
|
+
* - Horizon: `{reason: 'REPLAY_HORIZON_PASSED', currentSequence}` — a
|
|
42
|
+
* NORMAL result arm, not an error (the bridge rung is terminal;
|
|
43
|
+
* this is a re-sync instruction, mirroring the route's 410 body).
|
|
44
|
+
* Returned when the cursor fell out of the replayable window on
|
|
45
|
+
* EITHER side: `sinceSequence > lastSequence` (cursor from a
|
|
46
|
+
* different deployment / reset render) or
|
|
47
|
+
* `sinceSequence < horizonSeq` (events evicted from the bounded
|
|
48
|
+
* retention window). Client recovery: re-mount from a fresh
|
|
49
|
+
* snapshot and reset the cursor to `currentSequence`.
|
|
50
|
+
*
|
|
51
|
+
* **Failure modes:**
|
|
52
|
+
* - Unknown sessionId, cross-tenant sessionId, and a render deleted
|
|
53
|
+
* mid-read all surface uniformly as
|
|
54
|
+
* {@link GguiSessionNotFoundError} — no cross-tenant existence
|
|
55
|
+
* leak (tenancy gate: `renderStore.get` + `ctx.appId`).
|
|
56
|
+
* - Malformed input (empty sessionId, negative cursor, `limit < 1`)
|
|
57
|
+
* rejects at the zod boundary.
|
|
58
|
+
*
|
|
59
|
+
* **Visibility.** Registered with `_meta.ui.visibility: ['app']` so
|
|
60
|
+
* MCP Apps hosts route iframe-issued `tools/call` to this handler per
|
|
61
|
+
* spec §401 — REQUIRED: without it hosts reject view-issued calls, and
|
|
62
|
+
* the terminal rung goes dark exactly where it is the only channel
|
|
63
|
+
* left. Outer agents don't see the tool — ledger replay is a runtime
|
|
64
|
+
* concern, not an agent gesture.
|
|
65
|
+
*
|
|
66
|
+
* Deliberate divergences from the `/events` route, per the bridge-pull
|
|
67
|
+
* ruling: `sinceSequence` is optional (`?? 0` — the bridge rung owns
|
|
68
|
+
* its cursor), and `limit` above {@link RUNTIME_PULL_MAX_LIMIT} is
|
|
69
|
+
* CLAMPED, not rejected.
|
|
70
|
+
*/
|
|
71
|
+
import { z } from 'zod';
|
|
72
|
+
import { type GguiRuntimePullOutput } from '@ggui-ai/protocol';
|
|
73
|
+
import type { GguiSessionStore } from '@ggui-ai/mcp-server-core';
|
|
74
|
+
import type { SharedHandler } from '../types.js';
|
|
75
|
+
declare const inputSchema: {
|
|
76
|
+
readonly sessionId: z.ZodString;
|
|
77
|
+
readonly sinceSequence: z.ZodOptional<z.ZodNumber>;
|
|
78
|
+
readonly limit: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
readonly wait: z.ZodOptional<z.ZodNumber>;
|
|
80
|
+
};
|
|
81
|
+
declare const outputSchema: {
|
|
82
|
+
/** Normal page only — ledger rows with `seq > sinceSequence`, ascending. */
|
|
83
|
+
readonly events: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
84
|
+
seq: z.ZodNumber;
|
|
85
|
+
type: z.ZodString;
|
|
86
|
+
timestamp: z.ZodString;
|
|
87
|
+
data: z.ZodUnknown;
|
|
88
|
+
}, z.core.$strip>>>;
|
|
89
|
+
/** Normal page only — the render's current high-water mark (cursor floor on empty pages). */
|
|
90
|
+
readonly lastSequence: z.ZodOptional<z.ZodNumber>;
|
|
91
|
+
/** Normal page only — `true` when `limit` truncated the page; re-pull immediately. */
|
|
92
|
+
readonly hasMore: z.ZodOptional<z.ZodBoolean>;
|
|
93
|
+
/** Horizon arm only — the cursor fell out of the replayable window (normal result, not an error). */
|
|
94
|
+
readonly reason: z.ZodOptional<z.ZodLiteral<"REPLAY_HORIZON_PASSED">>;
|
|
95
|
+
/** Horizon arm only — reset the cursor here after re-mounting from a snapshot. */
|
|
96
|
+
readonly currentSequence: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
};
|
|
98
|
+
export interface GguiRuntimePullHandlerDeps {
|
|
99
|
+
/**
|
|
100
|
+
* The same store `ggui_render` commits to and the `/events` route
|
|
101
|
+
* reads from — one ledger, two carriers. The handler needs both
|
|
102
|
+
* `get` (tenancy gate) and `listEventsSince` (cursor read).
|
|
103
|
+
*/
|
|
104
|
+
readonly renderStore: GguiSessionStore;
|
|
105
|
+
/**
|
|
106
|
+
* Subscription-mode hold probe cadence, in ms. During a `wait` hold
|
|
107
|
+
* the handler re-reads the ledger at this interval — deliberately a
|
|
108
|
+
* store poll rather than an in-process wake seam, because the
|
|
109
|
+
* update that ends the hold may land on a DIFFERENT pod (the store
|
|
110
|
+
* is the only cross-pod truth this package may assume). Defaults to
|
|
111
|
+
* 1000. Test hook: inject a small value for deterministic hold
|
|
112
|
+
* tests without fake timers.
|
|
113
|
+
*/
|
|
114
|
+
readonly waitProbeIntervalMs?: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the `ggui_runtime_pull` handler. Registers as app-visible so
|
|
118
|
+
* MCP Apps hosts route iframe-issued `tools/call` to it per spec §401.
|
|
119
|
+
*/
|
|
120
|
+
export declare function createGguiRuntimePullHandler(deps: GguiRuntimePullHandlerDeps): SharedHandler<typeof inputSchema, typeof outputSchema, GguiRuntimePullOutput>;
|
|
121
|
+
export {};
|
|
122
|
+
//# sourceMappingURL=runtime-pull.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-pull.d.ts","sourceRoot":"","sources":["../../src/renders/runtime-pull.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjE,QAAA,MAAM,WAAW;;;;;CAAwB,CAAC;AAQ1C,QAAA,MAAM,YAAY;IAChB,4EAA4E;;;;;;;IAE5E,6FAA6F;;IAE7F,sFAAsF;;IAEtF,qGAAqG;;IAErG,kFAAkF;;CAE1E,CAAC;AAEX,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,0BAA0B,GAC/B,aAAa,CAAC,OAAO,WAAW,EAAE,OAAO,YAAY,EAAE,qBAAqB,CAAC,CAkG/E"}
|