@opengeni/api-router 0.23.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/app.d.ts +3 -2
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +61 -0
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/browser-state-authority.d.ts +27 -0
  8. package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
  9. package/dist/chunk-2PQMSRCB.js.map +1 -0
  10. package/dist/codemode.d.ts +23 -0
  11. package/dist/controller-data-plane.d.ts +12 -0
  12. package/dist/editable-artifact-live-hints.d.ts +11 -0
  13. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  14. package/dist/editable-artifact-office-import.d.ts +22 -0
  15. package/dist/editable-artifact-production.d.ts +29 -0
  16. package/dist/editable-artifact-websocket.d.ts +49 -0
  17. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  18. package/dist/github-browser-flow.d.ts +6 -0
  19. package/dist/http/auth.d.ts +1 -1
  20. package/dist/http/cors.d.ts +1 -0
  21. package/dist/http/sse.d.ts +18 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1796 -22
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/api-integrations.d.ts +24 -0
  26. package/dist/integrations/atlassian.d.ts +176 -0
  27. package/dist/integrations/fiken.d.ts +233 -0
  28. package/dist/integrations/github-skill-source.d.ts +5 -0
  29. package/dist/integrations/google-drive.d.ts +85 -0
  30. package/dist/integrations/oauth-client.d.ts +22 -1
  31. package/dist/integrations/provider-oauth.d.ts +17 -0
  32. package/dist/integrations/slack-bot.d.ts +175 -1
  33. package/dist/integrations/slack-interactions.d.ts +24 -6
  34. package/dist/integrations/social-api.d.ts +2 -1
  35. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  36. package/dist/interaction-metrics.d.ts +11 -0
  37. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  38. package/dist/mcp/editable-artifacts.d.ts +13 -0
  39. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  40. package/dist/mcp/server.d.ts +16 -4
  41. package/dist/memory-slack-delivery.d.ts +9 -0
  42. package/dist/model-catalog.d.ts +1 -0
  43. package/dist/routes/api-integrations.d.ts +8 -0
  44. package/dist/routes/browser-identities.d.ts +5 -0
  45. package/dist/routes/browser-sessions.d.ts +6 -0
  46. package/dist/routes/canonical-human-identities.d.ts +3 -0
  47. package/dist/routes/channels.d.ts +3 -0
  48. package/dist/routes/company-profile.d.ts +3 -0
  49. package/dist/routes/computer-sessions.d.ts +6 -0
  50. package/dist/routes/editable-artifacts.d.ts +44 -0
  51. package/dist/routes/integration-facets.d.ts +3 -0
  52. package/dist/routes/interaction-resources.d.ts +5 -0
  53. package/dist/routes/memory-slack-publications.d.ts +6 -0
  54. package/dist/routes/organization-memberships.d.ts +3 -0
  55. package/dist/routes/plugins.d.ts +8 -0
  56. package/dist/routes/sessions.d.ts +18 -2
  57. package/dist/routes/skills.d.ts +6 -0
  58. package/dist/routes/slack-task-policy.d.ts +3 -0
  59. package/dist/routes/supergrok.d.ts +3 -0
  60. package/dist/routes/video-generation.d.ts +3 -0
  61. package/dist/sandbox/channel-a.d.ts +62 -2
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  64. package/dist/sandbox/viewer.d.ts +7 -3
  65. package/dist/slack-reaction-files.d.ts +27 -0
  66. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  67. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  68. package/dist/xai-realtime.d.ts +26 -0
  69. package/dist/xai-subscription-auth.d.ts +23 -0
  70. package/package.json +20 -14
  71. package/src/app.ts +604 -64
  72. package/src/auth/canonical-human-session-admission.ts +116 -0
  73. package/src/auth/managed-auth.ts +101 -0
  74. package/src/browser-auth-broker.ts +155 -0
  75. package/src/browser-controller-authority.ts +186 -0
  76. package/src/browser-network-route.ts +34 -0
  77. package/src/browser-state-authority.ts +236 -0
  78. package/src/codemode.ts +186 -0
  79. package/src/controller-data-plane.ts +35 -0
  80. package/src/editable-artifact-live-hints.ts +64 -0
  81. package/src/editable-artifact-native-kernel.ts +659 -0
  82. package/src/editable-artifact-office-import.ts +230 -0
  83. package/src/editable-artifact-production.ts +419 -0
  84. package/src/editable-artifact-websocket.ts +311 -0
  85. package/src/editable-artifact-workspace-files.ts +186 -0
  86. package/src/github-browser-flow.ts +35 -6
  87. package/src/http/auth.ts +29 -9
  88. package/src/http/cors.ts +3 -0
  89. package/src/http/sse.ts +229 -5
  90. package/src/index.ts +119 -20
  91. package/src/integrations/api-integrations.ts +365 -0
  92. package/src/integrations/atlassian.ts +1621 -0
  93. package/src/integrations/fiken.ts +1230 -0
  94. package/src/integrations/github-skill-source.ts +142 -0
  95. package/src/integrations/google-drive.ts +1005 -64
  96. package/src/integrations/oauth-client.ts +123 -33
  97. package/src/integrations/provider-oauth.ts +756 -0
  98. package/src/integrations/slack-bot.ts +669 -11
  99. package/src/integrations/slack-interactions.ts +2063 -93
  100. package/src/integrations/social-api.ts +11 -0
  101. package/src/interaction-holder-heartbeat.ts +95 -0
  102. package/src/interaction-metrics.ts +159 -0
  103. package/src/mcp/documents.ts +146 -6
  104. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  105. package/src/mcp/editable-artifacts.ts +448 -0
  106. package/src/mcp/scheduled-task-view.ts +34 -0
  107. package/src/mcp/server.ts +1756 -355
  108. package/src/memory-slack-delivery.ts +209 -0
  109. package/src/model-catalog.ts +19 -6
  110. package/src/routes/api-integrations.ts +410 -0
  111. package/src/routes/browser-identities.ts +160 -0
  112. package/src/routes/browser-sessions.ts +4482 -0
  113. package/src/routes/canonical-human-identities.ts +156 -0
  114. package/src/routes/channels.ts +90 -0
  115. package/src/routes/company-profile.ts +255 -0
  116. package/src/routes/computer-sessions.ts +1602 -0
  117. package/src/routes/connections.ts +421 -99
  118. package/src/routes/documents.ts +22 -3
  119. package/src/routes/editable-artifacts.ts +1159 -0
  120. package/src/routes/files.ts +106 -4
  121. package/src/routes/github.ts +18 -2
  122. package/src/routes/install.ts +7 -1
  123. package/src/routes/integration-facets.ts +258 -0
  124. package/src/routes/interaction-resources.ts +595 -0
  125. package/src/routes/memory-slack-publications.ts +216 -0
  126. package/src/routes/organization-memberships.ts +53 -0
  127. package/src/routes/packs.ts +437 -7
  128. package/src/routes/plugins.ts +752 -0
  129. package/src/routes/rigs.ts +77 -20
  130. package/src/routes/scheduled-tasks.ts +67 -40
  131. package/src/routes/sessions.ts +830 -278
  132. package/src/routes/skills.ts +256 -0
  133. package/src/routes/slack-task-policy.ts +115 -0
  134. package/src/routes/supergrok.ts +717 -0
  135. package/src/routes/transcription-recordings.ts +9 -2
  136. package/src/routes/transcriptions.ts +2 -1
  137. package/src/routes/video-generation.ts +162 -0
  138. package/src/routes/workspaces.ts +68 -28
  139. package/src/sandbox/channel-a.ts +750 -116
  140. package/src/sandbox/machines.ts +5 -5
  141. package/src/sandbox/metrics-ingestion.ts +121 -3
  142. package/src/sandbox/rematerialize.ts +35 -47
  143. package/src/sandbox/viewer.ts +39 -12
  144. package/src/slack-reaction-files.ts +181 -0
  145. package/src/temporal-schedule-cleanup.ts +135 -0
  146. package/src/transcription/providers/xai-subscription.ts +110 -0
  147. package/src/transcription/service.ts +17 -2
  148. package/src/xai-realtime.ts +216 -0
  149. package/src/xai-subscription-auth.ts +132 -0
  150. package/dist/chunk-T4T2PGU4.js.map +0 -1
  151. package/dist/mcp/toolspace.d.ts +0 -71
  152. package/src/mcp/toolspace.ts +0 -1190
@@ -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
+ }
@@ -24,6 +24,14 @@ type Utf8Projection = {
24
24
  export type ScheduledTaskUtf8ProjectionFact = Omit<Utf8Projection, "value">;
25
25
 
26
26
  type ScheduledTaskScheduleProjection =
27
+ | {
28
+ schedule: Extract<ScheduledTaskScheduleSpec, { type: "manual" }>;
29
+ projection: {
30
+ type: "manual";
31
+ truncated: false;
32
+ fields: Record<string, never>;
33
+ };
34
+ }
27
35
  | {
28
36
  schedule: Extract<ScheduledTaskScheduleSpec, { type: "once" }>;
29
37
  projection: {
@@ -171,6 +179,11 @@ export function projectScheduledTaskSchedule(
171
179
  schedule: ScheduledTaskScheduleSpec,
172
180
  ): ScheduledTaskScheduleProjection {
173
181
  switch (schedule.type) {
182
+ case "manual":
183
+ return {
184
+ schedule: { type: "manual" },
185
+ projection: { type: "manual", truncated: false, fields: {} },
186
+ };
174
187
  case "once": {
175
188
  const runAt = projectScheduledTaskUtf8(
176
189
  schedule.runAt,
@@ -294,6 +307,7 @@ function toolIdentity(
294
307
  }
295
308
 
296
309
  export function scheduledTaskMcpSummary(task: ScheduledTask) {
310
+ const action = task.action ?? ({ kind: "agent_turn" } as const);
297
311
  const name = projectScheduledTaskUtf8(task.name, SCHEDULED_TASK_NAME_MAX_BYTES);
298
312
  const schedule = projectScheduledTaskSchedule(task.schedule);
299
313
  const model = task.agentConfig.model
@@ -308,6 +322,26 @@ export function scheduledTaskMcpSummary(task: ScheduledTask) {
308
322
  schedule: schedule.schedule,
309
323
  runMode: task.runMode,
310
324
  overlapPolicy: task.overlapPolicy,
325
+ action:
326
+ action.kind === "agent_turn"
327
+ ? { kind: "agent_turn" as const }
328
+ : {
329
+ kind: action.kind,
330
+ sourceId: action.sourceId,
331
+ sourceGeneration: action.sourceGeneration,
332
+ sourceLifecycleGeneration: action.sourceLifecycleGeneration,
333
+ destination: action.destination,
334
+ initiatingSubjectId: action.initiatingSubjectId,
335
+ allDescendants: action.allDescendants,
336
+ connection: {
337
+ connectionId: action.connection.connectionId,
338
+ connectionVersion: action.connection.connectionVersion,
339
+ providerDomain: action.connection.providerDomain,
340
+ kind: action.connection.kind,
341
+ ownerSubjectId: action.connection.ownerSubjectId,
342
+ },
343
+ limits: action.limits,
344
+ },
311
345
  targetSessionId: task.targetSessionId,
312
346
  reusableSessionId: task.reusableSessionId,
313
347
  variableSetId: task.variableSetId,