@opengeni/api-router 0.22.2 → 0.26.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 (124) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +0 -30
  4. package/dist/browser-controller-authority.d.ts +43 -0
  5. package/dist/browser-state-authority.d.ts +27 -0
  6. package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
  7. package/dist/chunk-JIKNR5YL.js.map +1 -0
  8. package/dist/codemode.d.ts +23 -0
  9. package/dist/editable-artifact-live-hints.d.ts +11 -0
  10. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  11. package/dist/editable-artifact-office-import.d.ts +22 -0
  12. package/dist/editable-artifact-production.d.ts +29 -0
  13. package/dist/editable-artifact-websocket.d.ts +49 -0
  14. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  15. package/dist/github-browser-flow.d.ts +6 -0
  16. package/dist/http/cors.d.ts +1 -0
  17. package/dist/http/sse.d.ts +2 -0
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.js +1824 -30
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/api-integrations.d.ts +24 -0
  22. package/dist/integrations/atlassian.d.ts +176 -0
  23. package/dist/integrations/github-skill-source.d.ts +5 -0
  24. package/dist/integrations/google-drive.d.ts +85 -0
  25. package/dist/integrations/oauth-client.d.ts +30 -1
  26. package/dist/integrations/provider-oauth.d.ts +19 -0
  27. package/dist/integrations/slack-bot.d.ts +4 -0
  28. package/dist/integrations/slack-interactions.d.ts +14 -2
  29. package/dist/integrations/social-api.d.ts +2 -1
  30. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  31. package/dist/mcp/editable-artifacts.d.ts +13 -0
  32. package/dist/mcp/receipts.d.ts +28 -0
  33. package/dist/mcp/scheduled-task-view.d.ts +518 -0
  34. package/dist/mcp/server.d.ts +14 -3
  35. package/dist/memory-slack-delivery.d.ts +9 -0
  36. package/dist/routes/api-integrations.d.ts +8 -0
  37. package/dist/routes/browser-identities.d.ts +5 -0
  38. package/dist/routes/browser-sessions.d.ts +6 -0
  39. package/dist/routes/company-profile.d.ts +3 -0
  40. package/dist/routes/computer-sessions.d.ts +6 -0
  41. package/dist/routes/editable-artifacts.d.ts +44 -0
  42. package/dist/routes/integration-features.d.ts +3 -0
  43. package/dist/routes/memory-slack-publications.d.ts +6 -0
  44. package/dist/routes/plugins.d.ts +8 -0
  45. package/dist/routes/sessions.d.ts +17 -2
  46. package/dist/routes/skills.d.ts +6 -0
  47. package/dist/routes/video-generation.d.ts +3 -0
  48. package/dist/sandbox/auth-callout.d.ts +2 -0
  49. package/dist/sandbox/channel-a.d.ts +59 -2
  50. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  51. package/dist/sandbox/viewer.d.ts +4 -2
  52. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  53. package/package.json +19 -14
  54. package/src/app.ts +277 -41
  55. package/src/auth/managed-auth.ts +0 -16
  56. package/src/browser-controller-authority.ts +137 -0
  57. package/src/browser-state-authority.ts +236 -0
  58. package/src/codemode.ts +186 -0
  59. package/src/editable-artifact-live-hints.ts +64 -0
  60. package/src/editable-artifact-native-kernel.ts +659 -0
  61. package/src/editable-artifact-office-import.ts +230 -0
  62. package/src/editable-artifact-production.ts +419 -0
  63. package/src/editable-artifact-websocket.ts +311 -0
  64. package/src/editable-artifact-workspace-files.ts +186 -0
  65. package/src/github-browser-flow.ts +35 -6
  66. package/src/http/auth.ts +2 -0
  67. package/src/http/cors.ts +3 -0
  68. package/src/http/sse.ts +101 -6
  69. package/src/index.ts +147 -23
  70. package/src/integrations/api-integrations.ts +350 -0
  71. package/src/integrations/atlassian.ts +1621 -0
  72. package/src/integrations/github-skill-source.ts +142 -0
  73. package/src/integrations/google-drive.ts +1000 -64
  74. package/src/integrations/oauth-client.ts +159 -89
  75. package/src/integrations/provider-oauth.ts +777 -0
  76. package/src/integrations/slack-bot.ts +31 -2
  77. package/src/integrations/slack-interactions.ts +610 -42
  78. package/src/integrations/social-api.ts +11 -0
  79. package/src/mcp/documents.ts +74 -26
  80. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  81. package/src/mcp/editable-artifacts.ts +448 -0
  82. package/src/mcp/receipts.ts +95 -0
  83. package/src/mcp/scheduled-task-view.ts +642 -0
  84. package/src/mcp/server.ts +1718 -310
  85. package/src/memory-slack-delivery.ts +209 -0
  86. package/src/observability.ts +3 -3
  87. package/src/routes/api-integrations.ts +407 -0
  88. package/src/routes/api-keys.ts +7 -1
  89. package/src/routes/browser-identities.ts +136 -0
  90. package/src/routes/browser-sessions.ts +2543 -0
  91. package/src/routes/codex.ts +7 -4
  92. package/src/routes/company-profile.ts +255 -0
  93. package/src/routes/computer-sessions.ts +1247 -0
  94. package/src/routes/connections.ts +358 -102
  95. package/src/routes/documents.ts +22 -3
  96. package/src/routes/editable-artifacts.ts +1159 -0
  97. package/src/routes/enrollments.ts +54 -12
  98. package/src/routes/environments.ts +60 -11
  99. package/src/routes/files.ts +277 -65
  100. package/src/routes/github.ts +18 -2
  101. package/src/routes/install.ts +38 -2
  102. package/src/routes/integration-features.ts +258 -0
  103. package/src/routes/machines.ts +1 -1
  104. package/src/routes/memory-slack-publications.ts +216 -0
  105. package/src/routes/packs.ts +437 -7
  106. package/src/routes/plugins.ts +751 -0
  107. package/src/routes/rigs.ts +77 -20
  108. package/src/routes/scheduled-tasks.ts +94 -42
  109. package/src/routes/sessions.ts +475 -234
  110. package/src/routes/skills.ts +174 -0
  111. package/src/routes/transcription-recordings.ts +65 -33
  112. package/src/routes/video-generation.ts +132 -0
  113. package/src/routes/workspaces.ts +46 -24
  114. package/src/sandbox/auth-callout.ts +16 -4
  115. package/src/sandbox/channel-a.ts +809 -85
  116. package/src/sandbox/enrollment.ts +13 -3
  117. package/src/sandbox/machines.ts +1 -1
  118. package/src/sandbox/metrics-ingestion.ts +121 -3
  119. package/src/sandbox/rematerialize.ts +35 -47
  120. package/src/sandbox/viewer.ts +58 -29
  121. package/src/temporal-schedule-cleanup.ts +135 -0
  122. package/dist/chunk-HWXJW5C7.js.map +0 -1
  123. package/dist/mcp/toolspace.d.ts +0 -62
  124. package/src/mcp/toolspace.ts +0 -1186
@@ -0,0 +1,448 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import type { AccessGrant } from "@opengeni/contracts";
4
+ import type {
5
+ EditableArtifactAgentApplication,
6
+ EditableArtifactAgentCommandBatch,
7
+ EditableArtifactAgentContext,
8
+ EditableArtifactAgentQuery,
9
+ } from "@opengeni/core/editable-artifacts";
10
+ import {
11
+ editableArtifactClientTransactionId,
12
+ editableArtifactId,
13
+ } from "@opengeni/core/editable-artifacts";
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
16
+ import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js";
17
+ import * as z from "zod/v4";
18
+
19
+ import type { ApiRouteDeps } from "@opengeni/core";
20
+ import { editableArtifactActorForGrant } from "../routes/editable-artifacts";
21
+ import { EditableArtifactQueryRequestSchema } from "./editable-artifact-query-schema";
22
+
23
+ const ArtifactId = z
24
+ .string()
25
+ .regex(/^[0-9a-f]{32}$/u)
26
+ .refine((value) => value !== "0".repeat(32));
27
+ const StateHash = z.string().regex(/^sha256:[0-9a-f]{64}$/u);
28
+ const Modality = z.enum(["spreadsheet", "document", "presentation"]);
29
+ const Format = z.enum(["xlsx", "pptx", "docx", "pdf", "png", "webp"]);
30
+ const JsonRecord = z.record(z.string(), z.unknown());
31
+ const ArtifactCommand = z
32
+ .object({ kind: z.string().min(1).max(128) })
33
+ .passthrough()
34
+ .describe(
35
+ "One canonical modality command. Use the exact OpenGeni document, spreadsheet, or presentation skill command contract; never send local-file facade calls here.",
36
+ );
37
+
38
+ const ArtifactMetadata = z
39
+ .object({
40
+ id: ArtifactId,
41
+ modality: Modality,
42
+ title: z.string(),
43
+ lifecycle: z.enum(["active", "archived"]),
44
+ headSequence: z.number().int().nonnegative(),
45
+ stateHash: StateHash,
46
+ createdAt: z.string(),
47
+ updatedAt: z.string(),
48
+ })
49
+ .strict();
50
+ const MutationReceipt = z
51
+ .object({
52
+ artifact: ArtifactMetadata,
53
+ transaction: z
54
+ .object({
55
+ id: ArtifactId,
56
+ clientTransactionId: z.string(),
57
+ sequenceStart: z.number().int().positive(),
58
+ sequenceEnd: z.number().int().positive(),
59
+ stateHash: StateHash,
60
+ committedAt: z.string(),
61
+ replayed: z.boolean(),
62
+ })
63
+ .strict(),
64
+ })
65
+ .strict();
66
+ const ExportFile = z
67
+ .object({
68
+ fileId: z.string().uuid(),
69
+ filename: z.string(),
70
+ contentType: z.string(),
71
+ sizeBytes: z.number().int().positive(),
72
+ sha256: z.string().regex(/^[0-9a-f]{64}$/u),
73
+ artifactId: ArtifactId,
74
+ versionId: ArtifactId,
75
+ materializationJobId: ArtifactId,
76
+ sourceHeadSequence: z.number().int().nonnegative(),
77
+ sourceStateHash: StateHash,
78
+ })
79
+ .strict();
80
+
81
+ type ToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
82
+ type RegisterEditableArtifactToolsInput = Readonly<{
83
+ server: McpServer;
84
+ deps: ApiRouteDeps;
85
+ grant: AccessGrant;
86
+ sessionId: string;
87
+ authorize(): Promise<void>;
88
+ }>;
89
+
90
+ /** Register the one canonical agent artifact surface used directly and by CodeMode. */
91
+ export function registerEditableArtifactAgentTools(
92
+ input: RegisterEditableArtifactToolsInput,
93
+ ): void {
94
+ const application = () => requireApplication(input.deps);
95
+ const context = (toolName: string, extra: ToolExtra) =>
96
+ artifactAttemptContext(input.grant, input.sessionId, toolName, extra);
97
+ const execute = async <T>(
98
+ operation: () => Promise<T>,
99
+ ): Promise<ReturnType<typeof jsonResult>> => {
100
+ await input.authorize();
101
+ return jsonResult(await operation());
102
+ };
103
+
104
+ input.server.registerTool(
105
+ "editable_artifact_list",
106
+ {
107
+ title: "List session artifacts",
108
+ description:
109
+ "List the durable editable workbooks, documents, and presentations used in this session. These are the same live artifacts visible to the user.",
110
+ inputSchema: { limit: z.number().int().min(1).max(64).optional() },
111
+ outputSchema: { artifacts: z.array(ArtifactMetadata) },
112
+ annotations: readOnlyAnnotations("List session artifacts"),
113
+ },
114
+ async ({ limit }, extra) =>
115
+ await execute(async () => ({
116
+ artifacts: await application().list({
117
+ ...context("editable_artifact_list", extra),
118
+ ...(limit === undefined ? {} : { limit }),
119
+ }),
120
+ })),
121
+ );
122
+
123
+ input.server.registerTool(
124
+ "editable_artifact_create",
125
+ {
126
+ title: "Create editable artifact",
127
+ description:
128
+ "Create an empty durable workbook, document, or presentation and associate it with this session. Continue editing it with opengeni__editable_artifact_apply; do not create a shadow Office file.",
129
+ inputSchema: {
130
+ modality: Modality,
131
+ title: z.string().trim().min(1).max(512),
132
+ },
133
+ outputSchema: ArtifactMetadata,
134
+ annotations: mutationAnnotations("Create editable artifact", { idempotent: true }),
135
+ },
136
+ async ({ modality, title }, extra) => {
137
+ const invocation = context("editable_artifact_create", extra);
138
+ return await execute(
139
+ async () =>
140
+ await application().create({
141
+ ...invocation,
142
+ idempotencyKey: editableArtifactClientTransactionId(invocation.operationKey),
143
+ modality,
144
+ title,
145
+ signal: extra.signal,
146
+ }),
147
+ );
148
+ },
149
+ );
150
+
151
+ input.server.registerTool(
152
+ "editable_artifact_import",
153
+ {
154
+ title: "Import Office file",
155
+ description:
156
+ "Import one ready workspace DOCX, XLSX, or PPTX file into a new durable editable artifact. The imported artifact becomes the shared working state; the source file remains immutable provenance.",
157
+ inputSchema: {
158
+ fileId: z.string().uuid(),
159
+ modality: Modality,
160
+ title: z.string().trim().min(1).max(512),
161
+ },
162
+ outputSchema: ArtifactMetadata,
163
+ annotations: mutationAnnotations("Import Office file", { idempotent: true }),
164
+ },
165
+ async ({ fileId, modality, title }, extra) => {
166
+ const invocation = context("editable_artifact_import", extra);
167
+ return await execute(
168
+ async () =>
169
+ await application().import({
170
+ ...invocation,
171
+ idempotencyKey: editableArtifactClientTransactionId(invocation.operationKey),
172
+ fileId,
173
+ modality,
174
+ title,
175
+ signal: extra.signal,
176
+ }),
177
+ );
178
+ },
179
+ );
180
+
181
+ input.server.registerTool(
182
+ "editable_artifact_get",
183
+ {
184
+ title: "Get editable artifact",
185
+ description:
186
+ "Read current durable metadata for one editable artifact and associate it with this session.",
187
+ inputSchema: { artifactId: ArtifactId },
188
+ outputSchema: ArtifactMetadata,
189
+ annotations: readOnlyAnnotations("Get editable artifact"),
190
+ },
191
+ async ({ artifactId }, extra) =>
192
+ await execute(
193
+ async () =>
194
+ await application().get({
195
+ ...context("editable_artifact_get", extra),
196
+ artifactId: editableArtifactId(artifactId),
197
+ }),
198
+ ),
199
+ );
200
+
201
+ input.server.registerTool(
202
+ "editable_artifact_inspect",
203
+ {
204
+ title: "Inspect editable artifact",
205
+ description:
206
+ "Query the artifact's current canonical head through the same native kernel used by the browser. Use bounded modality queries; results are current, not a sandbox-file snapshot.",
207
+ inputSchema: {
208
+ artifactId: ArtifactId,
209
+ modality: Modality,
210
+ request: EditableArtifactQueryRequestSchema,
211
+ },
212
+ outputSchema: { artifact: ArtifactMetadata, projection: z.unknown() },
213
+ annotations: readOnlyAnnotations("Inspect editable artifact"),
214
+ },
215
+ async ({ artifactId, modality, request }, extra) =>
216
+ await execute(
217
+ async () =>
218
+ await application().inspect({
219
+ ...context("editable_artifact_inspect", extra),
220
+ artifactId: editableArtifactId(artifactId),
221
+ request: { modality, query: request } as EditableArtifactAgentQuery,
222
+ }),
223
+ ),
224
+ );
225
+
226
+ input.server.registerTool(
227
+ "editable_artifact_apply",
228
+ {
229
+ title: "Edit editable artifact",
230
+ description:
231
+ "Atomically apply a bounded modality command batch to the current durable artifact head. Inspect first, use stable object IDs from that projection, and verify the result afterward.",
232
+ inputSchema: {
233
+ artifactId: ArtifactId,
234
+ modality: Modality,
235
+ expectedHeadSequence: z.number().int().nonnegative(),
236
+ expectedStateHash: StateHash,
237
+ commands: z.array(ArtifactCommand).min(1).max(4_096),
238
+ },
239
+ outputSchema: MutationReceipt,
240
+ annotations: mutationAnnotations("Edit editable artifact", {
241
+ destructive: true,
242
+ idempotent: true,
243
+ }),
244
+ },
245
+ async ({ artifactId, modality, expectedHeadSequence, expectedStateHash, commands }, extra) => {
246
+ const invocation = context("editable_artifact_apply", extra);
247
+ return await execute(
248
+ async () =>
249
+ await application().apply({
250
+ ...invocation,
251
+ artifactId: editableArtifactId(artifactId),
252
+ clientTransactionId: editableArtifactClientTransactionId(invocation.operationKey),
253
+ expectedHeadSequence,
254
+ expectedStateHash,
255
+ batch: { modality, commands } as EditableArtifactAgentCommandBatch,
256
+ }),
257
+ );
258
+ },
259
+ );
260
+
261
+ input.server.registerTool(
262
+ "editable_artifact_export",
263
+ {
264
+ title: "Export editable artifact",
265
+ description:
266
+ "Pin the current artifact head and start an immutable Office/PDF/image export. This does not write into the sandbox. Poll opengeni__editable_artifact_export_status for the resulting workspace file ID.",
267
+ inputSchema: {
268
+ artifactId: ArtifactId,
269
+ format: Format,
270
+ options: JsonRecord.optional(),
271
+ },
272
+ outputSchema: {
273
+ artifact: ArtifactMetadata,
274
+ versionId: ArtifactId,
275
+ jobId: ArtifactId,
276
+ sourceHeadSequence: z.number().int().nonnegative(),
277
+ sourceStateHash: StateHash,
278
+ state: z.enum(["pending", "running", "succeeded", "failed"]),
279
+ },
280
+ annotations: mutationAnnotations("Export editable artifact", { idempotent: true }),
281
+ },
282
+ async ({ artifactId, format, options }, extra) => {
283
+ const invocation = context("editable_artifact_export", extra);
284
+ return await execute(
285
+ async () =>
286
+ await application().startExport({
287
+ ...invocation,
288
+ artifactId: editableArtifactId(artifactId),
289
+ idempotencyKey: invocation.operationKey,
290
+ format,
291
+ ...(options ? { options } : {}),
292
+ signal: extra.signal,
293
+ }),
294
+ );
295
+ },
296
+ );
297
+
298
+ input.server.registerTool(
299
+ "editable_artifact_export_status",
300
+ {
301
+ title: "Get artifact export",
302
+ description:
303
+ "Read export status. When complete, atomically promote the immutable result into the workspace file domain and return its file ID; download it only when sandbox-local bytes are actually needed.",
304
+ inputSchema: {
305
+ artifactId: ArtifactId,
306
+ versionId: ArtifactId,
307
+ jobId: ArtifactId,
308
+ },
309
+ outputSchema: {
310
+ artifact: ArtifactMetadata,
311
+ versionId: ArtifactId,
312
+ jobId: ArtifactId,
313
+ sourceHeadSequence: z.number().int().nonnegative(),
314
+ sourceStateHash: StateHash,
315
+ state: z.enum(["pending", "running", "succeeded", "failed"]),
316
+ errorCode: z.string().nullable(),
317
+ file: ExportFile.nullable(),
318
+ },
319
+ annotations: mutationAnnotations("Get artifact export", { idempotent: true }),
320
+ },
321
+ async ({ artifactId, versionId, jobId }, extra) =>
322
+ await execute(
323
+ async () =>
324
+ await application().exportStatus({
325
+ ...context("editable_artifact_export_status", extra),
326
+ artifactId: editableArtifactId(artifactId),
327
+ versionId,
328
+ jobId,
329
+ signal: extra.signal,
330
+ }),
331
+ ),
332
+ );
333
+ }
334
+
335
+ function artifactAttemptContext(
336
+ grant: AccessGrant,
337
+ sessionId: string,
338
+ toolName: string,
339
+ extra: ToolExtra,
340
+ ): EditableArtifactAgentContext & Readonly<{ operationKey: string }> {
341
+ const operationId = trustedOperationId(extra);
342
+ const digest = createHash("sha256")
343
+ .update("opengeni:editable-artifact-agent-call:v1\0", "utf8")
344
+ .update(grant.accountId, "utf8")
345
+ .update("\0", "utf8")
346
+ .update(grant.workspaceId, "utf8")
347
+ .update("\0", "utf8")
348
+ .update(sessionId, "utf8")
349
+ .update("\0", "utf8")
350
+ .update(String(grant.metadata?.["turnId"] ?? ""), "utf8")
351
+ .update("\0", "utf8")
352
+ .update(String(grant.metadata?.["attemptId"] ?? ""), "utf8")
353
+ .update("\0", "utf8")
354
+ .update(String(grant.metadata?.["executionGeneration"] ?? ""), "utf8")
355
+ .update("\0", "utf8")
356
+ .update(toolName, "utf8")
357
+ .update("\0", "utf8")
358
+ .update(operationId, "utf8")
359
+ .digest("hex");
360
+ const replicaId =
361
+ digest.slice(0, 16) === "0".repeat(16) ? `1${digest.slice(1, 16)}` : digest.slice(0, 16);
362
+ const actor = editableArtifactActorForGrant(grant, replicaId);
363
+ if (actor.kind !== "agent" || actor.sessionId !== sessionId) {
364
+ throw new Error("Editable artifact tools require exact signed attempt authority");
365
+ }
366
+ return Object.freeze({
367
+ scope: Object.freeze({ accountId: grant.accountId, workspaceId: grant.workspaceId }),
368
+ actor,
369
+ sessionId,
370
+ operationKey: `artifact:${toolName}:${digest}`,
371
+ });
372
+ }
373
+
374
+ function trustedOperationId(extra: ToolExtra): string {
375
+ const value = extra._meta?.["opengeniOperationId"];
376
+ if (typeof value === "string" && /^[A-Za-z0-9._:-]{1,200}$/u.test(value)) return value;
377
+ const requestId = String(extra.requestId);
378
+ if (requestId.length < 1 || requestId.length > 256) {
379
+ throw new Error("Editable artifact tool request identity is invalid");
380
+ }
381
+ return requestId;
382
+ }
383
+
384
+ function requireApplication(deps: ApiRouteDeps): EditableArtifactAgentApplication {
385
+ if (!deps.editableArtifactAgent) {
386
+ throw new Error("Editable artifact agent service is unavailable");
387
+ }
388
+ return deps.editableArtifactAgent;
389
+ }
390
+
391
+ function jsonResult(value: unknown) {
392
+ const structuredContent = jsonValue(value);
393
+ if (!isRecord(structuredContent)) {
394
+ throw new Error("Editable artifact tool result must be an object");
395
+ }
396
+ return {
397
+ content: [{ type: "text" as const, text: JSON.stringify(structuredContent) }],
398
+ structuredContent,
399
+ };
400
+ }
401
+
402
+ function jsonValue(value: unknown): unknown {
403
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
404
+ if (typeof value === "number") {
405
+ if (!Number.isFinite(value)) throw new Error("Artifact result contains a non-finite number");
406
+ return value;
407
+ }
408
+ if (typeof value === "bigint") return value.toString(10);
409
+ if (value instanceof Uint8Array) {
410
+ return Object.freeze({ encoding: "base64", data: Buffer.from(value).toString("base64") });
411
+ }
412
+ if (Array.isArray(value)) return value.map(jsonValue);
413
+ if (isRecord(value)) {
414
+ const output: Record<string, unknown> = {};
415
+ for (const [key, entry] of Object.entries(value)) {
416
+ if (entry !== undefined) output[key] = jsonValue(entry);
417
+ }
418
+ return output;
419
+ }
420
+ throw new Error("Artifact result contains unsupported data");
421
+ }
422
+
423
+ function isRecord(value: unknown): value is Record<string, unknown> {
424
+ return typeof value === "object" && value !== null && !Array.isArray(value);
425
+ }
426
+
427
+ function readOnlyAnnotations(title: string) {
428
+ return {
429
+ title,
430
+ readOnlyHint: true,
431
+ destructiveHint: false,
432
+ idempotentHint: true,
433
+ openWorldHint: false,
434
+ } as const;
435
+ }
436
+
437
+ function mutationAnnotations(
438
+ title: string,
439
+ options: Readonly<{ destructive?: boolean; idempotent: boolean }>,
440
+ ) {
441
+ return {
442
+ title,
443
+ readOnlyHint: false,
444
+ destructiveHint: options.destructive === true,
445
+ idempotentHint: options.idempotent,
446
+ openWorldHint: false,
447
+ } as const;
448
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ MCP_MUTATION_RECEIPT_VERSION,
3
+ McpMutationReceipt,
4
+ type McpMutationReceiptType,
5
+ } from "@opengeni/contracts";
6
+
7
+ export type McpMutationReceiptInput = Omit<
8
+ McpMutationReceiptType,
9
+ "receiptVersion" | "timestamp" | "warnings"
10
+ > & {
11
+ timestamp?: string;
12
+ warnings?: string[];
13
+ };
14
+
15
+ /**
16
+ * Build and validate a compact first-party MCP mutation receipt at the API
17
+ * boundary. Contract parsing is intentional: it prevents a handler from
18
+ * accidentally adding an unbounded entity or a copy of request fields.
19
+ */
20
+ export function mcpMutationReceipt(input: McpMutationReceiptInput): McpMutationReceiptType {
21
+ return McpMutationReceipt.parse({
22
+ receiptVersion: MCP_MUTATION_RECEIPT_VERSION,
23
+ ...input,
24
+ timestamp: input.timestamp ?? new Date().toISOString(),
25
+ warnings: input.warnings ?? [],
26
+ });
27
+ }
28
+
29
+ export type SessionCreateReceiptResult = {
30
+ session: {
31
+ id: string;
32
+ queueVersion: number;
33
+ status: string;
34
+ sandboxGroupId: string;
35
+ parentSessionId: string | null;
36
+ rootSessionId: string;
37
+ nestedAgentDepth: number;
38
+ effectiveMaxNestedAgentDepth: number;
39
+ };
40
+ outcome: "created" | "repaired" | "replayed";
41
+ changed: boolean;
42
+ usageRecording: "recorded" | "failed";
43
+ };
44
+
45
+ /** Project committed session-create truth without copying request fields. */
46
+ export function sessionCreateMutationReceipt(
47
+ result: SessionCreateReceiptResult,
48
+ idempotencyKeyRequested: boolean,
49
+ ): McpMutationReceiptType {
50
+ const usageRecordingFailed = result.usageRecording === "failed";
51
+ const retryable = usageRecordingFailed && idempotencyKeyRequested;
52
+ return mcpMutationReceipt({
53
+ operation: "session_create",
54
+ committed: true,
55
+ outcome: usageRecordingFailed ? "partial_failure" : result.outcome,
56
+ changed: result.changed,
57
+ resource: {
58
+ type: "session",
59
+ id: result.session.id,
60
+ version: result.session.queueVersion,
61
+ state: result.session.status,
62
+ },
63
+ idempotency: {
64
+ status:
65
+ result.outcome === "replayed"
66
+ ? "replayed"
67
+ : idempotencyKeyRequested
68
+ ? "applied"
69
+ : "not_requested",
70
+ },
71
+ ...(usageRecordingFailed
72
+ ? {
73
+ partialFailure: { stage: "usage_recording", retryable },
74
+ warnings: [
75
+ retryable
76
+ ? "The session committed, but usage recording failed. Retry only with the same idempotency key."
77
+ : "The session committed, but usage recording failed. Do not retry this keyless request; inspect the returned session.",
78
+ ],
79
+ }
80
+ : {}),
81
+ facts: {
82
+ sandboxGroupId: result.session.sandboxGroupId,
83
+ parentSessionId: result.session.parentSessionId,
84
+ sessionCreateOutcome: result.outcome,
85
+ },
86
+ id: result.session.id,
87
+ rootSessionId: result.session.rootSessionId,
88
+ nestedAgentDepth: result.session.nestedAgentDepth,
89
+ effectiveMaxNestedAgentDepth: result.session.effectiveMaxNestedAgentDepth,
90
+ nextAction: {
91
+ tool: "session_get",
92
+ arguments: { sessionId: result.session.id },
93
+ },
94
+ });
95
+ }