@opengeni/api-router 0.26.1 → 0.30.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 (87) hide show
  1. package/dist/app.d.ts +1 -0
  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 +19 -1
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/{chunk-JIKNR5YL.js → chunk-2PQMSRCB.js} +15518 -6863
  8. package/dist/chunk-2PQMSRCB.js.map +1 -0
  9. package/dist/controller-data-plane.d.ts +12 -0
  10. package/dist/http/auth.d.ts +1 -1
  11. package/dist/http/sse.d.ts +18 -1
  12. package/dist/index.js +1 -1
  13. package/dist/integrations/api-integrations.d.ts +2 -2
  14. package/dist/integrations/fiken.d.ts +233 -0
  15. package/dist/integrations/google-drive.d.ts +6 -6
  16. package/dist/integrations/provider-oauth.d.ts +0 -2
  17. package/dist/integrations/slack-bot.d.ts +171 -1
  18. package/dist/integrations/slack-interactions.d.ts +17 -5
  19. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  20. package/dist/interaction-metrics.d.ts +11 -0
  21. package/dist/mcp/server.d.ts +3 -2
  22. package/dist/model-catalog.d.ts +1 -0
  23. package/dist/routes/canonical-human-identities.d.ts +3 -0
  24. package/dist/routes/channels.d.ts +3 -0
  25. package/dist/routes/integration-facets.d.ts +3 -0
  26. package/dist/routes/interaction-resources.d.ts +5 -0
  27. package/dist/routes/organization-memberships.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +2 -1
  29. package/dist/routes/slack-task-policy.d.ts +3 -0
  30. package/dist/routes/supergrok.d.ts +3 -0
  31. package/dist/sandbox/channel-a.d.ts +8 -1
  32. package/dist/sandbox/machines.d.ts +2 -2
  33. package/dist/sandbox/viewer.d.ts +3 -1
  34. package/dist/slack-reaction-files.d.ts +27 -0
  35. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  36. package/dist/xai-realtime.d.ts +26 -0
  37. package/dist/xai-subscription-auth.d.ts +23 -0
  38. package/package.json +17 -16
  39. package/src/app.ts +352 -49
  40. package/src/auth/canonical-human-session-admission.ts +116 -0
  41. package/src/auth/managed-auth.ts +101 -0
  42. package/src/browser-auth-broker.ts +155 -0
  43. package/src/browser-controller-authority.ts +50 -1
  44. package/src/browser-network-route.ts +34 -0
  45. package/src/controller-data-plane.ts +35 -0
  46. package/src/http/auth.ts +27 -9
  47. package/src/http/sse.ts +229 -5
  48. package/src/integrations/api-integrations.ts +57 -42
  49. package/src/integrations/fiken.ts +1230 -0
  50. package/src/integrations/google-drive.ts +31 -26
  51. package/src/integrations/provider-oauth.ts +70 -91
  52. package/src/integrations/slack-bot.ts +638 -9
  53. package/src/integrations/slack-interactions.ts +1583 -73
  54. package/src/interaction-holder-heartbeat.ts +95 -0
  55. package/src/interaction-metrics.ts +159 -0
  56. package/src/mcp/documents.ts +115 -6
  57. package/src/mcp/server.ts +865 -242
  58. package/src/model-catalog.ts +19 -6
  59. package/src/routes/api-integrations.ts +32 -29
  60. package/src/routes/browser-identities.ts +24 -0
  61. package/src/routes/browser-sessions.ts +2122 -183
  62. package/src/routes/canonical-human-identities.ts +156 -0
  63. package/src/routes/channels.ts +90 -0
  64. package/src/routes/computer-sessions.ts +441 -86
  65. package/src/routes/connections.ts +141 -4
  66. package/src/routes/{integration-features.ts → integration-facets.ts} +63 -63
  67. package/src/routes/interaction-resources.ts +595 -0
  68. package/src/routes/organization-memberships.ts +53 -0
  69. package/src/routes/plugins.ts +2 -1
  70. package/src/routes/sessions.ts +419 -43
  71. package/src/routes/skills.ts +92 -10
  72. package/src/routes/slack-task-policy.ts +115 -0
  73. package/src/routes/supergrok.ts +717 -0
  74. package/src/routes/transcription-recordings.ts +9 -2
  75. package/src/routes/transcriptions.ts +2 -1
  76. package/src/routes/video-generation.ts +34 -4
  77. package/src/routes/workspaces.ts +22 -4
  78. package/src/sandbox/channel-a.ts +29 -2
  79. package/src/sandbox/machines.ts +5 -5
  80. package/src/sandbox/viewer.ts +10 -3
  81. package/src/slack-reaction-files.ts +181 -0
  82. package/src/transcription/providers/xai-subscription.ts +110 -0
  83. package/src/transcription/service.ts +17 -2
  84. package/src/xai-realtime.ts +216 -0
  85. package/src/xai-subscription-auth.ts +132 -0
  86. package/dist/chunk-JIKNR5YL.js.map +0 -1
  87. package/dist/routes/integration-features.d.ts +0 -3
package/src/mcp/server.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import {
3
3
  CreateScheduledTaskRequest,
4
- DEFAULT_FIRST_PARTY_MCP_TOOLS,
5
4
  FIRST_PARTY_MCP_TOOL_NAMES,
6
5
  defaultRepositoryMountPath,
7
6
  SESSION_EVENT_RAW_DELTA_TYPES,
@@ -30,10 +29,21 @@ import {
30
29
  type SessionAuthorizationActor,
31
30
  type SessionAuthorizationSurface,
32
31
  type Session,
32
+ type WorkspaceMemoryPromptMode,
33
33
  type ScheduledTask,
34
34
  UpdateScheduledTaskRequest,
35
35
  normalizeWorkspaceArtifactSlug,
36
36
  WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES,
37
+ SESSION_GOAL_PROGRESS_MAX_BYTES,
38
+ SESSION_GOAL_RATIONALE_MAX_BYTES,
39
+ SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
40
+ SESSION_GOAL_TEXT_MAX_BYTES,
41
+ sessionGoalUtf8Bytes,
42
+ TASK_NOTE_LIST_DEFAULT_LIMIT,
43
+ TASK_NOTE_LIST_MAX_LIMIT,
44
+ TASK_NOTE_MAX_LIFETIME_DAYS,
45
+ TASK_NOTE_REASON_MAX_BYTES,
46
+ TASK_NOTE_TEXT_MAX_BYTES,
37
47
  } from "@opengeni/contracts";
38
48
  import {
39
49
  countVariableSets,
@@ -75,6 +85,7 @@ import {
75
85
  searchWorkspaceMemories,
76
86
  serializeEffectiveSessionControl,
77
87
  setSessionGoalStatusWithEvent,
88
+ recordSessionGoalProgressWithEvent,
78
89
  setVariableSetVariable,
79
90
  updateScheduledTask,
80
91
  updateSessionGoalWithEvent,
@@ -86,12 +97,16 @@ import {
86
97
  listWorkspaceArtifacts,
87
98
  publishWorkspaceArtifactVersion,
88
99
  rollbackWorkspaceArtifact,
100
+ archiveTaskNote,
101
+ createTaskNote,
102
+ listTaskNotes,
89
103
  } from "@opengeni/db";
90
104
  import {
91
105
  appendAndPublishEvents,
92
106
  appendAndPublishTurnEventsFenced,
93
107
  publishDurableSessionEvents,
94
108
  } from "@opengeni/events";
109
+ import { allowedFirstPartyMcpToolsForSession } from "@opengeni/config";
95
110
  import {
96
111
  createSignedState,
97
112
  GitHubAppConfigurationError,
@@ -104,6 +119,7 @@ import {
104
119
  } from "@modelcontextprotocol/sdk/server/mcp.js";
105
120
  import type { AnySchema, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
106
121
  import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
122
+ import { HTTPException } from "hono/http-exception";
107
123
  import * as z4 from "zod/v4";
108
124
  import {
109
125
  hasLiteralPermission,
@@ -116,6 +132,8 @@ import {
116
132
  requireLiveAgentAttemptAuthorization,
117
133
  requireSessionAuthorization,
118
134
  requireSessionAuthorizationListScope,
135
+ SessionAuthorizationDeniedError,
136
+ SessionAuthorizationUnavailableError,
119
137
  saveWorkspaceMemoryWithSlackPublication,
120
138
  searchCapabilityCatalogItems,
121
139
  type ResolvedSessionAuthorization,
@@ -206,6 +224,7 @@ import {
206
224
  createOpenGeniSlackBotClient,
207
225
  resolveSlackBotConnectionForTool,
208
226
  } from "../integrations/slack-bot";
227
+ import { createFikenClient, resolveFikenConnectionForTool } from "../integrations/fiken";
209
228
  import {
210
229
  browseAtlassianSources,
211
230
  getAtlassianLiveItem,
@@ -220,8 +239,120 @@ export type McpServerOptions = {
220
239
  // tools use it only when no configured public base URL is available.
221
240
  requestOrigin?: string | null;
222
241
  workspaceMemoryEnabled?: boolean | undefined;
242
+ workspaceMemoryPromptMode?: WorkspaceMemoryPromptMode | undefined;
223
243
  };
224
244
 
245
+ const ORCHESTRATION_FAILURE_CODE_MAX_LENGTH = 128;
246
+ const ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
247
+
248
+ type OrchestrationToolName = "session_create" | "session_send_message";
249
+
250
+ function boundedOrchestrationFailureMessage(value: string): string {
251
+ const normalized = value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
252
+ if (!normalized) return "OpenGeni could not complete the request.";
253
+ const encoded = new TextEncoder().encode(normalized);
254
+ if (encoded.byteLength <= ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES) return normalized;
255
+ let end = ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES;
256
+ while (end > 0 && (encoded[end]! & 0xc0) === 0x80) end -= 1;
257
+ return new TextDecoder().decode(encoded.slice(0, end)).trim();
258
+ }
259
+
260
+ function orchestrationFailureCode(tool: OrchestrationToolName, error: HTTPException): string {
261
+ const suffix =
262
+ error.status === 401
263
+ ? "unauthenticated"
264
+ : error.status === 403
265
+ ? "forbidden"
266
+ : error.status === 404
267
+ ? "not_found"
268
+ : error.status === 409
269
+ ? "conflict"
270
+ : error.status === 429
271
+ ? "limit_exceeded"
272
+ : error.status >= 500
273
+ ? "unavailable"
274
+ : "rejected";
275
+ return `${tool}_${suffix}`.slice(0, ORCHESTRATION_FAILURE_CODE_MAX_LENGTH);
276
+ }
277
+
278
+ function orchestrationFailureEnvelope(tool: OrchestrationToolName, error: unknown) {
279
+ if (error instanceof SessionSpawnDeniedError) {
280
+ const denial = sessionSpawnDenialEnvelope(error);
281
+ return {
282
+ error: {
283
+ ...denial.error,
284
+ message: boundedOrchestrationFailureMessage(denial.error.message),
285
+ },
286
+ };
287
+ }
288
+ if (error instanceof HTTPException) {
289
+ return {
290
+ error: {
291
+ code: orchestrationFailureCode(tool, error),
292
+ message:
293
+ error.status >= 500
294
+ ? "OpenGeni is temporarily unavailable — retry."
295
+ : boundedOrchestrationFailureMessage(error.message),
296
+ },
297
+ };
298
+ }
299
+ if (error instanceof SessionAuthorizationDeniedError) {
300
+ return {
301
+ error: {
302
+ code: `${tool}_not_found_or_denied`,
303
+ message: "Session not found or access denied.",
304
+ },
305
+ };
306
+ }
307
+ if (error instanceof SessionAuthorizationUnavailableError) {
308
+ return {
309
+ error: {
310
+ code: `${tool}_authorization_unavailable`,
311
+ message: "Session authorization is temporarily unavailable — retry.",
312
+ },
313
+ };
314
+ }
315
+ const typedCode =
316
+ error && typeof error === "object" && "code" in error
317
+ ? (error as { code?: unknown }).code
318
+ : null;
319
+ const knownFailure =
320
+ typedCode === "CALLER_STALE"
321
+ ? ["caller_stale", "The calling session no longer owns this attempt."]
322
+ : typedCode === "CALLER_INTERRUPTED"
323
+ ? ["caller_interrupted", "The calling session was interrupted before delivery."]
324
+ : typedCode === "TARGET_NOT_VERTICAL"
325
+ ? ["target_not_vertical", "Agents may message only their parent or immediate children."]
326
+ : typedCode === "CONTROL_CHANGED"
327
+ ? ["conflict", "The target session control state changed; refresh and retry."]
328
+ : typedCode === "IDEMPOTENCY_KEY_REUSED"
329
+ ? ["idempotency_key_reused", "The idempotency key was reused with different input."]
330
+ : null;
331
+ if (knownFailure) {
332
+ return {
333
+ error: {
334
+ code: `${tool}_${knownFailure[0]}`,
335
+ message: knownFailure[1],
336
+ },
337
+ };
338
+ }
339
+ return {
340
+ error: {
341
+ code: `${tool}_failed`,
342
+ message: "OpenGeni could not complete the request.",
343
+ },
344
+ };
345
+ }
346
+
347
+ function orchestrationFailureResult(tool: OrchestrationToolName, error: unknown) {
348
+ const envelope = orchestrationFailureEnvelope(tool, error);
349
+ return {
350
+ content: [{ type: "text" as const, text: JSON.stringify(envelope, null, 2) }],
351
+ structuredContent: envelope,
352
+ isError: true as const,
353
+ };
354
+ }
355
+
225
356
  type FirstPartyToolAuthorization = {
226
357
  sessionRequired?: true;
227
358
  allOf?: readonly Permission[];
@@ -237,6 +368,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
237
368
  set_session_title: { sessionRequired: true, allOf: ["sessions:control"] },
238
369
  goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
239
370
  goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
371
+ goal_progress: { sessionRequired: true, allOf: ["goals:manage"] },
240
372
  goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
241
373
  goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
242
374
  memory_search: { sessionRequired: true, allOf: ["documents:search"] },
@@ -247,6 +379,9 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
247
379
  allOf: ["workspace:read"],
248
380
  },
249
381
  preference_registry_get: { sessionRequired: true, allOf: ["workspace:read"] },
382
+ task_notes_list: { sessionRequired: true, allOf: ["sessions:read"] },
383
+ task_note_save: { sessionRequired: true, allOf: ["sessions:control"] },
384
+ task_note_archive: { sessionRequired: true, allOf: ["sessions:control"] },
250
385
  sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
251
386
  sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
252
387
  sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
@@ -272,13 +407,20 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
272
407
  browser_tabs: { sessionRequired: true, allOf: ["sessions:control"] },
273
408
  browser_observe: { sessionRequired: true, allOf: ["sessions:read"] },
274
409
  browser_act: { sessionRequired: true, allOf: ["sessions:control"] },
410
+ browser_clipboard: { sessionRequired: true, allOf: ["sessions:read"] },
275
411
  browser_debug: { sessionRequired: true, allOf: ["sessions:read"] },
412
+ browser_auth: { sessionRequired: true, allOf: ["sessions:control"] },
413
+ interaction_request_human: {
414
+ sessionRequired: true,
415
+ allOf: ["sessions:control"],
416
+ },
276
417
  browser_identity: { sessionRequired: true, allOf: ["sessions:control"] },
277
418
  browser_publish: { sessionRequired: true, allOf: ["sessions:control"] },
278
419
  browser_lifecycle: { sessionRequired: true, allOf: ["sessions:control"] },
279
420
  computer_open: { sessionRequired: true, allOf: ["sessions:control"] },
280
421
  computer_targets: { sessionRequired: true, allOf: ["sessions:read"] },
281
422
  computer_observe: { sessionRequired: true, allOf: ["sessions:read"] },
423
+ computer_clipboard: { sessionRequired: true, allOf: ["sessions:read"] },
282
424
  computer_act: { sessionRequired: true, allOf: ["sessions:control"] },
283
425
  computer_lifecycle: { sessionRequired: true, allOf: ["sessions:control"] },
284
426
  variable_set_list: { allOf: ["variable-sets:list", "secrets:list"] },
@@ -291,7 +433,10 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
291
433
  allOf: ["variable-sets:write", "secrets:write"],
292
434
  },
293
435
  environment_set_variable: { allOf: ["variable-sets:write", "secrets:write"] },
294
- capability_catalog_search: { sessionRequired: true, allOf: ["workspace:read"] },
436
+ capability_catalog_search: {
437
+ sessionRequired: true,
438
+ allOf: ["workspace:read"],
439
+ },
295
440
  capability_authorization_request: {
296
441
  sessionRequired: true,
297
442
  allOf: ["workspace:read"],
@@ -346,6 +491,18 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
346
491
  slack_bot_file_content: { allOf: ["connections:read"] },
347
492
  slack_bot_post_message: { allOf: ["connections:read"] },
348
493
  slack_bot_delete_message: { allOf: ["connections:read"] },
494
+ fiken_companies_list: { allOf: ["connections:read"] },
495
+ fiken_contacts_list: { allOf: ["connections:read"] },
496
+ fiken_products_list: { allOf: ["connections:read"] },
497
+ fiken_invoices_list: { allOf: ["connections:read"] },
498
+ fiken_invoice_get: { allOf: ["connections:read"] },
499
+ fiken_bank_accounts_list: { allOf: ["connections:read"] },
500
+ fiken_purchases_list: { allOf: ["connections:read"] },
501
+ fiken_sales_list: { allOf: ["connections:read"] },
502
+ // Writes into the workspace's real accounting ledger surface take the write
503
+ // scope, keeping them out of the default agent permission set.
504
+ fiken_contact_create: { allOf: ["connections:write"] },
505
+ fiken_invoice_draft_create: { allOf: ["connections:write"] },
349
506
  atlassian_sources_list: { allOf: ["connections:read"] },
350
507
  atlassian_search: { allOf: ["connections:read"] },
351
508
  atlassian_get: { allOf: ["connections:read"] },
@@ -355,14 +512,23 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
355
512
  artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
356
513
  artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] },
357
514
  editable_artifact_list: { sessionRequired: true, allOf: ["artifacts:read"] },
358
- editable_artifact_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
515
+ editable_artifact_create: {
516
+ sessionRequired: true,
517
+ allOf: ["artifacts:publish"],
518
+ },
359
519
  editable_artifact_import: {
360
520
  sessionRequired: true,
361
521
  allOf: ["artifacts:publish", "files:read"],
362
522
  },
363
523
  editable_artifact_get: { sessionRequired: true, allOf: ["artifacts:read"] },
364
- editable_artifact_inspect: { sessionRequired: true, allOf: ["artifacts:read"] },
365
- editable_artifact_apply: { sessionRequired: true, allOf: ["artifacts:publish"] },
524
+ editable_artifact_inspect: {
525
+ sessionRequired: true,
526
+ allOf: ["artifacts:read"],
527
+ },
528
+ editable_artifact_apply: {
529
+ sessionRequired: true,
530
+ allOf: ["artifacts:publish"],
531
+ },
366
532
  editable_artifact_export: {
367
533
  sessionRequired: true,
368
534
  allOf: ["artifacts:read"],
@@ -491,10 +657,21 @@ export function buildOpenGeniMcpServer(
491
657
  const selectedTools =
492
658
  sessionId !== null
493
659
  ? new Set(
494
- (grant.metadata?.["firstPartyMcpTools"] as FirstPartyMcpToolName[] | undefined) ??
495
- DEFAULT_FIRST_PARTY_MCP_TOOLS,
660
+ allowedFirstPartyMcpToolsForSession(
661
+ deps.settings,
662
+ grant.metadata?.["firstPartyMcpTools"] as FirstPartyMcpToolName[] | undefined,
663
+ ),
496
664
  )
497
665
  : null;
666
+ const nestedAgentDepth = grant.metadata?.["nestedAgentDepth"];
667
+ const effectiveMaxNestedAgentDepth = grant.metadata?.["effectiveMaxNestedAgentDepth"];
668
+ // Optional claims keep rolling deployments compatible. When both trusted
669
+ // facts are present, an exhausted session does not receive an unusable spawn
670
+ // tool; stale/legacy callers still meet the authoritative DB admission gate.
671
+ const sessionCreateVisible =
672
+ typeof nestedAgentDepth !== "number" ||
673
+ typeof effectiveMaxNestedAgentDepth !== "number" ||
674
+ nestedAgentDepth < effectiveMaxNestedAgentDepth;
498
675
  const server = new PolicyMcpServer(grant, sessionId, selectedTools);
499
676
  // set_session_title names the agent's OWN session — pure session metadata,
500
677
  // not a goal operation — so it is available on every session, gated only on
@@ -523,10 +700,18 @@ export function buildOpenGeniMcpServer(
523
700
  registerGoalTools(server, deps, grant, sessionId, json);
524
701
  }
525
702
  if (sessionId !== null && options.workspaceMemoryEnabled === true) {
526
- registerMemoryTools(server, deps, grant, sessionId, json);
703
+ registerMemoryTools(
704
+ server,
705
+ deps,
706
+ grant,
707
+ sessionId,
708
+ json,
709
+ options.workspaceMemoryPromptMode ?? "legacy_standing",
710
+ );
527
711
  }
528
712
  if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
529
713
  registerPreferenceRegistryTools(server, deps, grant, json);
714
+ registerTaskNoteTools(server, deps, grant, sessionId, json);
530
715
  }
531
716
  if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
532
717
  registerWorkspaceArtifactTools(server, deps, grant, sessionId, json);
@@ -555,6 +740,7 @@ export function buildOpenGeniMcpServer(
555
740
  }
556
741
  registerRigTools(server, deps, grant, can, sessionId, json);
557
742
  registerSlackBotTools(server, deps, grant, sessionId, json);
743
+ registerFikenTools(server, deps, grant, sessionId, json);
558
744
  registerAtlassianTools(server, deps, grant, json);
559
745
 
560
746
  // Orchestration, variableSet, and GitHub status tools are permission-gated
@@ -568,7 +754,15 @@ export function buildOpenGeniMcpServer(
568
754
  // never returned through a model-visible MCP tool. A user DEMOTES a specific
569
755
  // session by setting a narrower session.firstPartyMcpPermissions (capped to
570
756
  // the creator's own grant); operators still cap what any session can be given.
571
- registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, json);
757
+ registerWorkspaceOrchestrationTools(
758
+ server,
759
+ deps,
760
+ grant,
761
+ can,
762
+ sessionId,
763
+ sessionCreateVisible,
764
+ json,
765
+ );
572
766
  registerVariableSetTools(server, deps, grant, can, sessionId, json);
573
767
  if (sessionId !== null && can("workspace:read")) {
574
768
  registerCapabilityDiscoveryTools(server, deps, grant, sessionId, json);
@@ -863,7 +1057,10 @@ export function buildOpenGeniMcpServer(
863
1057
  },
864
1058
  { query, subreddit, limit },
865
1059
  );
866
- return json({ provider: result.connection.provider, posts: result.posts });
1060
+ return json({
1061
+ provider: result.connection.provider,
1062
+ posts: result.posts,
1063
+ });
867
1064
  },
868
1065
  );
869
1066
  server.registerTool(
@@ -890,7 +1087,10 @@ export function buildOpenGeniMcpServer(
890
1087
  },
891
1088
  { sinceId, limit },
892
1089
  );
893
- return json({ provider: result.connection.provider, posts: result.posts });
1090
+ return json({
1091
+ provider: result.connection.provider,
1092
+ posts: result.posts,
1093
+ });
894
1094
  },
895
1095
  );
896
1096
  server.registerTool(
@@ -917,7 +1117,10 @@ export function buildOpenGeniMcpServer(
917
1117
  },
918
1118
  { id, limit },
919
1119
  );
920
- return json({ provider: result.connection.provider, posts: result.posts });
1120
+ return json({
1121
+ provider: result.connection.provider,
1122
+ posts: result.posts,
1123
+ });
921
1124
  },
922
1125
  );
923
1126
  }
@@ -1674,53 +1877,268 @@ function registerSlackBotTools(
1674
1877
  );
1675
1878
 
1676
1879
  server.registerTool(
1677
- "slack_bot_post_message",
1880
+ "slack_bot_delete_message",
1678
1881
  {
1679
1882
  description:
1680
- "Post as the workspace-shared OpenGeni bot. Pass channelId for a channel where the bot is already a member, or userId to open/post a DM; pass exactly one. To reply in a thread, also pass its parent message timestamp as threadTimestamp. Generate one operationId UUID per intended message and reuse that same UUID on every retry.",
1883
+ "Delete a message authored by the workspace-shared OpenGeni bot. Pass the channel ID and exact message timestamp returned by a prior post or channel/thread read. Generate one operationId UUID per intended deletion and reuse it on every retry, including after an unknown outcome. Slack refuses deletion of messages not authored by this bot.",
1681
1884
  inputSchema: {
1682
1885
  connectionId: z4.string().uuid().optional(),
1683
1886
  operationId: z4.string().uuid(),
1684
- channelId: z4.string().min(1).max(64).optional(),
1685
- userId: z4.string().min(1).max(64).optional(),
1686
- threadTimestamp: z4.string().min(1).max(64).optional(),
1687
- text: z4.string().min(1).max(40_000),
1887
+ channelId: z4.string().min(1).max(64),
1888
+ timestamp: z4.string().min(1).max(64),
1688
1889
  },
1689
1890
  },
1690
- async ({ connectionId, operationId, channelId, userId, threadTimestamp, text }) => {
1691
- if (Boolean(channelId) === Boolean(userId)) {
1692
- throw new Error("exactly one of channelId or userId is required");
1693
- }
1694
- return json(
1891
+ async ({ connectionId, operationId, channelId, timestamp }) =>
1892
+ json(
1893
+ await (await clientFor(connectionId)).deleteMessage({ operationId, channelId, timestamp }),
1894
+ ),
1895
+ );
1896
+ }
1897
+
1898
+ function registerFikenTools(
1899
+ server: McpServer,
1900
+ deps: ApiRouteDeps,
1901
+ grant: AccessGrant,
1902
+ sessionId: string | null,
1903
+ json: JsonResult,
1904
+ ): void {
1905
+ // One resolution per requested connection per MCP server instance: the
1906
+ // bound row cannot change mid-request, and re-resolving on every tool call
1907
+ // would pay an extra connections read each time.
1908
+ const clients = new Map<string, Promise<ReturnType<typeof createFikenClient>>>();
1909
+ const clientFor = (connectionId?: string) => {
1910
+ const cacheKey = connectionId ?? "";
1911
+ let client = clients.get(cacheKey);
1912
+ if (!client) {
1913
+ client = resolveFikenConnectionForTool({
1914
+ db: deps.db,
1915
+ grant,
1916
+ sessionId,
1917
+ ...(connectionId ? { requestedConnectionId: connectionId } : {}),
1918
+ }).then((resolved) => createFikenClient(deps, resolved));
1919
+ // A failed resolution must not be cached as a poisoned entry.
1920
+ client.catch(() => clients.delete(cacheKey));
1921
+ clients.set(cacheKey, client);
1922
+ }
1923
+ return client;
1924
+ };
1925
+ const companySlugInput = z4
1926
+ .string()
1927
+ .min(1)
1928
+ .max(128)
1929
+ .optional()
1930
+ .describe(
1931
+ "Fiken company slug. Optional when the connection has a default company or access to exactly one company.",
1932
+ );
1933
+ const pageInputs = {
1934
+ page: z4.number().int().min(0).optional(),
1935
+ pageSize: z4.number().int().min(1).max(100).optional(),
1936
+ };
1937
+ const isoDate = z4
1938
+ .string()
1939
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
1940
+ .optional();
1941
+
1942
+ server.registerTool(
1943
+ "fiken_companies_list",
1944
+ {
1945
+ description:
1946
+ "List the Fiken companies this workspace's Fiken connection can act on. Use the returned slug as companySlug in other fiken tools.",
1947
+ inputSchema: {
1948
+ connectionId: z4.string().uuid().optional(),
1949
+ ...pageInputs,
1950
+ },
1951
+ },
1952
+ async ({ connectionId, page, pageSize }) =>
1953
+ json(
1695
1954
  await (
1696
1955
  await clientFor(connectionId)
1697
- ).postMessage({
1698
- operationId,
1699
- ...(channelId ? { channelId } : {}),
1700
- ...(userId ? { userId } : {}),
1701
- ...(threadTimestamp ? { threadTimestamp } : {}),
1702
- text,
1956
+ ).listCompanies({
1957
+ ...(page !== undefined ? { page } : {}),
1958
+ ...(pageSize !== undefined ? { pageSize } : {}),
1703
1959
  }),
1704
- );
1960
+ ),
1961
+ );
1962
+
1963
+ server.registerTool(
1964
+ "fiken_contacts_list",
1965
+ {
1966
+ description:
1967
+ "List or search contacts (customers and suppliers) in a Fiken company. Filters combine with AND. customerId used elsewhere equals a contact's contactId where customer is true.",
1968
+ inputSchema: {
1969
+ connectionId: z4.string().uuid().optional(),
1970
+ companySlug: companySlugInput,
1971
+ ...pageInputs,
1972
+ name: z4.string().min(1).max(256).optional(),
1973
+ email: z4.string().min(1).max(256).optional(),
1974
+ organizationNumber: z4.string().min(1).max(64).optional(),
1975
+ customer: z4.boolean().optional(),
1976
+ supplier: z4.boolean().optional(),
1977
+ inactive: z4.boolean().optional(),
1978
+ },
1705
1979
  },
1980
+ async ({ connectionId, ...input }) =>
1981
+ json(await (await clientFor(connectionId)).listContacts(input)),
1706
1982
  );
1707
1983
 
1708
1984
  server.registerTool(
1709
- "slack_bot_delete_message",
1985
+ "fiken_contact_create",
1710
1986
  {
1711
1987
  description:
1712
- "Delete a message authored by the workspace-shared OpenGeni bot. Pass the channel ID and exact message timestamp returned by a prior post or channel/thread read. Generate one operationId UUID per intended deletion and reuse it on every retry, including after an unknown outcome. Slack refuses deletion of messages not authored by this bot.",
1988
+ "Create a contact in a Fiken company. Retrying after an unknown outcome may create a duplicate; search fiken_contacts_list first when unsure.",
1713
1989
  inputSchema: {
1714
1990
  connectionId: z4.string().uuid().optional(),
1991
+ companySlug: companySlugInput,
1992
+ name: z4.string().min(1).max(256),
1993
+ email: z4.string().min(1).max(256).optional(),
1994
+ organizationNumber: z4.string().min(1).max(64).optional(),
1995
+ phoneNumber: z4.string().min(1).max(64).optional(),
1996
+ customer: z4.boolean().optional(),
1997
+ supplier: z4.boolean().optional(),
1998
+ },
1999
+ },
2000
+ async ({ connectionId, ...input }) =>
2001
+ json(await (await clientFor(connectionId)).createContact(input)),
2002
+ );
2003
+
2004
+ server.registerTool(
2005
+ "fiken_products_list",
2006
+ {
2007
+ description: "List products in a Fiken company.",
2008
+ inputSchema: {
2009
+ connectionId: z4.string().uuid().optional(),
2010
+ companySlug: companySlugInput,
2011
+ ...pageInputs,
2012
+ name: z4.string().min(1).max(256).optional(),
2013
+ active: z4.boolean().optional(),
2014
+ },
2015
+ },
2016
+ async ({ connectionId, ...input }) =>
2017
+ json(await (await clientFor(connectionId)).listProducts(input)),
2018
+ );
2019
+
2020
+ server.registerTool(
2021
+ "fiken_invoices_list",
2022
+ {
2023
+ description:
2024
+ "List invoices in a Fiken company. Dates are yyyy-mm-dd; monetary amounts in results are in cents/øre of the invoice currency.",
2025
+ inputSchema: {
2026
+ connectionId: z4.string().uuid().optional(),
2027
+ companySlug: companySlugInput,
2028
+ ...pageInputs,
2029
+ issueDateGe: isoDate,
2030
+ issueDateLe: isoDate,
2031
+ customerId: z4.number().int().positive().optional(),
2032
+ settled: z4.boolean().optional(),
2033
+ invoiceNumber: z4.string().min(1).max(64).optional(),
2034
+ },
2035
+ },
2036
+ async ({ connectionId, ...input }) =>
2037
+ json(await (await clientFor(connectionId)).listInvoices(input)),
2038
+ );
2039
+
2040
+ server.registerTool(
2041
+ "fiken_invoice_get",
2042
+ {
2043
+ description: "Read a single Fiken invoice by its invoiceId.",
2044
+ inputSchema: {
2045
+ connectionId: z4.string().uuid().optional(),
2046
+ companySlug: companySlugInput,
2047
+ invoiceId: z4.number().int().positive(),
2048
+ },
2049
+ },
2050
+ async ({ connectionId, ...input }) =>
2051
+ json(await (await clientFor(connectionId)).getInvoice(input)),
2052
+ );
2053
+
2054
+ server.registerTool(
2055
+ "fiken_invoice_draft_create",
2056
+ {
2057
+ description:
2058
+ "Create an invoice DRAFT in Fiken. Drafts are not sent to the customer; a human finishes and sends them from Fiken. unitPriceCents is the net price per unit in cents/øre. Each line needs either productId or an incomeAccount + vatType. Generate one operationId UUID per intended draft and reuse that same UUID on every retry; a retry returns the existing draft instead of duplicating it.",
2059
+ inputSchema: {
2060
+ connectionId: z4.string().uuid().optional(),
2061
+ companySlug: companySlugInput,
1715
2062
  operationId: z4.string().uuid(),
1716
- channelId: z4.string().min(1).max(64),
1717
- timestamp: z4.string().min(1).max(64),
2063
+ customerId: z4.number().int().positive(),
2064
+ daysUntilDueDate: z4.number().int().min(0).max(365),
2065
+ invoiceText: z4.string().max(500).optional(),
2066
+ yourReference: z4.string().max(128).optional(),
2067
+ ourReference: z4.string().max(128).optional(),
2068
+ currency: z4
2069
+ .string()
2070
+ .regex(/^[A-Z]{3}$/)
2071
+ .optional(),
2072
+ bankAccountNumber: z4.string().min(1).max(64).optional(),
2073
+ lines: z4
2074
+ .array(
2075
+ z4.object({
2076
+ description: z4.string().max(200).optional(),
2077
+ productId: z4.number().int().positive().optional(),
2078
+ unitPriceCents: z4.number().int().optional(),
2079
+ vatType: z4.string().min(1).max(64).optional(),
2080
+ quantity: z4.number().positive(),
2081
+ discountPercent: z4.number().min(0).max(100).optional(),
2082
+ incomeAccount: z4.string().min(1).max(16).optional(),
2083
+ comment: z4.string().max(200).optional(),
2084
+ }),
2085
+ )
2086
+ .min(1)
2087
+ .max(100),
1718
2088
  },
1719
2089
  },
1720
- async ({ connectionId, operationId, channelId, timestamp }) =>
1721
- json(
1722
- await (await clientFor(connectionId)).deleteMessage({ operationId, channelId, timestamp }),
1723
- ),
2090
+ async ({ connectionId, ...input }) =>
2091
+ json(await (await clientFor(connectionId)).createInvoiceDraft(input)),
2092
+ );
2093
+
2094
+ server.registerTool(
2095
+ "fiken_bank_accounts_list",
2096
+ {
2097
+ description:
2098
+ "List bank accounts in a Fiken company, including reconciled balances in cents/øre.",
2099
+ inputSchema: {
2100
+ connectionId: z4.string().uuid().optional(),
2101
+ companySlug: companySlugInput,
2102
+ ...pageInputs,
2103
+ inactive: z4.boolean().optional(),
2104
+ },
2105
+ },
2106
+ async ({ connectionId, ...input }) =>
2107
+ json(await (await clientFor(connectionId)).listBankAccounts(input)),
2108
+ );
2109
+
2110
+ server.registerTool(
2111
+ "fiken_purchases_list",
2112
+ {
2113
+ description: "List purchases in a Fiken company. Dates are yyyy-mm-dd.",
2114
+ inputSchema: {
2115
+ connectionId: z4.string().uuid().optional(),
2116
+ companySlug: companySlugInput,
2117
+ ...pageInputs,
2118
+ dateGe: isoDate,
2119
+ dateLe: isoDate,
2120
+ paid: z4.boolean().optional(),
2121
+ },
2122
+ },
2123
+ async ({ connectionId, ...input }) =>
2124
+ json(await (await clientFor(connectionId)).listPurchases(input)),
2125
+ );
2126
+
2127
+ server.registerTool(
2128
+ "fiken_sales_list",
2129
+ {
2130
+ description: "List sales in a Fiken company. Dates are yyyy-mm-dd.",
2131
+ inputSchema: {
2132
+ connectionId: z4.string().uuid().optional(),
2133
+ companySlug: companySlugInput,
2134
+ ...pageInputs,
2135
+ dateGe: isoDate,
2136
+ dateLe: isoDate,
2137
+ settled: z4.boolean().optional(),
2138
+ },
2139
+ },
2140
+ async ({ connectionId, ...input }) =>
2141
+ json(await (await clientFor(connectionId)).listSales(input)),
1724
2142
  );
1725
2143
  }
1726
2144
 
@@ -1731,7 +2149,10 @@ function registerAtlassianTools(
1731
2149
  json: JsonResult,
1732
2150
  ): void {
1733
2151
  const connectionFor = async (connectionId?: string) => {
1734
- const authorized = await authorizedAtlassianConnectionsForGrant({ db: deps.db, grant });
2152
+ const authorized = await authorizedAtlassianConnectionsForGrant({
2153
+ db: deps.db,
2154
+ grant,
2155
+ });
1735
2156
  const candidates = authorized.filter(({ connection }) =>
1736
2157
  connectionId ? connection.id === connectionId : true,
1737
2158
  );
@@ -1775,7 +2196,10 @@ function registerAtlassianTools(
1775
2196
  return json({
1776
2197
  connectionId: authority.connection.id,
1777
2198
  account: authority.metadata.displayName,
1778
- items: response.items.map((item) => ({ ...item, selected: selected.has(item.id) })),
2199
+ items: response.items.map((item) => ({
2200
+ ...item,
2201
+ selected: selected.has(item.id),
2202
+ })),
1779
2203
  });
1780
2204
  },
1781
2205
  );
@@ -1886,20 +2310,43 @@ function registerGoalTools(
1886
2310
  sessionId: string,
1887
2311
  json: (value: unknown) => { content: Array<{ type: "text"; text: string }> },
1888
2312
  ): void {
2313
+ const boundedGoalToolString = (maxBytes: number, field: string) =>
2314
+ z4
2315
+ .string()
2316
+ .min(1)
2317
+ .refine((value) => sessionGoalUtf8Bytes(value) <= maxBytes, {
2318
+ message: `${field} exceeds ${maxBytes} UTF-8 bytes`,
2319
+ });
2320
+ const goalText = boundedGoalToolString(SESSION_GOAL_TEXT_MAX_BYTES, "goal text");
2321
+ const successCriteriaSchema = boundedGoalToolString(
2322
+ SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
2323
+ "goal success criteria",
2324
+ );
2325
+ const goalRationale = boundedGoalToolString(SESSION_GOAL_RATIONALE_MAX_BYTES, "goal rationale");
2326
+ const progressNoteSchema = boundedGoalToolString(
2327
+ SESSION_GOAL_PROGRESS_MAX_BYTES,
2328
+ "goal progress note",
2329
+ );
1889
2330
  server.registerTool(
1890
2331
  "goal_set",
1891
2332
  {
1892
2333
  description:
1893
- "Set or replace this session's goal. While a goal is active the session keeps working: idle moments synthesize continuation turns until goal_complete or goal_pause is called. Replacing a goal reactivates it and resets the continuation budget.",
2334
+ "Create a goal when this session has none. While active, idle moments synthesize continuation turns until goal_complete or goal_pause. To change an existing goal, use goal_update with its objective revision, a change kind, and rationale.",
1894
2335
  inputSchema: {
1895
- text: z4.string().min(1),
1896
- successCriteria: z4.string().min(1).optional(),
2336
+ text: goalText,
2337
+ successCriteria: successCriteriaSchema.optional(),
1897
2338
  maxAutoContinuations: z4.number().int().positive().optional(),
1898
2339
  },
1899
2340
  },
1900
2341
  async ({ text, successCriteria, maxAutoContinuations }) => {
1901
2342
  await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
1902
2343
  await requireSession(deps.db, grant.workspaceId, sessionId);
2344
+ const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
2345
+ if (existing) {
2346
+ throw new Error(
2347
+ `this session already has a goal at objective revision ${existing.objectiveRevision}; use goal_update to revise it`,
2348
+ );
2349
+ }
1903
2350
  const callerTurnId =
1904
2351
  typeof grant.metadata?.["turnId"] === "string"
1905
2352
  ? (grant.metadata["turnId"] as string)
@@ -1943,26 +2390,100 @@ function registerGoalTools(
1943
2390
  "goal_update",
1944
2391
  {
1945
2392
  description:
1946
- "Revise the session goal's text or success criteria, or record a progress note. Counts as progress for the no-progress detector; the goal stays active.",
2393
+ "Propose or apply a semantic goal revision under the session's mutation policy. Retain the standing goal unless explicit user direction or meaningful new evidence justifies the declared refinement, adaptation, or replacement. A rewrite never counts as execution progress; use goal_progress for that.",
1947
2394
  inputSchema: {
1948
- text: z4.string().min(1).optional(),
1949
- successCriteria: z4.string().min(1).optional(),
1950
- progressNote: z4.string().min(1).optional(),
2395
+ text: goalText.optional(),
2396
+ successCriteria: successCriteriaSchema.nullable().optional(),
2397
+ // Optional for rolling compatibility with the former goal_update
2398
+ // surface. Omitted semantic metadata is classified as a refinement of
2399
+ // the currently fenced objective; new callers should always supply it.
2400
+ changeKind: z4.enum(["refinement", "adaptation", "replacement"]).optional(),
2401
+ rationale: goalRationale.optional(),
2402
+ expectedObjectiveRevision: z4.number().int().positive().optional(),
2403
+ // Deprecated compatibility input. It is committed through the new
2404
+ // progress operation and never makes a semantic rewrite count itself.
2405
+ progressNote: progressNoteSchema.optional(),
1951
2406
  idempotencyKey: z4.string().uuid(),
1952
2407
  },
1953
2408
  },
1954
- async ({ text, successCriteria, progressNote, idempotencyKey }) => {
2409
+ async ({
2410
+ text,
2411
+ successCriteria,
2412
+ changeKind,
2413
+ rationale,
2414
+ expectedObjectiveRevision,
2415
+ progressNote,
2416
+ idempotencyKey,
2417
+ }) => {
1955
2418
  await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
2419
+ if (text === undefined && successCriteria === undefined && progressNote === undefined) {
2420
+ throw new Error("goal_update requires semantic content or a progressNote");
2421
+ }
1956
2422
  const context = exactAgentCommandContext(grant, sessionId);
1957
- const { goal, events, operationId, replay } = await updateSessionGoalWithEvent(
2423
+ const command = {
2424
+ accountId: grant.accountId,
2425
+ actor: {
2426
+ type: "agent_attempt" as const,
2427
+ attemptId: context.callerAttemptId,
2428
+ sessionId: context.callerSessionId,
2429
+ turnId: context.callerTurnId,
2430
+ executionGeneration: context.callerExecutionGeneration,
2431
+ },
2432
+ operationKey: idempotencyKey,
2433
+ };
2434
+ const semantic =
2435
+ text !== undefined || successCriteria !== undefined
2436
+ ? await updateSessionGoalWithEvent(deps.db, grant.workspaceId, sessionId, {
2437
+ ...(text !== undefined ? { text } : {}),
2438
+ ...(successCriteria !== undefined ? { successCriteria } : {}),
2439
+ changeKind: changeKind ?? "refinement",
2440
+ rationale: rationale ?? "Compatibility refinement from goal_update",
2441
+ ...(expectedObjectiveRevision !== undefined ? { expectedObjectiveRevision } : {}),
2442
+ actor: "agent",
2443
+ command,
2444
+ })
2445
+ : null;
2446
+ const progress = progressNote
2447
+ ? await recordSessionGoalProgressWithEvent(deps.db, grant.workspaceId, sessionId, {
2448
+ progressNote,
2449
+ command,
2450
+ })
2451
+ : null;
2452
+ await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, [
2453
+ ...(semantic?.events ?? []),
2454
+ ...(progress?.events ?? []),
2455
+ ]);
2456
+ const goal = progress?.goal ?? semantic?.goal;
2457
+ if (!goal) throw new Error("goal_update produced no mutation");
2458
+ return json({
2459
+ ...goal,
2460
+ operationId: semantic?.operationId ?? progress?.operationId ?? null,
2461
+ replay: Boolean(semantic?.replay || progress?.replay),
2462
+ outcome: semantic?.outcome ?? "progress_recorded",
2463
+ proposalId: semantic?.proposalId ?? null,
2464
+ });
2465
+ },
2466
+ );
2467
+
2468
+ server.registerTool(
2469
+ "goal_progress",
2470
+ {
2471
+ description:
2472
+ "Record concrete progress toward the unchanged active goal. This does not change goal text, success criteria, mutation policy, or objective revision. Do not use it merely to keep the continuation loop alive.",
2473
+ inputSchema: {
2474
+ progressNote: progressNoteSchema,
2475
+ idempotencyKey: z4.string().uuid(),
2476
+ },
2477
+ },
2478
+ async ({ progressNote, idempotencyKey }) => {
2479
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
2480
+ const context = exactAgentCommandContext(grant, sessionId);
2481
+ const { goal, events, operationId, replay } = await recordSessionGoalProgressWithEvent(
1958
2482
  deps.db,
1959
2483
  grant.workspaceId,
1960
2484
  sessionId,
1961
2485
  {
1962
- ...(text !== undefined ? { text } : {}),
1963
- ...(successCriteria !== undefined ? { successCriteria } : {}),
1964
- ...(progressNote !== undefined ? { progressNote } : {}),
1965
- actor: "agent",
2486
+ progressNote,
1966
2487
  command: {
1967
2488
  accountId: grant.accountId,
1968
2489
  actor: {
@@ -2034,7 +2555,7 @@ function registerGoalTools(
2034
2555
  {
2035
2556
  description:
2036
2557
  "Pause the session goal with a rationale (blocked, not productive, needs human input). No further continuation turns are synthesized until the goal is resumed or replaced.",
2037
- inputSchema: { rationale: z4.string().min(1) },
2558
+ inputSchema: { rationale: goalRationale },
2038
2559
  },
2039
2560
  async ({ rationale }) => {
2040
2561
  await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
@@ -2209,7 +2730,7 @@ function registerWorkspaceArtifactTools(
2209
2730
  "artifacts_create",
2210
2731
  {
2211
2732
  description:
2212
- "Create and publish a generic static workspace artifact from a complete, self-contained HTML document with inline CSS. JavaScript and active or navigation-capable markup do not render in the MVP.",
2733
+ "Create and publish a generic workspace artifact from a complete HTML document. The exact HTML runs in an opaque-origin sandboxed iframe with JavaScript, external resources, forms, popups, and downloads enabled, but without parent-origin authority or top-level navigation.",
2213
2734
  inputSchema: {
2214
2735
  title: z4.string().min(1).max(120),
2215
2736
  description: z4.string().max(2000).nullable().optional(),
@@ -2244,7 +2765,7 @@ function registerWorkspaceArtifactTools(
2244
2765
  "artifacts_publish",
2245
2766
  {
2246
2767
  description:
2247
- "Publish a new immutable static HTML/CSS version. JavaScript and active or navigation-capable markup do not render in the MVP. First read the current source and pass its version id for optimistic concurrency.",
2768
+ "Publish a new immutable HTML version. The exact HTML runs in an opaque-origin sandboxed iframe. First read the current source and pass its version id for optimistic concurrency.",
2248
2769
  inputSchema: {
2249
2770
  artifactId: z4.string().uuid(),
2250
2771
  expectedCurrentVersionId: z4.string().uuid(),
@@ -2303,6 +2824,7 @@ function exactAgentAttemptClaims(grant: AccessGrant): {
2303
2824
  attemptId: string;
2304
2825
  executionGeneration: number;
2305
2826
  } | null {
2827
+ if (grant.principalKind !== "agent_attempt") return null;
2306
2828
  const metadata = grant.metadata ?? {};
2307
2829
  if (
2308
2830
  typeof metadata["sessionId"] !== "string" ||
@@ -2323,6 +2845,109 @@ function exactAgentAttemptClaims(grant: AccessGrant): {
2323
2845
  };
2324
2846
  }
2325
2847
 
2848
+ function registerTaskNoteTools(
2849
+ server: McpServer,
2850
+ deps: ApiRouteDeps,
2851
+ grant: AccessGrant,
2852
+ sessionId: string,
2853
+ json: JsonResult,
2854
+ ): void {
2855
+ const attemptClaims = () => {
2856
+ const resolved = exactAgentAttemptClaims(grant);
2857
+ if (!resolved || resolved.sessionId !== sessionId) {
2858
+ throw new Error("Exact signed task-note attempt authority is required.");
2859
+ }
2860
+ return { accountId: grant.accountId, workspaceId: grant.workspaceId, ...resolved };
2861
+ };
2862
+ const authorize = async () => {
2863
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
2864
+ };
2865
+ const boundedUtf8 = (maxBytes: number, label: string) =>
2866
+ z4
2867
+ .string()
2868
+ .min(1)
2869
+ .refine((value) => value === value.trim(), {
2870
+ message: `${label} must not have leading or trailing whitespace`,
2871
+ })
2872
+ .refine((value) => new TextEncoder().encode(value).byteLength <= maxBytes, {
2873
+ message: `${label} must be at most ${maxBytes} UTF-8 bytes`,
2874
+ });
2875
+
2876
+ server.registerTool(
2877
+ "task_notes_list",
2878
+ {
2879
+ description:
2880
+ "Explicitly retrieve bounded, unexpired coordination notes shared by this session's root task tree. Notes are non-authoritative and are never injected into prompts automatically.",
2881
+ inputSchema: {
2882
+ includeArchived: z4.boolean().optional().default(false),
2883
+ limit: z4
2884
+ .number()
2885
+ .int()
2886
+ .min(1)
2887
+ .max(TASK_NOTE_LIST_MAX_LIMIT)
2888
+ .optional()
2889
+ .default(TASK_NOTE_LIST_DEFAULT_LIMIT),
2890
+ },
2891
+ },
2892
+ async ({ includeArchived, limit }) => {
2893
+ await authorize();
2894
+ return json(await listTaskNotes(deps.db, { ...attemptClaims(), includeArchived, limit }));
2895
+ },
2896
+ );
2897
+
2898
+ server.registerTool(
2899
+ "task_note_save",
2900
+ {
2901
+ description:
2902
+ "Save one bounded, expiring, non-authoritative coordination note for agents in this root task tree. Use a fresh operationId; exact retries of the same attempt and input replay safely.",
2903
+ inputSchema: {
2904
+ operationId: z4.string().uuid(),
2905
+ kind: z4.enum(["finding", "decision", "blocker", "ownership", "artifact", "handoff"]),
2906
+ text: boundedUtf8(TASK_NOTE_TEXT_MAX_BYTES, "Task note text"),
2907
+ expiresInDays: z4.number().int().min(1).max(TASK_NOTE_MAX_LIFETIME_DAYS),
2908
+ },
2909
+ },
2910
+ async ({ operationId, kind, text, expiresInDays }) => {
2911
+ await authorize();
2912
+ return json(
2913
+ await createTaskNote(deps.db, {
2914
+ ...attemptClaims(),
2915
+ operationId,
2916
+ kind,
2917
+ text,
2918
+ expiresInDays,
2919
+ }),
2920
+ );
2921
+ },
2922
+ );
2923
+
2924
+ server.registerTool(
2925
+ "task_note_archive",
2926
+ {
2927
+ description:
2928
+ "Archive a coordination note from this root task tree with optimistic version fencing. This preserves its immutable create receipt and records a separate archive receipt.",
2929
+ inputSchema: {
2930
+ operationId: z4.string().uuid(),
2931
+ noteId: z4.string().uuid(),
2932
+ expectedVersion: z4.number().int().min(1).max(1),
2933
+ reason: boundedUtf8(TASK_NOTE_REASON_MAX_BYTES, "Task note archive reason"),
2934
+ },
2935
+ },
2936
+ async ({ operationId, noteId, expectedVersion, reason }) => {
2937
+ await authorize();
2938
+ return json(
2939
+ await archiveTaskNote(deps.db, {
2940
+ ...attemptClaims(),
2941
+ operationId,
2942
+ noteId,
2943
+ expectedVersion,
2944
+ reason,
2945
+ }),
2946
+ );
2947
+ },
2948
+ );
2949
+ }
2950
+
2326
2951
  function registerPreferenceRegistryTools(
2327
2952
  server: McpServer,
2328
2953
  deps: ApiRouteDeps,
@@ -2464,6 +3089,7 @@ function registerMemoryTools(
2464
3089
  grant: AccessGrant,
2465
3090
  sessionId: string,
2466
3091
  json: JsonResult,
3092
+ promptMode: WorkspaceMemoryPromptMode,
2467
3093
  ): void {
2468
3094
  const publicationActor = async () => {
2469
3095
  const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
@@ -2479,7 +3105,10 @@ function registerMemoryTools(
2479
3105
  server.registerTool(
2480
3106
  "memory_search",
2481
3107
  {
2482
- description: MEMORY_SEARCH_TOOL_DESCRIPTION,
3108
+ description:
3109
+ promptMode === "retrieval_only"
3110
+ ? `${MEMORY_SEARCH_TOOL_DESCRIPTION} Legacy preference-kind records are excluded here because structured preferences are the only behavioral authority.`
3111
+ : MEMORY_SEARCH_TOOL_DESCRIPTION,
2483
3112
  inputSchema: {
2484
3113
  query: z4.string().min(1),
2485
3114
  kind: MemoryKindSchema.optional(),
@@ -2495,6 +3124,7 @@ function registerMemoryTools(
2495
3124
  query,
2496
3125
  ...(kind ? { kind } : {}),
2497
3126
  ...(limit ? { limit } : {}),
3127
+ agentPromptMode: promptMode,
2498
3128
  },
2499
3129
  deps.getDocumentServices().embedder,
2500
3130
  ),
@@ -2504,7 +3134,10 @@ function registerMemoryTools(
2504
3134
  server.registerTool(
2505
3135
  "memory_save",
2506
3136
  {
2507
- description: MEMORY_SAVE_TOOL_DESCRIPTION,
3137
+ description:
3138
+ promptMode === "retrieval_only"
3139
+ ? `${MEMORY_SAVE_TOOL_DESCRIPTION} A preference-kind save is retained only as a legacy observation and cannot become behavioral authority; use the structured preference proposal surface for behavioral guidance.`
3140
+ : MEMORY_SAVE_TOOL_DESCRIPTION,
2508
3141
  inputSchema: {
2509
3142
  text: z4.string().min(1),
2510
3143
  kind: MemoryKindSchema,
@@ -3201,6 +3834,7 @@ function registerWorkspaceOrchestrationTools(
3201
3834
  grant: AccessGrant,
3202
3835
  can: (permission: Permission) => boolean,
3203
3836
  callerSessionId: string | null,
3837
+ sessionCreateVisible: boolean,
3204
3838
  json: JsonResult,
3205
3839
  ): void {
3206
3840
  if (can("sessions:read")) {
@@ -3400,135 +4034,94 @@ function registerWorkspaceOrchestrationTools(
3400
4034
  );
3401
4035
  }
3402
4036
 
3403
- if (can("sessions:create")) {
4037
+ if (can("sessions:create") && sessionCreateVisible) {
4038
+ const sessionCreateInput = z4
4039
+ .object({
4040
+ initialMessage: z4.string().min(1),
4041
+ instructions: z4.string().min(1).max(32768).optional(),
4042
+ goal: z4.unknown().optional(),
4043
+ resources: z4.array(z4.unknown()).optional(),
4044
+ tools: z4.array(z4.unknown()).optional(),
4045
+ mcpServers: z4.array(z4.unknown()).optional(),
4046
+ variableSetId: z4.string().uuid().optional(),
4047
+ environmentId: z4.string().uuid().optional(),
4048
+ rigId: z4.string().uuid().optional(),
4049
+ model: z4
4050
+ .string()
4051
+ .min(1)
4052
+ .optional()
4053
+ .describe(
4054
+ "Model for the worker. Omit to inherit the exact calling turn's model, including its Codex subscription billing path.",
4055
+ ),
4056
+ reasoningEffort: z4
4057
+ .string()
4058
+ .optional()
4059
+ .describe("Omit to inherit the exact calling turn's reasoning effort."),
4060
+ latencyMode: z4
4061
+ .enum(["standard", "priority", "fast"])
4062
+ .optional()
4063
+ .describe("Omit to inherit the exact calling turn's latency mode."),
4064
+ sandboxBackend: z4.string().optional(),
4065
+ // Model-only structural coupling: workingDir cannot exist without a
4066
+ // targetSandboxId because both live inside one optional object. The
4067
+ // handler maps this back to the stable public REST/SDK request fields.
4068
+ machineTarget: z4
4069
+ .object({
4070
+ targetSandboxId: z4.string().uuid(),
4071
+ workingDir: z4.string().optional(),
4072
+ })
4073
+ .strict()
4074
+ .optional(),
4075
+ metadata: z4.record(z4.string(), z4.unknown()).optional(),
4076
+ idempotencyKey: z4.string().min(1).max(200).optional(),
4077
+ firstPartyMcpPermissions: z4
4078
+ .array(z4.string())
4079
+ .optional()
4080
+ .describe(
4081
+ "Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly.",
4082
+ ),
4083
+ firstPartyMcpTools: z4
4084
+ .array(z4.enum(FIRST_PARTY_MCP_TOOL_NAMES))
4085
+ .optional()
4086
+ .describe(
4087
+ "Exact model-visible first-party tool selection for the child. Omit to inherit this session's effective selection. To create a non-delegating leaf, provide a selection that omits session_create. This does not grant permissions.",
4088
+ ),
4089
+ // Omission is the ordinary safe sharing path. Literal "shared" remains
4090
+ // available to advanced REST/SDK callers but is intentionally absent
4091
+ // from the model surface because it turns compatibility drift into a
4092
+ // deterministic failure instead of the omission path's safe own-box fallback.
4093
+ sandbox: z4
4094
+ .union([z4.literal("new"), z4.object({ groupId: z4.string().uuid() })])
4095
+ .optional(),
4096
+ })
4097
+ .strict();
3404
4098
  server.registerTool(
3405
4099
  "session_create",
3406
4100
  {
3407
4101
  description:
3408
- "Spawn a new agent session (a worker) with an initial message and optional goal, resources (e.g. repositories from github_repositories_list), tools, and variable set attachment. VariableSet attachment happens at creation only it cannot be added to a running session and requires the variable-sets:use permission. When targetSandboxId names a machine, workingDir sets the working directory (cwd) the spawned session runs under on that machine.",
3409
- inputSchema: {
3410
- initialMessage: z4.string().min(1),
3411
- // Per-session agent persona/system instructions for the spawned worker
3412
- // (a per-agent-type prompt). Delivered system-level, composed AFTER the
3413
- // workspace persona; never shown in the worker's timeline. Trimmed,
3414
- // non-empty, max 32768 chars (re-validated by the contracts schema).
3415
- instructions: z4.string().min(1).max(32768).optional(),
3416
- goal: z4.unknown().optional(),
3417
- resources: z4.array(z4.unknown()).optional(),
3418
- tools: z4.array(z4.unknown()).optional(),
3419
- // Per-session third-party MCP servers. Credential header values are
3420
- // accepted only at create and never appear in responses/events.
3421
- mcpServers: z4.array(z4.unknown()).optional(),
3422
- variableSetId: z4.string().uuid().optional(),
3423
- // Deprecated alias of variableSetId (rename back-compat); declared so MCP
3424
- // validation doesn't strip it before createSessionForRequest maps it.
3425
- environmentId: z4.string().uuid().optional(),
3426
- // Bind the spawned session to a rig (freezes its active version);
3427
- // declared so MCP validation doesn't strip it before the domain reads it.
3428
- rigId: z4.string().uuid().optional(),
3429
- model: z4
3430
- .string()
3431
- .min(1)
3432
- .optional()
3433
- .describe(
3434
- "Model for the worker. Omit to inherit the exact calling turn's model, including its Codex subscription billing path.",
3435
- ),
3436
- reasoningEffort: z4
3437
- .string()
3438
- .optional()
3439
- .describe("Omit to inherit the exact calling turn's reasoning effort."),
3440
- latencyMode: z4
3441
- .enum(["standard", "priority", "fast"])
3442
- .optional()
3443
- .describe("Omit to inherit the exact calling turn's latency mode."),
3444
- sandboxBackend: z4.string().optional(),
3445
- // Create-time machine targeting: an enrolled sandbox id (from
3446
- // sandboxes_list) to run the spawned session on. Seeds the active-sandbox
3447
- // pointer at creation so the FIRST turn lands on the chosen machine
3448
- // (race-free). Ownership + liveness are validated in the domain via the
3449
- // same path as sandbox_swap; an unowned/offline/unknown target 422s.
3450
- targetSandboxId: z4.string().uuid().optional(),
3451
- // The working directory (cwd) for a machine target: the path/cwd base the
3452
- // spawned session's agent exec, terminal, and file dock run under. A
3453
- // workspace_root-relative subdir or an absolute machine path. Only valid
3454
- // WITH targetSandboxId (workingDir alone 422s); omitted ⇒ workspace_root.
3455
- workingDir: z4.string().optional(),
3456
- metadata: z4.record(z4.string(), z4.unknown()).optional(),
3457
- // Workspace-scoped CREATE idempotency key: a retried session_create with
3458
- // the same key returns the already-spawned worker instead of a duplicate.
3459
- idempotencyKey: z4.string().min(1).max(200).optional(),
3460
- // Per-session/agent descendant policy. Reductions need only create;
3461
- // increases are authorized server-side with workspace:admin.
3462
- maxNestedAgentDepth: z4.number().int().nonnegative().optional(),
3463
- // First-party MCP token permissions for the spawned session; every
3464
- // permission must be held by this grant (validated in the domain).
3465
- // A goal requires goals:manage in the resulting set; it is never
3466
- // silently added beyond the inherited or explicit authority.
3467
- firstPartyMcpPermissions: z4
3468
- .array(z4.string())
3469
- .optional()
3470
- .describe(
3471
- "Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly.",
3472
- ),
3473
- firstPartyMcpTools: z4
3474
- .array(z4.enum(FIRST_PARTY_MCP_TOOL_NAMES))
3475
- .optional()
3476
- .describe(
3477
- "Exact model-visible first-party tool selection for the child. Omit to inherit this session's effective selection. This does not grant permissions.",
3478
- ),
3479
- // Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
3480
- // creator's box — one filesystem/repo/desktop, N independent conversations;
3481
- // this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
3482
- // repo set or a genuinely separate filesystem), or {groupId} (a sibling
3483
- // session's `sandboxGroupId` from a prior session_create response) to join
3484
- // that specific sibling's box.
3485
- // Shared state must be compatible: a shared box requires the SAME image
3486
- // (rejected at the lease layer, B3) and — because the box's variable set is
3487
- // fixed at creation under the current mechanics — the SAME workspace
3488
- // VariableSet. The domain layer is env-aware: an inherited default with a
3489
- // different variableSetId silently gets its OWN box (the spawn still works),
3490
- // while an explicit shared/{groupId} with a mismatched variableSet 422s at
3491
- // create. When the VariableSet is eventually evicted from the box manifest
3492
- // (per-exec, like the git token), the env check dissolves on its own.
3493
- // The description below is what the AGENT sees (this comment is invisible to
3494
- // it); keep the two in sync.
3495
- sandbox: z4
3496
- .union([
3497
- z4.literal("shared"),
3498
- z4.literal("new"),
3499
- z4.object({ groupId: z4.string().uuid() }),
3500
- ])
3501
- .describe(
3502
- "Sandbox placement. OMIT (default) to SHARE the creator's box — one filesystem/repo/desktop, N independent conversations; this is the safe default. If the new session attaches a DIFFERENT variableSet than the creator's box, the platform automatically gives it its own box (the box variable set is fixed at creation), so omitting stays safe. Pass 'new' for a fresh isolated box (different repo set or a genuinely separate filesystem). Pass {groupId} to join a specific sibling's box — requires the same variableSet (a mismatch is rejected at create) and the same image (a conflicting image is rejected when the box warms).",
3503
- )
3504
- .optional(),
3505
- // The parent (manager) session is auto-inferred from the caller's
3506
- // worker-signed sessionId claim, so a spawned worker's completion wakes
3507
- // its manager automatically. There is deliberately no caller-supplied
3508
- // parent parameter: it would let a sessions:create grant target an
3509
- // arbitrary session's wake channel without sessions:control on it.
3510
- },
4102
+ "Spawn a new agent session (a worker). Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set or Rig gets its own compatible box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
4103
+ inputSchema: sessionCreateInput,
3511
4104
  },
3512
4105
  async (args) => {
3513
4106
  try {
3514
4107
  if (callerSessionId !== null) {
3515
4108
  await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
3516
4109
  }
3517
- const result = await createSessionForRequestWithOutcome(
3518
- deps,
3519
- grant,
3520
- grant.workspaceId,
3521
- args,
3522
- );
3523
- return json(sessionCreateMutationReceipt(result, Boolean(args.idempotencyKey)));
4110
+ const { machineTarget, ...request } = args;
4111
+ const result = await createSessionForRequestWithOutcome(deps, grant, grant.workspaceId, {
4112
+ ...request,
4113
+ ...(machineTarget
4114
+ ? {
4115
+ targetSandboxId: machineTarget.targetSandboxId,
4116
+ ...(machineTarget.workingDir !== undefined
4117
+ ? { workingDir: machineTarget.workingDir }
4118
+ : {}),
4119
+ }
4120
+ : {}),
4121
+ });
4122
+ return json(sessionCreateMutationReceipt(result, Boolean(request.idempotencyKey)));
3524
4123
  } catch (error) {
3525
- if (error instanceof SessionSpawnDeniedError) {
3526
- return {
3527
- ...json(sessionSpawnDenialEnvelope(error)),
3528
- isError: true,
3529
- };
3530
- }
3531
- throw error;
4124
+ return orchestrationFailureResult("session_create", error);
3532
4125
  }
3533
4126
  },
3534
4127
  );
@@ -3550,81 +4143,87 @@ function registerWorkspaceOrchestrationTools(
3550
4143
  },
3551
4144
  },
3552
4145
  async ({ sessionId: targetSessionId, text, idempotencyKey, mcpCredentialUpdates }) => {
3553
- await authorizeFirstPartySession(deps, grant, targetSessionId, "session.append");
3554
- if (callerSessionId !== null) {
3555
- if ((mcpCredentialUpdates?.length ?? 0) > 0) {
3556
- throw new Error("internal session updates cannot change MCP credentials");
4146
+ try {
4147
+ await authorizeFirstPartySession(deps, grant, targetSessionId, "session.append");
4148
+ if (callerSessionId !== null) {
4149
+ if ((mcpCredentialUpdates?.length ?? 0) > 0) {
4150
+ throw new HTTPException(422, {
4151
+ message: "internal session updates cannot change MCP credentials",
4152
+ });
4153
+ }
4154
+ const result = await sendAgentSessionMessage(
4155
+ deps,
4156
+ exactAgentCommandContext(grant, callerSessionId),
4157
+ { targetSessionId, text, idempotencyKey },
4158
+ );
4159
+ return json(
4160
+ mcpMutationReceipt({
4161
+ operation: "session_send_message",
4162
+ committed: true,
4163
+ outcome: result.replay ? "replayed" : "accepted",
4164
+ changed: !result.replay,
4165
+ resource: {
4166
+ type: "session_system_update",
4167
+ id: result.updateId,
4168
+ state: result.effectiveState,
4169
+ },
4170
+ relatedResources: [{ type: "session", id: targetSessionId }],
4171
+ timestamp: result.receipt.createdAt.toISOString(),
4172
+ idempotency: { status: result.replay ? "replayed" : "applied" },
4173
+ facts: {
4174
+ delivery: "coalesced_internal_update",
4175
+ wakeRequested: result.wakeRevision !== null,
4176
+ resumeRequired: result.effectiveState === "paused",
4177
+ },
4178
+ nextAction: {
4179
+ tool: "session_get",
4180
+ arguments: { sessionId: targetSessionId },
4181
+ },
4182
+ }),
4183
+ );
3557
4184
  }
3558
- const result = await sendAgentSessionMessage(
4185
+ const { accepted, turn, replay } = await acceptSessionUserMessageWithOutcome(
3559
4186
  deps,
3560
- exactAgentCommandContext(grant, callerSessionId),
3561
- { targetSessionId, text, idempotencyKey },
4187
+ grant,
4188
+ grant.workspaceId,
4189
+ targetSessionId,
4190
+ {
4191
+ text,
4192
+ delivery: "send",
4193
+ origin: "operator",
4194
+ clientEventId: idempotencyKey,
4195
+ mcpCredentialUpdates: (mcpCredentialUpdates ?? []).map((update) =>
4196
+ SessionMcpCredentialUpdateInput.parse(update),
4197
+ ),
4198
+ },
3562
4199
  );
3563
4200
  return json(
3564
4201
  mcpMutationReceipt({
3565
4202
  operation: "session_send_message",
3566
4203
  committed: true,
3567
- outcome: result.replay ? "replayed" : "accepted",
3568
- changed: !result.replay,
4204
+ outcome: replay ? "replayed" : "accepted",
4205
+ changed: !replay,
3569
4206
  resource: {
3570
- type: "session_system_update",
3571
- id: result.updateId,
3572
- state: result.effectiveState,
3573
- },
3574
- relatedResources: [{ type: "session", id: targetSessionId }],
3575
- timestamp: result.receipt.createdAt.toISOString(),
3576
- idempotency: { status: result.replay ? "replayed" : "applied" },
3577
- facts: {
3578
- delivery: "coalesced_internal_update",
3579
- wakeRequested: result.wakeRevision !== null,
3580
- resumeRequired: result.effectiveState === "paused",
4207
+ type: "session_turn",
4208
+ id: turn.id,
4209
+ version: turn.version,
4210
+ state: turn.status,
3581
4211
  },
4212
+ relatedResources: [
4213
+ { type: "session", id: targetSessionId },
4214
+ { type: "session_event", id: accepted.id, state: accepted.type },
4215
+ ],
4216
+ timestamp: accepted.occurredAt,
4217
+ idempotency: { status: replay ? "replayed" : "applied" },
3582
4218
  nextAction: {
3583
4219
  tool: "session_get",
3584
4220
  arguments: { sessionId: targetSessionId },
3585
4221
  },
3586
4222
  }),
3587
4223
  );
4224
+ } catch (error) {
4225
+ return orchestrationFailureResult("session_send_message", error);
3588
4226
  }
3589
- const { accepted, turn, replay } = await acceptSessionUserMessageWithOutcome(
3590
- deps,
3591
- grant,
3592
- grant.workspaceId,
3593
- targetSessionId,
3594
- {
3595
- text,
3596
- delivery: "send",
3597
- origin: "operator",
3598
- clientEventId: idempotencyKey,
3599
- mcpCredentialUpdates: (mcpCredentialUpdates ?? []).map((update) =>
3600
- SessionMcpCredentialUpdateInput.parse(update),
3601
- ),
3602
- },
3603
- );
3604
- return json(
3605
- mcpMutationReceipt({
3606
- operation: "session_send_message",
3607
- committed: true,
3608
- outcome: replay ? "replayed" : "accepted",
3609
- changed: !replay,
3610
- resource: {
3611
- type: "session_turn",
3612
- id: turn.id,
3613
- version: turn.version,
3614
- state: turn.status,
3615
- },
3616
- relatedResources: [
3617
- { type: "session", id: targetSessionId },
3618
- { type: "session_event", id: accepted.id, state: accepted.type },
3619
- ],
3620
- timestamp: accepted.occurredAt,
3621
- idempotency: { status: replay ? "replayed" : "applied" },
3622
- nextAction: {
3623
- tool: "session_get",
3624
- arguments: { sessionId: targetSessionId },
3625
- },
3626
- }),
3627
- );
3628
4227
  },
3629
4228
  );
3630
4229
 
@@ -3661,10 +4260,16 @@ function registerWorkspaceOrchestrationTools(
3661
4260
  committed: true,
3662
4261
  outcome: controlled.replay ? "replayed" : "updated",
3663
4262
  changed: !controlled.replay,
3664
- resource: { type: "session", id: sessionId, state: effectiveControl.state },
4263
+ resource: {
4264
+ type: "session",
4265
+ id: sessionId,
4266
+ state: effectiveControl.state,
4267
+ },
3665
4268
  relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
3666
4269
  timestamp: controlled.receipt.createdAt.toISOString(),
3667
- idempotency: { status: controlled.replay ? "replayed" : "applied" },
4270
+ idempotency: {
4271
+ status: controlled.replay ? "replayed" : "applied",
4272
+ },
3668
4273
  facts: { interruptionCount: controlled.interruptionCount },
3669
4274
  nextAction: { tool: "session_get", arguments: { sessionId } },
3670
4275
  }),
@@ -3723,10 +4328,16 @@ function registerWorkspaceOrchestrationTools(
3723
4328
  committed: true,
3724
4329
  outcome: controlled.replay ? "replayed" : "updated",
3725
4330
  changed: !controlled.replay,
3726
- resource: { type: "session", id: sessionId, state: effectiveControl.state },
4331
+ resource: {
4332
+ type: "session",
4333
+ id: sessionId,
4334
+ state: effectiveControl.state,
4335
+ },
3727
4336
  relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
3728
4337
  timestamp: controlled.receipt.createdAt.toISOString(),
3729
- idempotency: { status: controlled.replay ? "replayed" : "applied" },
4338
+ idempotency: {
4339
+ status: controlled.replay ? "replayed" : "applied",
4340
+ },
3730
4341
  facts: { interruptionCount: controlled.interruptionCount },
3731
4342
  nextAction: { tool: "session_get", arguments: { sessionId } },
3732
4343
  }),
@@ -4222,7 +4833,11 @@ async function capabilitySetupProjection(
4222
4833
  }
4223
4834
  const installations = await listWorkspaceGitHubInstallationBindings(deps, workspaceId);
4224
4835
  if (githubBindingStatus(true, installations) === "bound") {
4225
- return { status: "ready", action: null, detail: "GitHub is connected and ready." };
4836
+ return {
4837
+ status: "ready",
4838
+ action: null,
4839
+ detail: "GitHub is connected and ready.",
4840
+ };
4226
4841
  }
4227
4842
  return {
4228
4843
  status: "authorization_required",
@@ -4232,7 +4847,11 @@ async function capabilitySetupProjection(
4232
4847
  }
4233
4848
  if (item.surfaceType === "codex_apps") {
4234
4849
  return item.enabled
4235
- ? { status: "ready", action: null, detail: "Codex Apps is connected and ready." }
4850
+ ? {
4851
+ status: "ready",
4852
+ action: null,
4853
+ detail: "Codex Apps is connected and ready.",
4854
+ }
4236
4855
  : {
4237
4856
  status: "authorization_required",
4238
4857
  action: "connect",
@@ -4240,7 +4859,11 @@ async function capabilitySetupProjection(
4240
4859
  };
4241
4860
  }
4242
4861
  if (item.enabled) {
4243
- return { status: "ready", action: null, detail: "This capability is already enabled." };
4862
+ return {
4863
+ status: "ready",
4864
+ action: null,
4865
+ detail: "This capability is already enabled.",
4866
+ };
4244
4867
  }
4245
4868
  if (!item.runtime.available) {
4246
4869
  return {