stitchkit 0.55.0 → 0.56.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.
- package/dist/browser/socket-io.d.ts +14 -6
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/contract/errors.d.ts +7 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/files/boundary.d.ts +8 -5
- package/dist/files/boundary.d.ts.map +1 -1
- package/dist/files.js +1 -1
- package/dist/{index-rgd18hx3.js → index-28cqssm3.js} +7 -0
- package/dist/index-557by2db.js +34 -0
- package/dist/index-5r13htq1.js +283 -0
- package/dist/index-6y759j86.js +1031 -0
- package/dist/{index-npye5kt7.js → index-75njxz7p.js} +69 -7
- package/dist/{index-1k16zv57.js → index-bfcpjw20.js} +125 -31
- package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
- package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
- package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
- package/dist/{index-pzc32v75.js → index-sm2tjx06.js} +41 -4
- package/dist/{index-js7yexng.js → index-xy8fmh6w.js} +83 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -6
- package/dist/node.js +5 -4
- package/dist/observability/audit.d.ts +4 -0
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/event.d.ts +5 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +17 -11
- package/dist/remote.js +1 -1
- package/dist/server/context-contribution.d.ts +8 -0
- package/dist/server/context-contribution.d.ts.map +1 -1
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +1 -1
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +10 -6
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware/auth.d.ts +29 -2
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/request-body.d.ts.map +1 -1
- package/dist/testing/surface-conformance.d.ts +143 -10
- package/dist/testing/surface-conformance.d.ts.map +1 -1
- package/dist/testing/surface-manifest.d.ts +132 -33
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +470 -122
- package/dist/tools/async-operation.d.ts +210 -18
- package/dist/tools/async-operation.d.ts.map +1 -1
- package/dist/tools/define-download-tool.d.ts.map +1 -1
- package/dist/tools/define-upload-tool.d.ts.map +1 -1
- package/dist/tools/internal/surface-projector.d.ts +101 -0
- package/dist/tools/internal/surface-projector.d.ts.map +1 -0
- package/dist/tools/managed-file-error.d.ts +6 -0
- package/dist/tools/managed-file-error.d.ts.map +1 -0
- package/dist/tools/mcp-prepare.d.ts +3 -21
- package/dist/tools/mcp-prepare.d.ts.map +1 -1
- package/dist/tools/mcp-round-policy.d.ts +9 -0
- package/dist/tools/mcp-round-policy.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +0 -4
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +5 -9
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/surface.d.ts +7 -0
- package/dist/tools/surface.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +348 -383
- package/llms-full.txt +425 -66
- package/package.json +1 -1
- package/dist/index-h60df7rj.js +0 -110
- package/dist/index-k4dftwf7.js +0 -66
package/dist/testing.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./index-
|
|
2
|
+
RealtimeRequestDisconnectedError,
|
|
3
|
+
RealtimeRequestInvalidAcknowledgementError,
|
|
4
|
+
RealtimeRequestTimeoutError
|
|
5
|
+
} from "./index-557by2db.js";
|
|
6
|
+
import {
|
|
7
|
+
mcpProjectionCandidate,
|
|
8
|
+
prepareProjectedMcpTools,
|
|
9
|
+
projectToolSurface
|
|
10
|
+
} from "./index-6y759j86.js";
|
|
6
11
|
import {
|
|
7
12
|
toJsonSchema
|
|
8
13
|
} from "./index-cby4ar3v.js";
|
|
@@ -24,6 +29,7 @@ import { createHash } from "node:crypto";
|
|
|
24
29
|
import { z } from "zod";
|
|
25
30
|
var HttpMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
26
31
|
var ToolTransportSchema = z.enum(["MCP", "AGENT", "CLI"]);
|
|
32
|
+
var SurfaceTransportSchema = z.enum(["HTTP", "MCP", "AGENT", "CLI", "REALTIME"]);
|
|
27
33
|
var SurfaceSchemaDigestsSchema = z.object({
|
|
28
34
|
params: z.string().nullable(),
|
|
29
35
|
input: z.string().nullable(),
|
|
@@ -38,8 +44,19 @@ var SurfaceManifestOperationSchema = z.object({
|
|
|
38
44
|
scope: z.string().nullable(),
|
|
39
45
|
description: z.string(),
|
|
40
46
|
schemas: SurfaceSchemaDigestsSchema,
|
|
41
|
-
http: z.array(z.object({ method: HttpMethodSchema, path: z.string() }))
|
|
42
|
-
|
|
47
|
+
http: z.array(z.object({ method: HttpMethodSchema, path: z.string() }))
|
|
48
|
+
});
|
|
49
|
+
var SurfaceManifestToolSchema = z.object({
|
|
50
|
+
kind: z.enum(["contract", "runtime"]),
|
|
51
|
+
service: z.string(),
|
|
52
|
+
action: z.string(),
|
|
53
|
+
name: z.string(),
|
|
54
|
+
input: z.string()
|
|
55
|
+
});
|
|
56
|
+
var SurfaceManifestToolSurfaceSchema = z.object({
|
|
57
|
+
transport: ToolTransportSchema,
|
|
58
|
+
surface: z.string().nullable(),
|
|
59
|
+
tools: z.array(SurfaceManifestToolSchema)
|
|
43
60
|
});
|
|
44
61
|
var SurfaceManifestCliCommandSchema = z.object({
|
|
45
62
|
name: z.string(),
|
|
@@ -49,12 +66,26 @@ var SurfaceManifestCliCommandSchema = z.object({
|
|
|
49
66
|
});
|
|
50
67
|
var SurfaceManifestExtensionSchema = z.object({
|
|
51
68
|
name: z.string(),
|
|
52
|
-
transport:
|
|
69
|
+
transport: SurfaceTransportSchema
|
|
70
|
+
});
|
|
71
|
+
var SurfaceRealtimeSchemaPairSchema = z.object({
|
|
72
|
+
input: z.string(),
|
|
73
|
+
output: z.string()
|
|
74
|
+
});
|
|
75
|
+
var SurfaceManifestRealtimeEventSchema = z.object({
|
|
76
|
+
contract: z.string(),
|
|
77
|
+
direction: z.enum(["serverToClient", "clientToServer"]),
|
|
78
|
+
event: z.string(),
|
|
79
|
+
args: SurfaceRealtimeSchemaPairSchema,
|
|
80
|
+
acknowledgement: SurfaceRealtimeSchemaPairSchema.nullable()
|
|
53
81
|
});
|
|
54
82
|
var SurfaceManifestSchema = z.object({
|
|
55
|
-
manifestVersion: z.literal(
|
|
83
|
+
manifestVersion: z.literal(2),
|
|
56
84
|
digestVersion: z.literal(1),
|
|
57
85
|
operations: z.array(SurfaceManifestOperationSchema),
|
|
86
|
+
toolSurfaces: z.array(SurfaceManifestToolSurfaceSchema),
|
|
87
|
+
realtimeContracts: z.array(z.string()),
|
|
88
|
+
realtime: z.array(SurfaceManifestRealtimeEventSchema),
|
|
58
89
|
cliOnly: z.array(SurfaceManifestCliCommandSchema),
|
|
59
90
|
extensions: z.array(SurfaceManifestExtensionSchema)
|
|
60
91
|
});
|
|
@@ -100,6 +131,9 @@ function schemaDigest(schema, io) {
|
|
|
100
131
|
return null;
|
|
101
132
|
return digestValue(canonicalSchemaValue(toJsonSchema(schema, io, "any")));
|
|
102
133
|
}
|
|
134
|
+
function presentationDigest(schema) {
|
|
135
|
+
return digestValue(canonicalSchemaValue(schema));
|
|
136
|
+
}
|
|
103
137
|
function multipartDigest(value) {
|
|
104
138
|
return value === undefined ? null : digestValue(value);
|
|
105
139
|
}
|
|
@@ -110,114 +144,202 @@ function joinPath(...parts) {
|
|
|
110
144
|
function operationKey(kind, service, action) {
|
|
111
145
|
return `${kind}\x00${service}\x00${action}`;
|
|
112
146
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
147
|
+
function operationFrom(kind, source) {
|
|
148
|
+
return {
|
|
149
|
+
kind,
|
|
150
|
+
service: source.serviceName,
|
|
151
|
+
action: source.key,
|
|
152
|
+
method: source.method,
|
|
153
|
+
scope: source.scope ?? null,
|
|
154
|
+
description: source.desc,
|
|
155
|
+
schemas: {
|
|
156
|
+
params: schemaDigest(source.paramsSchema, "input"),
|
|
157
|
+
input: schemaDigest(source.inputSchema, "input"),
|
|
158
|
+
output: schemaDigest(source.outputSchema, "output"),
|
|
159
|
+
multipart: multipartDigest(source.multipart)
|
|
160
|
+
},
|
|
161
|
+
http: []
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function operationFingerprint(source) {
|
|
165
|
+
const mcp = source.mcp ? {
|
|
166
|
+
inputRequired: source.mcp.inputRequired.map((request) => ({
|
|
167
|
+
key: request.key,
|
|
168
|
+
message: request.message,
|
|
169
|
+
schema: schemaDigest(request.schema, "input")
|
|
170
|
+
}))
|
|
171
|
+
} : null;
|
|
172
|
+
return serializeSurfaceValue({
|
|
173
|
+
method: source.method,
|
|
174
|
+
serviceName: source.serviceName,
|
|
175
|
+
key: source.key,
|
|
176
|
+
desc: source.desc,
|
|
177
|
+
scope: source.scope ?? null,
|
|
178
|
+
path: source.path ?? null,
|
|
179
|
+
expose: source.expose ? [...source.expose].sort(compareCodepoints) : null,
|
|
180
|
+
toolName: source.toolName ?? null,
|
|
181
|
+
annotations: source.annotations ?? null,
|
|
182
|
+
ui: source.ui ?? null,
|
|
183
|
+
mcp,
|
|
184
|
+
rawBody: source.rawBody ?? false,
|
|
185
|
+
rawResponse: source.rawResponse ?? false,
|
|
186
|
+
responseMeta: source.responseMeta ?? null,
|
|
187
|
+
maxJsonBodyBytes: source.maxJsonBodyBytes ?? null,
|
|
188
|
+
idempotent: source.idempotent ?? false,
|
|
189
|
+
contentType: source.contentType ?? null,
|
|
190
|
+
meta: source.meta ?? null,
|
|
191
|
+
schemas: {
|
|
192
|
+
params: schemaDigest(source.paramsSchema, "input"),
|
|
193
|
+
input: schemaDigest(source.inputSchema, "input"),
|
|
194
|
+
output: schemaDigest(source.outputSchema, "output"),
|
|
195
|
+
multipart: multipartDigest(source.multipart)
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function sortTools(tools) {
|
|
200
|
+
return tools.sort((left, right) => compareCodepoints(left.name, right.name) || compareCodepoints(left.service, right.service) || compareCodepoints(left.action, right.action) || compareCodepoints(left.kind, right.kind));
|
|
122
201
|
}
|
|
123
202
|
function buildSurfaceManifest(config) {
|
|
124
203
|
const operations = new Map;
|
|
125
|
-
const
|
|
204
|
+
const operationFingerprints = new Map;
|
|
205
|
+
const operationDefinitions = new Map;
|
|
206
|
+
const addOperation = (kind, source) => {
|
|
207
|
+
const candidate = operationFrom(kind, source);
|
|
208
|
+
const key = operationKey(kind, candidate.service, candidate.action);
|
|
209
|
+
const definition = source.definitionToken ?? source;
|
|
210
|
+
const existing = operations.get(key);
|
|
211
|
+
if (!existing) {
|
|
212
|
+
operations.set(key, candidate);
|
|
213
|
+
operationFingerprints.set(key, operationFingerprint(source));
|
|
214
|
+
operationDefinitions.set(key, definition);
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
if (operationDefinitions.get(key) !== definition || operationFingerprints.get(key) !== operationFingerprint(source)) {
|
|
218
|
+
throw new Error(`Conflicting ${kind} operation identity ${candidate.service}.${candidate.action}`);
|
|
219
|
+
}
|
|
220
|
+
return existing;
|
|
221
|
+
};
|
|
222
|
+
const addHttp = (service, prefix) => {
|
|
126
223
|
for (const method of Object.values(service.methods)) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const http = !method.expose || method.expose.includes("HTTP") ? [{ method: method.method, path: joinPath(prefix, service.prefix, method.path) }] : [];
|
|
130
|
-
if (existing) {
|
|
131
|
-
existing.http.push(...http);
|
|
224
|
+
const operation = addOperation("contract", method);
|
|
225
|
+
if (method.expose && !method.expose.includes("HTTP"))
|
|
132
226
|
continue;
|
|
133
|
-
|
|
134
|
-
operations.set(key, {
|
|
135
|
-
kind: "contract",
|
|
136
|
-
service: service.name,
|
|
137
|
-
action: method.key,
|
|
227
|
+
const route = {
|
|
138
228
|
method: method.method,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
output: schemaDigest(method.outputSchema, "output"),
|
|
145
|
-
multipart: multipartDigest(method.multipart)
|
|
146
|
-
},
|
|
147
|
-
http,
|
|
148
|
-
tools: {}
|
|
149
|
-
});
|
|
229
|
+
path: joinPath(prefix, service.prefix, method.path)
|
|
230
|
+
};
|
|
231
|
+
if (!operation.http.some((entry) => entry.method === route.method && entry.path === route.path)) {
|
|
232
|
+
operation.http.push(route);
|
|
233
|
+
}
|
|
150
234
|
}
|
|
151
235
|
};
|
|
152
236
|
for (const service of config.services ?? []) {
|
|
153
|
-
|
|
237
|
+
addHttp(service, config.scopePrefixes?.[service.scope] ?? "");
|
|
154
238
|
}
|
|
155
239
|
for (const group of config.groups ?? []) {
|
|
156
240
|
for (const service of group.services)
|
|
157
|
-
|
|
241
|
+
addHttp(service, group.pathPrefix ?? "");
|
|
158
242
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
243
|
+
const toolSurfaces = [];
|
|
244
|
+
const addToolProjection = (transport, surface, projection, presentation = {}) => {
|
|
245
|
+
const projected = projectToolSurface(projection, transport, transport === "MCP" ? config.mcpPreparation ?? {} : presentation);
|
|
246
|
+
const collected = transport === "MCP" ? prepareProjectedMcpTools(projected.map(mcpProjectionCandidate), config.mcpPreparation ?? {}).map((prepared) => ({
|
|
247
|
+
...prepared.tool,
|
|
248
|
+
presentationSchema: prepared.inputSchema
|
|
249
|
+
})) : projected;
|
|
250
|
+
const tools = [];
|
|
251
|
+
for (const entry of collected) {
|
|
252
|
+
const source = entry.kind === "contract" ? entry.source : {
|
|
253
|
+
definitionToken: entry.source,
|
|
254
|
+
method: entry.source.identity.method,
|
|
255
|
+
serviceName: entry.source.identity.serviceName,
|
|
256
|
+
key: entry.source.identity.action,
|
|
257
|
+
desc: entry.source.description,
|
|
258
|
+
...entry.source.identity.scope !== undefined && {
|
|
259
|
+
scope: entry.source.identity.scope
|
|
260
|
+
},
|
|
261
|
+
inputSchema: entry.source.input,
|
|
262
|
+
...entry.source.output !== undefined && {
|
|
263
|
+
outputSchema: entry.source.output
|
|
264
|
+
},
|
|
265
|
+
toolName: entry.source.name,
|
|
266
|
+
expose: entry.source.transports,
|
|
267
|
+
...entry.source.annotations !== undefined && {
|
|
268
|
+
annotations: entry.source.annotations
|
|
269
|
+
},
|
|
270
|
+
...entry.source.ui !== undefined && { ui: entry.source.ui },
|
|
271
|
+
...entry.source.identity.meta !== undefined && {
|
|
272
|
+
meta: entry.source.identity.meta
|
|
273
|
+
},
|
|
274
|
+
...entry.source.mcp !== undefined && { mcp: entry.source.mcp }
|
|
275
|
+
};
|
|
276
|
+
const operation = addOperation(entry.kind, source);
|
|
277
|
+
tools.push({
|
|
278
|
+
kind: entry.kind,
|
|
279
|
+
service: operation.service,
|
|
280
|
+
action: operation.action,
|
|
281
|
+
name: entry.name,
|
|
282
|
+
input: presentationDigest(entry.presentationSchema)
|
|
283
|
+
});
|
|
164
284
|
}
|
|
165
|
-
|
|
166
|
-
kind: "runtime",
|
|
167
|
-
service: identity.serviceName,
|
|
168
|
-
action: identity.action,
|
|
169
|
-
method: identity.method,
|
|
170
|
-
scope: identity.scope ?? null,
|
|
171
|
-
description: definition.description,
|
|
172
|
-
schemas: {
|
|
173
|
-
params: null,
|
|
174
|
-
input: schemaDigest(definition.input, "input"),
|
|
175
|
-
output: schemaDigest(definition.output, "output"),
|
|
176
|
-
multipart: null
|
|
177
|
-
},
|
|
178
|
-
http: [],
|
|
179
|
-
tools: {}
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
const usedNames = {
|
|
183
|
-
MCP: new Set,
|
|
184
|
-
AGENT: new Set,
|
|
185
|
-
CLI: new Set
|
|
285
|
+
toolSurfaces.push({ transport, surface, tools: sortTools(tools) });
|
|
186
286
|
};
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const surface = transport === "CLI" ? "CLI command" : transport === "MCP" ? "MCP tool name" : "agent tool name";
|
|
191
|
-
assertUniqueToolName(name, usedNames[transport].has(name), surface);
|
|
192
|
-
usedNames[transport].add(name);
|
|
193
|
-
operation.tools[transport] = name;
|
|
287
|
+
const shared = {
|
|
288
|
+
services: config.services,
|
|
289
|
+
runtimeTools: config.runtimeTools
|
|
194
290
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
if (method.expose?.includes("CLI"))
|
|
208
|
-
addTool(operation, name, "CLI");
|
|
291
|
+
const explicitMcp = config.toolSurfaces?.MCP;
|
|
292
|
+
const namedMcp = config.mcpSurfaces;
|
|
293
|
+
if (explicitMcp && namedMcp) {
|
|
294
|
+
throw new Error("Surface manifest MCP topology must use either one direct surface or named surfaces");
|
|
295
|
+
}
|
|
296
|
+
if (explicitMcp)
|
|
297
|
+
addToolProjection("MCP", null, explicitMcp);
|
|
298
|
+
if (namedMcp) {
|
|
299
|
+
for (const name of Object.keys(namedMcp).sort(compareCodepoints)) {
|
|
300
|
+
const projection = namedMcp[name];
|
|
301
|
+
if (projection)
|
|
302
|
+
addToolProjection("MCP", name, projection);
|
|
209
303
|
}
|
|
210
304
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
305
|
+
if (!explicitMcp && !namedMcp)
|
|
306
|
+
addToolProjection("MCP", null, shared);
|
|
307
|
+
const agent = config.toolSurfaces?.AGENT;
|
|
308
|
+
addToolProjection("AGENT", null, agent ?? shared, agent ?? {});
|
|
309
|
+
addToolProjection("CLI", null, config.toolSurfaces?.CLI ?? shared);
|
|
310
|
+
const realtime = [];
|
|
311
|
+
const realtimeContracts = Object.keys(config.realtime ?? {}).sort(compareCodepoints);
|
|
312
|
+
for (const contractName of realtimeContracts) {
|
|
313
|
+
const contract = config.realtime?.[contractName];
|
|
314
|
+
if (!contract)
|
|
214
315
|
continue;
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
316
|
+
for (const direction of ["serverToClient", "clientToServer"]) {
|
|
317
|
+
const registry = contract[direction];
|
|
318
|
+
for (const event of Object.keys(registry).sort(compareCodepoints)) {
|
|
319
|
+
const definition = registry[event];
|
|
320
|
+
if (!definition)
|
|
321
|
+
continue;
|
|
322
|
+
const argsInput = schemaDigest(definition.args, "input");
|
|
323
|
+
const argsOutput = schemaDigest(definition.args, "output");
|
|
324
|
+
if (!argsInput || !argsOutput)
|
|
325
|
+
throw new Error("Realtime args schema digest missing");
|
|
326
|
+
let acknowledgement = null;
|
|
327
|
+
if (definition.ack) {
|
|
328
|
+
const input = schemaDigest(definition.ack, "input");
|
|
329
|
+
const output = schemaDigest(definition.ack, "output");
|
|
330
|
+
if (!input || !output)
|
|
331
|
+
throw new Error("Realtime ack schema digest missing");
|
|
332
|
+
acknowledgement = { input, output };
|
|
333
|
+
}
|
|
334
|
+
realtime.push({
|
|
335
|
+
contract: contractName,
|
|
336
|
+
direction,
|
|
337
|
+
event,
|
|
338
|
+
args: { input: argsInput, output: argsOutput },
|
|
339
|
+
acknowledgement
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
221
343
|
}
|
|
222
344
|
const cliOnly = (config.cliCommands ?? []).map((command) => ({
|
|
223
345
|
name: command.name,
|
|
@@ -225,10 +347,16 @@ function buildSurfaceManifest(config) {
|
|
|
225
347
|
input: schemaDigest(command.input, "input") ?? digestValue({}),
|
|
226
348
|
output: schemaDigest(command.output, "output")
|
|
227
349
|
})).sort((left, right) => compareCodepoints(left.name, right.name));
|
|
350
|
+
for (const operation of operations.values()) {
|
|
351
|
+
operation.http.sort((left, right) => compareCodepoints(left.path, right.path) || compareCodepoints(left.method, right.method));
|
|
352
|
+
}
|
|
228
353
|
return SurfaceManifestSchema.parse({
|
|
229
|
-
manifestVersion:
|
|
354
|
+
manifestVersion: 2,
|
|
230
355
|
digestVersion: 1,
|
|
231
356
|
operations: [...operations.values()].sort((left, right) => compareCodepoints(left.service, right.service) || compareCodepoints(left.action, right.action) || compareCodepoints(left.kind, right.kind)),
|
|
357
|
+
toolSurfaces: toolSurfaces.sort((left, right) => compareCodepoints(left.transport, right.transport) || compareCodepoints(left.surface ?? "", right.surface ?? "")),
|
|
358
|
+
realtimeContracts,
|
|
359
|
+
realtime,
|
|
232
360
|
cliOnly,
|
|
233
361
|
extensions: [...config.extensions ?? []].sort((left, right) => compareCodepoints(left.transport, right.transport) || compareCodepoints(left.name, right.name))
|
|
234
362
|
});
|
|
@@ -244,13 +372,182 @@ actual: ${actual}`);
|
|
|
244
372
|
}
|
|
245
373
|
|
|
246
374
|
// src/testing/surface-conformance.ts
|
|
247
|
-
var
|
|
248
|
-
|
|
249
|
-
|
|
375
|
+
var RealtimeRejectionObservationSchema = z2.object({
|
|
376
|
+
direction: z2.enum([
|
|
377
|
+
"client-inbound",
|
|
378
|
+
"client-outbound",
|
|
379
|
+
"server-inbound",
|
|
380
|
+
"server-outbound"
|
|
381
|
+
]),
|
|
382
|
+
phase: z2.enum(["arguments", "acknowledgement"]),
|
|
383
|
+
reason: z2.enum([
|
|
384
|
+
"unknown-event",
|
|
385
|
+
"invalid-arguments",
|
|
386
|
+
"invalid-acknowledgement-value",
|
|
387
|
+
"missing-acknowledgement"
|
|
388
|
+
]),
|
|
389
|
+
fault: z2.enum(["peer", "local"])
|
|
390
|
+
});
|
|
391
|
+
var ObservationFields = {
|
|
250
392
|
code: z2.string().optional(),
|
|
251
393
|
data: z2.unknown().optional(),
|
|
252
|
-
diagnostic: z2.string().optional()
|
|
394
|
+
diagnostic: z2.string().optional(),
|
|
395
|
+
handlerCalls: z2.number().int().nonnegative().optional()
|
|
396
|
+
};
|
|
397
|
+
var RealtimeDisconnectObservationSchema = z2.object({
|
|
398
|
+
phase: z2.enum(["before-invoke", "in-flight"])
|
|
253
399
|
});
|
|
400
|
+
var TransportObservationSchema = z2.discriminatedUnion("outcome", [
|
|
401
|
+
z2.object({ outcome: z2.literal("success"), ...ObservationFields }),
|
|
402
|
+
z2.object({ outcome: z2.literal("validation_error"), ...ObservationFields }),
|
|
403
|
+
z2.object({ outcome: z2.literal("domain_error"), ...ObservationFields }),
|
|
404
|
+
z2.object({ outcome: z2.literal("aborted"), ...ObservationFields }),
|
|
405
|
+
z2.object({
|
|
406
|
+
outcome: z2.literal("realtime_rejected"),
|
|
407
|
+
...ObservationFields,
|
|
408
|
+
rejection: RealtimeRejectionObservationSchema
|
|
409
|
+
}),
|
|
410
|
+
z2.object({
|
|
411
|
+
outcome: z2.literal("disconnected"),
|
|
412
|
+
...ObservationFields,
|
|
413
|
+
disconnect: RealtimeDisconnectObservationSchema
|
|
414
|
+
}),
|
|
415
|
+
z2.object({ outcome: z2.literal("timeout"), ...ObservationFields })
|
|
416
|
+
]);
|
|
417
|
+
function scenarioOutcome(scenario) {
|
|
418
|
+
if (scenario === "event" || scenario === "acknowledgement")
|
|
419
|
+
return "success";
|
|
420
|
+
if (scenario === "invalid_arguments" || scenario === "invalid_acknowledgement") {
|
|
421
|
+
return "realtime_rejected";
|
|
422
|
+
}
|
|
423
|
+
if (scenario === "disconnected" || scenario === "in_flight_disconnect") {
|
|
424
|
+
return "disconnected";
|
|
425
|
+
}
|
|
426
|
+
return "timeout";
|
|
427
|
+
}
|
|
428
|
+
function assertScenarioExpectation(scenario, expected) {
|
|
429
|
+
const outcome = scenarioOutcome(scenario);
|
|
430
|
+
if (expected.outcome !== outcome) {
|
|
431
|
+
throw new TypeError(`Realtime probe scenario "${scenario}" requires outcome "${outcome}"`);
|
|
432
|
+
}
|
|
433
|
+
if (expected.outcome === "realtime_rejected" && (scenario === "invalid_arguments" && expected.rejection.phase !== "arguments" || scenario === "invalid_acknowledgement" && expected.rejection.phase !== "acknowledgement")) {
|
|
434
|
+
throw new TypeError(`Realtime probe scenario "${scenario}" has incompatible rejection phase`);
|
|
435
|
+
}
|
|
436
|
+
if (scenario === "invalid_arguments" && expected.handlerCalls !== 0) {
|
|
437
|
+
throw new TypeError('Realtime probe scenario "invalid_arguments" requires handlerCalls: 0');
|
|
438
|
+
}
|
|
439
|
+
if (expected.outcome === "disconnected") {
|
|
440
|
+
const phase = scenario === "disconnected" ? "before-invoke" : "in-flight";
|
|
441
|
+
if (expected.disconnect.phase !== phase) {
|
|
442
|
+
throw new TypeError(`Realtime probe scenario "${scenario}" requires disconnect phase "${phase}"`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function defineRealtimeProbe(config) {
|
|
447
|
+
const expected = TransportObservationSchema.parse(config.expected);
|
|
448
|
+
assertScenarioExpectation(config.scenario, expected);
|
|
449
|
+
return {
|
|
450
|
+
name: config.name,
|
|
451
|
+
fixture: { scenario: config.scenario, value: config.fixture },
|
|
452
|
+
transports: ["REALTIME"],
|
|
453
|
+
expected: { REALTIME: expected },
|
|
454
|
+
setup: config.setup,
|
|
455
|
+
teardown: config.teardown
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function rejectionObservation(rejected) {
|
|
459
|
+
return RealtimeRejectionObservationSchema.parse(rejected);
|
|
460
|
+
}
|
|
461
|
+
function createRealtimeProbeDriver({
|
|
462
|
+
bind,
|
|
463
|
+
handlerCalls
|
|
464
|
+
}) {
|
|
465
|
+
return {
|
|
466
|
+
async invoke(fixture, signal) {
|
|
467
|
+
let rejected;
|
|
468
|
+
const currentRejection = () => rejected;
|
|
469
|
+
const adapter = bind((event) => {
|
|
470
|
+
rejected = event;
|
|
471
|
+
}, fixture);
|
|
472
|
+
const connectedBefore = adapter.connected();
|
|
473
|
+
const callsBefore = handlerCalls?.();
|
|
474
|
+
const withCalls = (observation) => {
|
|
475
|
+
if (callsBefore === undefined || !handlerCalls)
|
|
476
|
+
return observation;
|
|
477
|
+
return { ...observation, handlerCalls: handlerCalls() - callsBefore };
|
|
478
|
+
};
|
|
479
|
+
try {
|
|
480
|
+
signal.throwIfAborted();
|
|
481
|
+
const data = await adapter.invoke(fixture, signal);
|
|
482
|
+
const observedRejection = currentRejection();
|
|
483
|
+
if (observedRejection) {
|
|
484
|
+
return withCalls({
|
|
485
|
+
outcome: "realtime_rejected",
|
|
486
|
+
code: observedRejection.error.code,
|
|
487
|
+
rejection: rejectionObservation(observedRejection)
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return withCalls({
|
|
491
|
+
outcome: "success",
|
|
492
|
+
...data !== undefined && { data }
|
|
493
|
+
});
|
|
494
|
+
} catch (error) {
|
|
495
|
+
const observedRejection = currentRejection();
|
|
496
|
+
if (observedRejection) {
|
|
497
|
+
return withCalls({
|
|
498
|
+
outcome: "realtime_rejected",
|
|
499
|
+
code: error instanceof RealtimeRequestInvalidAcknowledgementError ? error.code : observedRejection.error.code,
|
|
500
|
+
rejection: rejectionObservation(observedRejection)
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (error instanceof RealtimeRequestInvalidAcknowledgementError) {
|
|
504
|
+
return withCalls({
|
|
505
|
+
outcome: "realtime_rejected",
|
|
506
|
+
code: error.code,
|
|
507
|
+
rejection: {
|
|
508
|
+
direction: "client-inbound",
|
|
509
|
+
phase: "acknowledgement",
|
|
510
|
+
reason: "invalid-acknowledgement-value",
|
|
511
|
+
fault: "peer"
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (error instanceof RealtimeRequestDisconnectedError) {
|
|
516
|
+
return withCalls({
|
|
517
|
+
outcome: "disconnected",
|
|
518
|
+
code: error.code,
|
|
519
|
+
disconnect: {
|
|
520
|
+
phase: connectedBefore ? "in-flight" : "before-invoke"
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (error instanceof RealtimeRequestTimeoutError) {
|
|
525
|
+
return withCalls({ outcome: "timeout", code: error.code });
|
|
526
|
+
}
|
|
527
|
+
if (signal.aborted && error === signal.reason) {
|
|
528
|
+
return withCalls({ outcome: "aborted" });
|
|
529
|
+
}
|
|
530
|
+
const parsedRejection = RealtimeRejectionObservationSchema.safeParse(typeof error === "object" && error !== null && "details" in error ? error.details : undefined);
|
|
531
|
+
if (parsedRejection.success) {
|
|
532
|
+
const code2 = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "REALTIME_CONTRACT_VIOLATION";
|
|
533
|
+
return withCalls({
|
|
534
|
+
outcome: "realtime_rejected",
|
|
535
|
+
code: code2,
|
|
536
|
+
rejection: parsedRejection.data
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : undefined;
|
|
540
|
+
return withCalls({
|
|
541
|
+
outcome: "domain_error",
|
|
542
|
+
...code !== undefined && { code },
|
|
543
|
+
diagnostic: error instanceof Error ? error.message : String(error)
|
|
544
|
+
});
|
|
545
|
+
} finally {
|
|
546
|
+
await adapter.dispose?.();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
254
551
|
function sorted(values) {
|
|
255
552
|
return [...values].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
256
553
|
}
|
|
@@ -266,11 +563,12 @@ actual: ${actual.join(", ") || "(empty)"}`);
|
|
|
266
563
|
function openApiOperations(document) {
|
|
267
564
|
return Object.entries(document.paths).flatMap(([path, item]) => Object.keys(item).filter((method) => ["get", "post", "put", "patch", "delete", "head"].includes(method)).map((method) => `${method.toUpperCase()}:${path}`));
|
|
268
565
|
}
|
|
269
|
-
function expectedToolNames(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
566
|
+
function expectedToolNames(manifest, transport, surface) {
|
|
567
|
+
const projection = manifest.toolSurfaces.find((candidate) => candidate.transport === transport && candidate.surface === surface);
|
|
568
|
+
if (!projection) {
|
|
569
|
+
throw new Error(`${transport} surface "${surface ?? "(default)"}" is not declared in the manifest`);
|
|
570
|
+
}
|
|
571
|
+
return projection.tools.map((tool) => tool.name);
|
|
274
572
|
}
|
|
275
573
|
function assertSurfaceDiscovery(manifest, observed) {
|
|
276
574
|
if (observed.openApi) {
|
|
@@ -278,13 +576,31 @@ function assertSurfaceDiscovery(manifest, observed) {
|
|
|
278
576
|
}
|
|
279
577
|
for (const transport of ["MCP", "AGENT", "CLI"]) {
|
|
280
578
|
const names = observed[transport];
|
|
281
|
-
if (names)
|
|
282
|
-
assertSameSet(`${transport} discovery`, expectedToolNames(manifest
|
|
579
|
+
if (names) {
|
|
580
|
+
assertSameSet(`${transport} discovery`, expectedToolNames(manifest, transport, null), names);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
for (const projection of observed.toolSurfaces ?? []) {
|
|
584
|
+
assertSameSet(`${projection.transport} surface "${projection.surface ?? "(default)"}" discovery`, expectedToolNames(manifest, projection.transport, projection.surface ?? null), projection.names);
|
|
585
|
+
}
|
|
586
|
+
for (const [contract, topology] of Object.entries(observed.realtime ?? {})) {
|
|
587
|
+
if (!manifest.realtimeContracts.includes(contract)) {
|
|
588
|
+
throw new Error(`REALTIME contract "${contract}" is not declared in the manifest`);
|
|
589
|
+
}
|
|
590
|
+
for (const direction of ["serverToClient", "clientToServer"]) {
|
|
591
|
+
assertSameSet(`REALTIME ${contract}.${direction} discovery`, manifest.realtime.filter((entry) => entry.contract === contract && entry.direction === direction).map((entry) => entry.event), topology[direction]);
|
|
592
|
+
}
|
|
283
593
|
}
|
|
284
594
|
if (observed.cliOnly) {
|
|
285
595
|
assertSameSet("CLI-only discovery", manifest.cliOnly.map((command) => command.name), observed.cliOnly);
|
|
286
596
|
}
|
|
287
|
-
for (const transport of [
|
|
597
|
+
for (const transport of [
|
|
598
|
+
"HTTP",
|
|
599
|
+
"MCP",
|
|
600
|
+
"AGENT",
|
|
601
|
+
"CLI",
|
|
602
|
+
"REALTIME"
|
|
603
|
+
]) {
|
|
288
604
|
const names = observed.extensions?.[transport];
|
|
289
605
|
if (!names)
|
|
290
606
|
continue;
|
|
@@ -303,8 +619,13 @@ function boundedObservation(observation, maxDiagnosticBytes) {
|
|
|
303
619
|
diagnostic: new TextDecoder().decode(bytes.slice(0, maxDiagnosticBytes))
|
|
304
620
|
};
|
|
305
621
|
}
|
|
306
|
-
async function
|
|
307
|
-
signal.
|
|
622
|
+
async function runBounded(run, signal) {
|
|
623
|
+
if (signal.aborted) {
|
|
624
|
+
Promise.resolve().then(run).catch(() => {
|
|
625
|
+
return;
|
|
626
|
+
});
|
|
627
|
+
throw signal.reason;
|
|
628
|
+
}
|
|
308
629
|
let removeAbortListener = () => {
|
|
309
630
|
return;
|
|
310
631
|
};
|
|
@@ -314,7 +635,7 @@ async function invokeBounded(driver, fixture, signal) {
|
|
|
314
635
|
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
315
636
|
});
|
|
316
637
|
try {
|
|
317
|
-
return await Promise.race([
|
|
638
|
+
return await Promise.race([Promise.resolve().then(run), aborted]);
|
|
318
639
|
} finally {
|
|
319
640
|
removeAbortListener();
|
|
320
641
|
}
|
|
@@ -326,22 +647,28 @@ async function runSurfaceProbes({
|
|
|
326
647
|
timeoutMs = 1e4,
|
|
327
648
|
maxDiagnosticBytes = 4096
|
|
328
649
|
}) {
|
|
329
|
-
if (!Number.
|
|
330
|
-
throw new TypeError("Surface probe timeoutMs must be a
|
|
650
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
651
|
+
throw new TypeError("Surface probe timeoutMs must be a positive integer");
|
|
331
652
|
}
|
|
332
653
|
if (!Number.isInteger(maxDiagnosticBytes) || maxDiagnosticBytes <= 0) {
|
|
333
654
|
throw new TypeError("Surface probe maxDiagnosticBytes must be a positive integer");
|
|
334
655
|
}
|
|
335
656
|
for (const probe of probes) {
|
|
336
|
-
|
|
657
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
658
|
+
const probeSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
659
|
+
let failed = false;
|
|
660
|
+
let failure;
|
|
337
661
|
try {
|
|
662
|
+
probeSignal.throwIfAborted();
|
|
663
|
+
if (probe.setup)
|
|
664
|
+
await runBounded(() => probe.setup?.(probeSignal), probeSignal);
|
|
338
665
|
for (const transport of probe.transports) {
|
|
666
|
+
probeSignal.throwIfAborted();
|
|
339
667
|
const driver = drivers[transport];
|
|
340
|
-
if (!driver)
|
|
668
|
+
if (!driver) {
|
|
341
669
|
throw new Error(`Surface probe "${probe.name}" has no ${transport} driver`);
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const actual = boundedObservation(await invokeBounded(driver, probe.fixture, invokeSignal), maxDiagnosticBytes);
|
|
670
|
+
}
|
|
671
|
+
const actual = boundedObservation(await runBounded(() => driver.invoke(probe.fixture, probeSignal), probeSignal), maxDiagnosticBytes);
|
|
345
672
|
const expected = probe.expected[transport];
|
|
346
673
|
if (!expected) {
|
|
347
674
|
throw new Error(`Surface probe "${probe.name}" has no ${transport} expectation`);
|
|
@@ -352,9 +679,22 @@ expected: ${serializeSurfaceValue(expected)}
|
|
|
352
679
|
actual: ${serializeSurfaceValue(actual)}`);
|
|
353
680
|
}
|
|
354
681
|
}
|
|
355
|
-
}
|
|
356
|
-
|
|
682
|
+
} catch (error) {
|
|
683
|
+
failed = true;
|
|
684
|
+
failure = error;
|
|
685
|
+
}
|
|
686
|
+
if (probe.teardown) {
|
|
687
|
+
try {
|
|
688
|
+
await runBounded(() => probe.teardown?.(probeSignal), probeSignal);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (!failed) {
|
|
691
|
+
failed = true;
|
|
692
|
+
failure = error;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
357
695
|
}
|
|
696
|
+
if (failed)
|
|
697
|
+
throw failure;
|
|
358
698
|
}
|
|
359
699
|
}
|
|
360
700
|
|
|
@@ -384,9 +724,15 @@ function createHandlerTestClients(config) {
|
|
|
384
724
|
return createClients(config.contracts, createTestClientConfig(config), config.contractConfig);
|
|
385
725
|
}
|
|
386
726
|
export {
|
|
727
|
+
RealtimeDisconnectObservationSchema,
|
|
728
|
+
RealtimeRejectionObservationSchema,
|
|
387
729
|
SurfaceManifestExtensionSchema,
|
|
388
730
|
SurfaceManifestOperationSchema,
|
|
731
|
+
SurfaceManifestRealtimeEventSchema,
|
|
389
732
|
SurfaceManifestSchema,
|
|
733
|
+
SurfaceManifestToolSchema,
|
|
734
|
+
SurfaceManifestToolSurfaceSchema,
|
|
735
|
+
SurfaceRealtimeSchemaPairSchema,
|
|
390
736
|
SurfaceSchemaDigestsSchema,
|
|
391
737
|
TransportObservationSchema,
|
|
392
738
|
assertSurfaceDiscovery,
|
|
@@ -394,6 +740,8 @@ export {
|
|
|
394
740
|
buildSurfaceManifest,
|
|
395
741
|
createHandlerTestClient,
|
|
396
742
|
createHandlerTestClients,
|
|
743
|
+
createRealtimeProbeDriver,
|
|
744
|
+
defineRealtimeProbe,
|
|
397
745
|
runSurfaceProbes,
|
|
398
746
|
serializeSurfaceValue
|
|
399
747
|
};
|