@opengeni/api-router 0.22.2 → 0.23.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 (44) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth.d.ts +0 -30
  3. package/dist/{chunk-HWXJW5C7.js → chunk-T4T2PGU4.js} +3036 -1362
  4. package/dist/chunk-T4T2PGU4.js.map +1 -0
  5. package/dist/http/sse.d.ts +2 -0
  6. package/dist/index.js +29 -9
  7. package/dist/index.js.map +1 -1
  8. package/dist/integrations/oauth-client.d.ts +8 -0
  9. package/dist/integrations/slack-interactions.d.ts +7 -1
  10. package/dist/mcp/receipts.d.ts +28 -0
  11. package/dist/mcp/scheduled-task-view.d.ts +350 -0
  12. package/dist/mcp/toolspace.d.ts +9 -0
  13. package/dist/sandbox/auth-callout.d.ts +2 -0
  14. package/dist/sandbox/channel-a.d.ts +5 -1
  15. package/package.json +12 -12
  16. package/src/app.ts +3 -4
  17. package/src/auth/managed-auth.ts +0 -16
  18. package/src/http/sse.ts +101 -6
  19. package/src/index.ts +28 -3
  20. package/src/integrations/oauth-client.ts +36 -56
  21. package/src/integrations/slack-interactions.ts +123 -15
  22. package/src/mcp/documents.ts +42 -25
  23. package/src/mcp/receipts.ts +95 -0
  24. package/src/mcp/scheduled-task-view.ts +608 -0
  25. package/src/mcp/server.ts +812 -182
  26. package/src/mcp/toolspace.ts +75 -71
  27. package/src/observability.ts +3 -3
  28. package/src/routes/api-keys.ts +7 -1
  29. package/src/routes/codex.ts +7 -4
  30. package/src/routes/connections.ts +74 -3
  31. package/src/routes/enrollments.ts +54 -12
  32. package/src/routes/environments.ts +60 -11
  33. package/src/routes/files.ts +175 -65
  34. package/src/routes/install.ts +31 -1
  35. package/src/routes/machines.ts +1 -1
  36. package/src/routes/scheduled-tasks.ts +39 -14
  37. package/src/routes/sessions.ts +77 -12
  38. package/src/routes/transcription-recordings.ts +65 -33
  39. package/src/sandbox/auth-callout.ts +16 -4
  40. package/src/sandbox/channel-a.ts +124 -7
  41. package/src/sandbox/enrollment.ts +13 -3
  42. package/src/sandbox/machines.ts +1 -1
  43. package/src/sandbox/viewer.ts +29 -20
  44. package/dist/chunk-HWXJW5C7.js.map +0 -1
package/src/mcp/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import {
3
3
  CreateScheduledTaskRequest,
4
4
  DEFAULT_FIRST_PARTY_MCP_TOOLS,
@@ -24,6 +24,7 @@ import {
24
24
  type SessionAuthorizationOperation,
25
25
  type SessionAuthorizationSurface,
26
26
  type Session,
27
+ type ScheduledTask,
27
28
  UpdateScheduledTaskRequest,
28
29
  normalizeWorkspaceArtifactSlug,
29
30
  WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES,
@@ -33,6 +34,7 @@ import {
33
34
  countVariableSets,
34
35
  beginRigChangeVerificationAttempt,
35
36
  createVariableSet,
37
+ decryptVariableSetValue,
36
38
  deleteScheduledTask,
37
39
  encryptVariableSetValue,
38
40
  getSession,
@@ -56,6 +58,8 @@ import {
56
58
  listRigVersionMonitoringSummaries,
57
59
  listSocialPosts,
58
60
  recordAuditEvent,
61
+ removeEnrollment,
62
+ readVariableSetSecretAtomically,
59
63
  recordSyncedSocialPosts,
60
64
  listVariableSets,
61
65
  MEMORY_CORRECT_TOOL_DESCRIPTION,
@@ -95,8 +99,10 @@ import type { AnySchema, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/ser
95
99
  import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
96
100
  import * as z4 from "zod/v4";
97
101
  import {
102
+ hasLiteralPermission,
98
103
  hasPermission,
99
104
  authorizedSocialConnectionsForGrant,
105
+ requireLiveAgentAttemptAuthorization,
100
106
  requireSessionAuthorization,
101
107
  requireSessionAuthorizationListScope,
102
108
  type ResolvedSessionAuthorization,
@@ -132,17 +138,21 @@ import {
132
138
  createValidatedScheduledTask,
133
139
  manualScheduledTaskTriggerUsageKey,
134
140
  manualScheduledTaskTriggerWorkflowId,
141
+ scheduledTaskForGrant,
142
+ scheduledTaskRunForGrant,
135
143
  scheduledTaskToolsProvided,
136
144
  scheduledTaskTriggerToken,
145
+ ScheduledTaskSyncError,
137
146
  syncCreatedScheduledTask,
138
147
  syncUpdatedScheduledTask,
148
+ validateScheduledTaskTarget,
139
149
  validatedScheduledTaskUpdate,
140
150
  } from "@opengeni/core";
141
151
  import {
142
- acceptSessionUserMessage,
152
+ acceptSessionUserMessageWithOutcome,
143
153
  controlAgentSessionWorkstream,
144
154
  controlHumanSessionWorkstream,
145
- createSessionForRequest,
155
+ createSessionForRequestWithOutcome,
146
156
  SessionSpawnDeniedError,
147
157
  sessionSpawnDenialEnvelope,
148
158
  sendAgentSessionMessage,
@@ -170,6 +180,12 @@ import {
170
180
  boundRigDetailMcp,
171
181
  SESSION_EVENT_MCP_MAX_BYTES,
172
182
  } from "./session-view";
183
+ import { mcpMutationReceipt, sessionCreateMutationReceipt } from "./receipts";
184
+ import {
185
+ boundScheduledTaskDetailMcp,
186
+ boundScheduledTaskMcpPage,
187
+ scheduledTaskMcpSummary,
188
+ } from "./scheduled-task-view";
173
189
  import type { ToolspaceMcpSurface } from "./toolspace";
174
190
  import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
175
191
  import {
@@ -205,13 +221,17 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
205
221
  memory_search: { sessionRequired: true, allOf: ["documents:search"] },
206
222
  memory_save: { sessionRequired: true, allOf: ["documents:search"] },
207
223
  memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
208
- preference_registry_summary: { sessionRequired: true, allOf: ["workspace:read"] },
224
+ preference_registry_summary: {
225
+ sessionRequired: true,
226
+ allOf: ["workspace:read"],
227
+ },
209
228
  preference_registry_get: { sessionRequired: true, allOf: ["workspace:read"] },
210
229
  sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
211
230
  sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
212
231
  sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
213
232
  run_on: { sessionRequired: true, allOf: ["sessions:control"] },
214
233
  sandbox_provision: { sessionRequired: true, allOf: ["sessions:control"] },
234
+ connected_machine_remove: { allOf: ["enrollments:manage"] },
215
235
  rig_list: { allOf: ["rigs:use"] },
216
236
  rig_get: { allOf: ["rigs:use"] },
217
237
  rig_propose_change: { allOf: ["rigs:use"] },
@@ -226,10 +246,16 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
226
246
  session_resume: { allOf: ["sessions:control"] },
227
247
  session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
228
248
  set_other_session_title: { allOf: ["sessions:control"] },
229
- variable_set_list: { allOf: ["variable-sets:use"] },
230
- environment_list: { allOf: ["variable-sets:use"] },
231
- variable_set_set_variable: { allOf: ["variable-sets:manage"] },
232
- environment_set_variable: { allOf: ["variable-sets:manage"] },
249
+ variable_set_list: { allOf: ["variable-sets:list", "secrets:list"] },
250
+ environment_list: { allOf: ["variable-sets:list", "secrets:list"] },
251
+ variable_set_get_variable: {
252
+ sessionRequired: true,
253
+ allOf: ["variable-sets:read", "secrets:read"],
254
+ },
255
+ variable_set_set_variable: {
256
+ allOf: ["variable-sets:write", "secrets:write"],
257
+ },
258
+ environment_set_variable: { allOf: ["variable-sets:write", "secrets:write"] },
233
259
  github_connect_link: { allOf: ["github:use"] },
234
260
  github_repositories_list: { allOf: ["github:use"] },
235
261
  social_connections_list: { allOf: ["connections:read"] },
@@ -244,15 +270,21 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
244
270
  // Publishes under the user's identity: connections:write keeps it out of the
245
271
  // default agent permission set, unlike the read-only social tools above.
246
272
  social_post_reply: { allOf: ["connections:write"] },
247
- scheduled_tasks_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
248
- scheduled_tasks_get: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
273
+ scheduled_tasks_list: {
274
+ anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
275
+ },
276
+ scheduled_tasks_get: {
277
+ anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
278
+ },
249
279
  scheduled_tasks_create: { allOf: ["scheduled_tasks:manage"] },
250
280
  scheduled_tasks_update: { allOf: ["scheduled_tasks:manage"] },
251
281
  scheduled_tasks_pause: { allOf: ["scheduled_tasks:manage"] },
252
282
  scheduled_tasks_resume: { allOf: ["scheduled_tasks:manage"] },
253
283
  scheduled_tasks_trigger: { allOf: ["scheduled_tasks:run"] },
254
284
  scheduled_tasks_delete: { allOf: ["scheduled_tasks:manage"] },
255
- scheduled_task_runs_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
285
+ scheduled_task_runs_list: {
286
+ anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
287
+ },
256
288
  slack_bot_list_channels: { allOf: ["connections:read"] },
257
289
  slack_bot_channel_history: { allOf: ["connections:read"] },
258
290
  slack_bot_thread_replies: { allOf: ["connections:read"] },
@@ -435,6 +467,9 @@ export function buildOpenGeniMcpServer(
435
467
  if (!toolspaceMode && sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
436
468
  registerFleetTools(server, deps, grant, sessionId, json);
437
469
  }
470
+ if (!toolspaceMode && can("enrollments:manage") && deps.settings.sandboxSelfhostedEnabled) {
471
+ registerConnectedMachineTools(server, deps, grant, json);
472
+ }
438
473
  if (!toolspaceMode) {
439
474
  registerRigTools(server, deps, grant, can, sessionId, json);
440
475
  registerSlackBotTools(server, deps, grant, sessionId, json);
@@ -452,7 +487,7 @@ export function buildOpenGeniMcpServer(
452
487
  // session by setting a narrower session.firstPartyMcpPermissions (capped to
453
488
  // the creator's own grant); operators still cap what any session can be given.
454
489
  registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, toolspaceMode, json);
455
- registerVariableSetTools(server, deps, grant, can, json);
490
+ registerVariableSetTools(server, deps, grant, can, sessionId, toolspaceMode, json);
456
491
  if (can("github:use")) {
457
492
  registerGitHubConnectTool(server, deps, grant, options, json);
458
493
  }
@@ -620,10 +655,17 @@ export function buildOpenGeniMcpServer(
620
655
  const authority = await requireAuthorizedSocialConnection(connectionId);
621
656
  const result = await socialSearchLive(
622
657
  deps,
623
- { workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
658
+ {
659
+ workspaceId: grant.workspaceId,
660
+ connectionId,
661
+ subjectId: authority.subjectId,
662
+ },
624
663
  { query, subreddit, limit },
625
664
  );
626
- return json({ provider: result.connection.provider, posts: result.posts });
665
+ return json({
666
+ provider: result.connection.provider,
667
+ posts: result.posts,
668
+ });
627
669
  },
628
670
  );
629
671
 
@@ -642,10 +684,17 @@ export function buildOpenGeniMcpServer(
642
684
  const authority = await requireAuthorizedSocialConnection(connectionId);
643
685
  const result = await socialMentionsLive(
644
686
  deps,
645
- { workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
687
+ {
688
+ workspaceId: grant.workspaceId,
689
+ connectionId,
690
+ subjectId: authority.subjectId,
691
+ },
646
692
  { sinceId, limit },
647
693
  );
648
- return json({ provider: result.connection.provider, posts: result.posts });
694
+ return json({
695
+ provider: result.connection.provider,
696
+ posts: result.posts,
697
+ });
649
698
  },
650
699
  );
651
700
 
@@ -664,10 +713,17 @@ export function buildOpenGeniMcpServer(
664
713
  const authority = await requireAuthorizedSocialConnection(connectionId);
665
714
  const result = await socialThreadLive(
666
715
  deps,
667
- { workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
716
+ {
717
+ workspaceId: grant.workspaceId,
718
+ connectionId,
719
+ subjectId: authority.subjectId,
720
+ },
668
721
  { id, limit },
669
722
  );
670
- return json({ provider: result.connection.provider, posts: result.posts });
723
+ return json({
724
+ provider: result.connection.provider,
725
+ posts: result.posts,
726
+ });
671
727
  },
672
728
  );
673
729
  }
@@ -690,7 +746,11 @@ export function buildOpenGeniMcpServer(
690
746
  const authority = await requireAuthorizedSocialConnection(connectionId);
691
747
  const result = await socialOwnPostsLive(
692
748
  deps,
693
- { workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
749
+ {
750
+ workspaceId: grant.workspaceId,
751
+ connectionId,
752
+ subjectId: authority.subjectId,
753
+ },
694
754
  { limit },
695
755
  );
696
756
  // A post without a provider timestamp is skipped rather than recorded
@@ -736,7 +796,11 @@ export function buildOpenGeniMcpServer(
736
796
  const authority = await requireAuthorizedSocialConnection(connectionId);
737
797
  const result = await socialPostReply(
738
798
  deps,
739
- { workspaceId: grant.workspaceId, connectionId, subjectId: authority.subjectId },
799
+ {
800
+ workspaceId: grant.workspaceId,
801
+ connectionId,
802
+ subjectId: authority.subjectId,
803
+ },
740
804
  { inReplyToId, text },
741
805
  );
742
806
  // Outbound publishes leave a durable, secret-free receipt (house
@@ -769,22 +833,47 @@ export function buildOpenGeniMcpServer(
769
833
  server.registerTool(
770
834
  "scheduled_tasks_list",
771
835
  {
772
- description: "List scheduled tasks.",
773
- inputSchema: { limit: z4.number().int().positive().optional() },
836
+ description:
837
+ "List compact scheduled-task summaries. Prompts, goal text, resource/tool bodies, and metadata values are represented by byte/count facts; page with offset and use scheduled_tasks_get for a bounded explicit detail projection.",
838
+ inputSchema: {
839
+ limit: z4.number().int().positive().max(50).optional(),
840
+ offset: z4.number().int().nonnegative().max(10_000).optional(),
841
+ },
842
+ },
843
+ async ({ limit: requestedLimit, offset: requestedOffset }) => {
844
+ const limit = requestedLimit ?? 25;
845
+ const offset = requestedOffset ?? 0;
846
+ const rows = await listScheduledTasks(deps.db, grant.workspaceId, limit + 1, offset);
847
+ return json(
848
+ boundScheduledTaskMcpPage({
849
+ tasks: rows.slice(0, limit).map((task) => scheduledTaskForGrant(task, grant)),
850
+ limit,
851
+ offset,
852
+ sourceHasMore: rows.length > limit,
853
+ }),
854
+ );
774
855
  },
775
- async ({ limit }) =>
776
- json({
777
- tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100),
778
- }),
779
856
  );
780
857
 
781
858
  server.registerTool(
782
859
  "scheduled_tasks_get",
783
860
  {
784
- description: "Get one scheduled task.",
785
- inputSchema: { id: z4.string().uuid() },
861
+ description:
862
+ "Get one scheduled task. The default is the same compact summary used by scheduled_tasks_list; pass includeEntity=true for a bounded projection with an 8 KiB prompt preview, bounded goal fields, resource/tool identity previews, and metadata keys without values.",
863
+ inputSchema: {
864
+ id: z4.string().uuid(),
865
+ includeEntity: z4.boolean().optional(),
866
+ },
867
+ },
868
+ async ({ id, includeEntity }) => {
869
+ const task = scheduledTaskForGrant(
870
+ await requireScheduledTask(deps.db, grant.workspaceId, id),
871
+ grant,
872
+ );
873
+ return json(
874
+ includeEntity ? boundScheduledTaskDetailMcp(task) : scheduledTaskMcpSummary(task),
875
+ );
786
876
  },
787
- async ({ id }) => json(await requireScheduledTask(deps.db, grant.workspaceId, id)),
788
877
  );
789
878
 
790
879
  server.registerTool(
@@ -795,6 +884,7 @@ export function buildOpenGeniMcpServer(
795
884
  name: z4.string(),
796
885
  schedule: z4.unknown(),
797
886
  runMode: z4.string().optional(),
887
+ targetSessionId: z4.string().uuid().nullable().optional(),
798
888
  overlapPolicy: z4.string().optional(),
799
889
  agentConfig: z4.unknown(),
800
890
  status: z4.string().optional(),
@@ -823,13 +913,29 @@ export function buildOpenGeniMcpServer(
823
913
  grant,
824
914
  payload,
825
915
  toolsProvided: scheduledTaskToolsProvided(args),
916
+ sessionAuthorization: deps.sessionAuthorization,
917
+ authorizationSurface: "first_party_mcp",
826
918
  });
827
- await syncCreatedScheduledTask({
828
- db: deps.db,
829
- workflowClient: deps.workflowClient,
830
- task,
831
- });
832
- return json(task);
919
+ try {
920
+ await syncCreatedScheduledTask({
921
+ db: deps.db,
922
+ workflowClient: deps.workflowClient,
923
+ task,
924
+ });
925
+ } catch (error) {
926
+ if (!(error instanceof ScheduledTaskSyncError) || error.persistenceRestored) {
927
+ throw error;
928
+ }
929
+ return json(
930
+ scheduledTaskReceipt("scheduled_tasks_create", task, "partial_failure", true, {
931
+ partialFailure: { stage: "schedule_sync", retryable: true },
932
+ warnings: [
933
+ "The task database record committed, but Temporal schedule synchronization failed.",
934
+ ],
935
+ }),
936
+ );
937
+ }
938
+ return json(scheduledTaskReceipt("scheduled_tasks_create", task, "created", true));
833
939
  },
834
940
  );
835
941
 
@@ -842,6 +948,7 @@ export function buildOpenGeniMcpServer(
842
948
  name: z4.string().optional(),
843
949
  schedule: z4.unknown().optional(),
844
950
  runMode: z4.string().optional(),
951
+ targetSessionId: z4.string().uuid().nullable().optional(),
845
952
  overlapPolicy: z4.string().optional(),
846
953
  agentConfig: z4.unknown().optional(),
847
954
  status: z4.string().optional(),
@@ -867,6 +974,8 @@ export function buildOpenGeniMcpServer(
867
974
  existing,
868
975
  payload,
869
976
  toolsProvided: scheduledTaskToolsProvided(raw),
977
+ sessionAuthorization: deps.sessionAuthorization,
978
+ authorizationSurface: "first_party_mcp",
870
979
  });
871
980
  const task = await updateScheduledTask(deps.db, grant.workspaceId, id, update);
872
981
  await syncUpdatedScheduledTask({
@@ -875,7 +984,7 @@ export function buildOpenGeniMcpServer(
875
984
  previous,
876
985
  task,
877
986
  });
878
- return json(task);
987
+ return json(scheduledTaskForGrant(task, grant));
879
988
  },
880
989
  );
881
990
 
@@ -897,7 +1006,7 @@ export function buildOpenGeniMcpServer(
897
1006
  previous,
898
1007
  task,
899
1008
  });
900
- return json(task);
1009
+ return json(scheduledTaskForGrant(task, grant));
901
1010
  },
902
1011
  );
903
1012
 
@@ -919,7 +1028,7 @@ export function buildOpenGeniMcpServer(
919
1028
  previous,
920
1029
  task,
921
1030
  });
922
- return json(task);
1031
+ return json(scheduledTaskForGrant(task, grant));
923
1032
  },
924
1033
  );
925
1034
 
@@ -935,6 +1044,18 @@ export function buildOpenGeniMcpServer(
935
1044
  },
936
1045
  async ({ id, triggerId }) => {
937
1046
  const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
1047
+ await validateScheduledTaskTarget({
1048
+ db: deps.db,
1049
+ sessionAuthorization: deps.sessionAuthorization,
1050
+ authorizationSurface: "first_party_mcp",
1051
+ grant,
1052
+ targetSessionId: task.targetSessionId,
1053
+ runMode: task.runMode,
1054
+ variableSetId: task.variableSetId,
1055
+ rigId: task.rigId,
1056
+ agentConfig: task.agentConfig,
1057
+ missingTargetStatus: 404,
1058
+ });
938
1059
  await requireLimit(deps, {
939
1060
  accountId: grant.accountId,
940
1061
  workspaceId: grant.workspaceId,
@@ -955,18 +1076,36 @@ export function buildOpenGeniMcpServer(
955
1076
  triggerWorkflowId,
956
1077
  initiator: { kind: "subject", subjectId: grant.subjectId },
957
1078
  });
958
- await recordWorkspaceUsage(deps, {
959
- accountId: grant.accountId,
960
- workspaceId: grant.workspaceId,
961
- subjectId: grant.subjectId,
962
- eventType: "agent_run.created",
963
- quantity: 1,
964
- unit: "run",
965
- sourceResourceType: "scheduled_task",
966
- sourceResourceId: task.id,
967
- idempotencyKey: agentRunUsageIdempotencyKey,
968
- });
969
- return json(task);
1079
+ try {
1080
+ await recordWorkspaceUsage(deps, {
1081
+ accountId: grant.accountId,
1082
+ workspaceId: grant.workspaceId,
1083
+ subjectId: grant.subjectId,
1084
+ eventType: "agent_run.created",
1085
+ quantity: 1,
1086
+ unit: "run",
1087
+ sourceResourceType: "scheduled_task",
1088
+ sourceResourceId: task.id,
1089
+ idempotencyKey: agentRunUsageIdempotencyKey,
1090
+ });
1091
+ } catch {
1092
+ return json(
1093
+ scheduledTaskReceipt("scheduled_tasks_trigger", task, "partial_failure", true, {
1094
+ partialFailure: { stage: "usage_recording", retryable: true },
1095
+ warnings: [
1096
+ "The Temporal run trigger completed, but usage recording failed; retry with the same triggerId.",
1097
+ ],
1098
+ idempotencyStatus: triggerId ? "unknown" : "not_requested",
1099
+ facts: { triggerWorkflowId },
1100
+ }),
1101
+ );
1102
+ }
1103
+ return json(
1104
+ scheduledTaskReceipt("scheduled_tasks_trigger", task, "triggered", true, {
1105
+ idempotencyStatus: triggerId ? "unknown" : "not_requested",
1106
+ facts: { triggerWorkflowId },
1107
+ }),
1108
+ );
970
1109
  },
971
1110
  );
972
1111
 
@@ -981,8 +1120,28 @@ export function buildOpenGeniMcpServer(
981
1120
  await deps.workflowClient.deleteScheduledTaskSchedule({
982
1121
  temporalScheduleId: task.temporalScheduleId,
983
1122
  });
984
- await deleteScheduledTask(deps.db, grant.workspaceId, id);
985
- return json({ ok: true });
1123
+ try {
1124
+ await deleteScheduledTask(deps.db, grant.workspaceId, id);
1125
+ } catch {
1126
+ return json(
1127
+ scheduledTaskReceipt("scheduled_tasks_delete", task, "partial_failure", true, {
1128
+ partialFailure: { stage: "database_delete", retryable: true },
1129
+ warnings: [
1130
+ "The Temporal schedule was deleted, but the task database record remains.",
1131
+ ],
1132
+ }),
1133
+ );
1134
+ }
1135
+ return json(
1136
+ mcpMutationReceipt({
1137
+ operation: "scheduled_tasks_delete",
1138
+ committed: true,
1139
+ outcome: "deleted",
1140
+ changed: true,
1141
+ resource: { type: "scheduled_task", id: task.id, state: "deleted" },
1142
+ idempotency: { status: "not_supported" },
1143
+ }),
1144
+ );
986
1145
  },
987
1146
  );
988
1147
 
@@ -997,7 +1156,9 @@ export function buildOpenGeniMcpServer(
997
1156
  },
998
1157
  async ({ taskId, limit }) =>
999
1158
  json({
1000
- runs: await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100),
1159
+ runs: (await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100)).map(
1160
+ (run) => scheduledTaskRunForGrant(run, grant),
1161
+ ),
1001
1162
  }),
1002
1163
  );
1003
1164
  }
@@ -1441,10 +1602,27 @@ function registerGoalTools(
1441
1602
  event: { type: "goal.completed", evidence },
1442
1603
  },
1443
1604
  );
1605
+ const changed = events.length > 0;
1444
1606
  if (events.length > 0) {
1445
1607
  await deps.bus.publish(grant.workspaceId, sessionId, events);
1446
1608
  }
1447
- return json(goal);
1609
+ return json(
1610
+ mcpMutationReceipt({
1611
+ operation: "goal_complete",
1612
+ committed: true,
1613
+ outcome: changed ? "updated" : "unchanged",
1614
+ changed,
1615
+ resource: {
1616
+ type: "session_goal",
1617
+ id: goal.id,
1618
+ version: goal.version,
1619
+ state: goal.status,
1620
+ },
1621
+ timestamp: goal.updatedAt,
1622
+ idempotency: { status: "not_supported" },
1623
+ nextAction: { tool: "session_get", arguments: { sessionId } },
1624
+ }),
1625
+ );
1448
1626
  },
1449
1627
  );
1450
1628
 
@@ -1478,10 +1656,27 @@ function registerGoalTools(
1478
1656
  },
1479
1657
  },
1480
1658
  );
1659
+ const changed = events.length > 0;
1481
1660
  if (events.length > 0) {
1482
1661
  await deps.bus.publish(grant.workspaceId, sessionId, events);
1483
1662
  }
1484
- return json(goal);
1663
+ return json(
1664
+ mcpMutationReceipt({
1665
+ operation: "goal_pause",
1666
+ committed: true,
1667
+ outcome: changed ? "updated" : "unchanged",
1668
+ changed,
1669
+ resource: {
1670
+ type: "session_goal",
1671
+ id: goal.id,
1672
+ version: goal.version,
1673
+ state: goal.status,
1674
+ },
1675
+ timestamp: goal.updatedAt,
1676
+ idempotency: { status: "not_supported" },
1677
+ nextAction: { tool: "session_get", arguments: { sessionId } },
1678
+ }),
1679
+ );
1485
1680
  },
1486
1681
  );
1487
1682
  }
@@ -1599,7 +1794,11 @@ function registerWorkspaceArtifactTools(
1599
1794
  const actualHash = createHash("sha256").update(object.bytes).digest("hex");
1600
1795
  if (actualHash !== ref.version.contentSha256)
1601
1796
  throw new Error("Artifact content failed integrity verification");
1602
- return json({ detail, version: ref.version, html: new TextDecoder().decode(object.bytes) });
1797
+ return json({
1798
+ detail,
1799
+ version: ref.version,
1800
+ html: new TextDecoder().decode(object.bytes),
1801
+ });
1603
1802
  },
1604
1803
  );
1605
1804
 
@@ -1761,6 +1960,38 @@ function registerPreferenceRegistryTools(
1761
1960
 
1762
1961
  const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
1763
1962
 
1963
+ function scheduledTaskReceipt(
1964
+ operation: string,
1965
+ task: ScheduledTask,
1966
+ outcome: "created" | "updated" | "unchanged" | "triggered" | "partial_failure",
1967
+ changed: boolean,
1968
+ options: {
1969
+ partialFailure?: { stage: string; retryable: boolean };
1970
+ warnings?: string[];
1971
+ idempotencyStatus?: "not_supported" | "not_requested" | "applied" | "replayed" | "unknown";
1972
+ facts?: Record<string, string | number | boolean | null>;
1973
+ } = {},
1974
+ ) {
1975
+ return mcpMutationReceipt({
1976
+ operation,
1977
+ committed: true,
1978
+ outcome,
1979
+ changed,
1980
+ resource: {
1981
+ type: "scheduled_task",
1982
+ id: task.id,
1983
+ version: task.updatedAt,
1984
+ state: task.status,
1985
+ },
1986
+ timestamp: task.updatedAt,
1987
+ idempotency: { status: options.idempotencyStatus ?? "not_supported" },
1988
+ ...(options.partialFailure ? { partialFailure: options.partialFailure } : {}),
1989
+ ...(options.warnings ? { warnings: options.warnings } : {}),
1990
+ ...(options.facts ? { facts: options.facts } : {}),
1991
+ nextAction: { tool: "scheduled_tasks_get", arguments: { id: task.id } },
1992
+ });
1993
+ }
1994
+
1764
1995
  function memoryPreview(text: string): string {
1765
1996
  const normalized = text.replace(/\s+/g, " ").trim();
1766
1997
  return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
@@ -1836,7 +2067,46 @@ function registerMemoryTools(
1836
2067
  },
1837
2068
  },
1838
2069
  ]);
1839
- return json(result);
2070
+ const changed = !result.deduped || result.updated || result.superseded !== null;
2071
+ const outcome =
2072
+ result.updated || result.superseded !== null
2073
+ ? "updated"
2074
+ : result.deduped
2075
+ ? "unchanged"
2076
+ : "created";
2077
+ return json(
2078
+ mcpMutationReceipt({
2079
+ operation: "memory_save",
2080
+ committed: true,
2081
+ outcome,
2082
+ changed,
2083
+ resource: {
2084
+ type: "knowledge_memory",
2085
+ id: result.memory.id,
2086
+ state: result.memory.status,
2087
+ },
2088
+ relatedResources: result.superseded
2089
+ ? [
2090
+ {
2091
+ type: "knowledge_memory",
2092
+ id: result.superseded.id,
2093
+ state: result.superseded.status,
2094
+ },
2095
+ ]
2096
+ : undefined,
2097
+ timestamp: result.memory.updatedAt,
2098
+ idempotency: { status: "not_supported" },
2099
+ warnings: !result.embedded
2100
+ ? ["Memory committed without a vector embedding; keyword search remains available."]
2101
+ : [],
2102
+ facts: {
2103
+ deduped: result.deduped,
2104
+ dedupeReason: result.dedupeReason,
2105
+ updatedInPlace: result.updated,
2106
+ embedded: result.embedded,
2107
+ },
2108
+ }),
2109
+ );
1840
2110
  },
1841
2111
  );
1842
2112
 
@@ -1881,7 +2151,31 @@ function registerMemoryTools(
1881
2151
  },
1882
2152
  },
1883
2153
  ]);
1884
- return json(result);
2154
+ return json(
2155
+ mcpMutationReceipt({
2156
+ operation: "memory_correct",
2157
+ committed: true,
2158
+ outcome: "updated",
2159
+ changed: true,
2160
+ resource: {
2161
+ type: "knowledge_memory",
2162
+ id: result.memory.id,
2163
+ state: result.memory.status,
2164
+ },
2165
+ relatedResources: result.replacement
2166
+ ? [
2167
+ {
2168
+ type: "knowledge_memory",
2169
+ id: result.replacement.id,
2170
+ state: result.replacement.status,
2171
+ },
2172
+ ]
2173
+ : undefined,
2174
+ timestamp: (result.replacement ?? result.memory).updatedAt,
2175
+ idempotency: { status: "not_supported" },
2176
+ facts: { correctionAction: result.action },
2177
+ }),
2178
+ );
1885
2179
  },
1886
2180
  );
1887
2181
  }
@@ -1928,7 +2222,7 @@ function registerFleetTools(
1928
2222
  "sandboxes_list",
1929
2223
  {
1930
2224
  description:
1931
- "List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `liveness` is conservative: online requires observed provider existence and verified workspace readiness. An idle session-home sandbox may report offline/cold/draining and still wake or restore on the next ordinary sandbox operation; never infer that shell/files are unavailable from list liveness alone—only a typed operation/attach failure proves that. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch are also reported separately. Use an entry `id` as an attach/swap/run_on target.",
2225
+ "List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `operationAvailability` is authoritative for ordinary shell/files use: `wakeable` means the next ordinary operation will wake or restore the idle managed home sandbox, even when `liveness=offline`, `leaseLiveness=cold|draining`, or `attachable=false`. `attachable` describes an already-live swap target, not ordinary operation availability. `recovering` requires a bounded retry/typed recovery result; `unavailable` is not usable. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch remain separate truth dimensions. Use an entry `id` as an attach/swap/run_on target.",
1932
2226
  inputSchema: {},
1933
2227
  },
1934
2228
  async () => json(await listFleet(services, await fleetContext())),
@@ -2000,6 +2294,45 @@ function registerFleetTools(
2000
2294
  );
2001
2295
  }
2002
2296
 
2297
+ // Workspace-admin/operator surface for removing a connected machine. This is
2298
+ // deliberately separate from the session-scoped fleet tools: a worker can list,
2299
+ // attach, or run on a machine only with session authority, while removal requires
2300
+ // the explicit high-trust enrollments:manage permission and never accepts a
2301
+ // sandbox id. A Modal record therefore cannot be removed through this operation.
2302
+ function registerConnectedMachineTools(
2303
+ server: McpServer,
2304
+ deps: ApiRouteDeps,
2305
+ grant: AccessGrant,
2306
+ json: JsonResult,
2307
+ ): void {
2308
+ server.registerTool(
2309
+ "connected_machine_remove",
2310
+ {
2311
+ description:
2312
+ "Remove one enrolled self-hosted machine while it is offline. Access is revoked, future heartbeat/reconnect credentials are rejected, session/route/lease/archive history is retained, and a fresh human-approved device-flow enrollment is required to reconnect. Pass the enrollmentId from the Machines surface, never a Modal sandbox id. Blocked outcomes include the exact active dependency and the action needed before retrying.",
2313
+ inputSchema: {
2314
+ enrollmentId: z4.string().uuid(),
2315
+ expectedUpdatedAt: z4.string().datetime({ offset: true }).optional(),
2316
+ idempotencyKey: z4.string().trim().min(1).max(200).optional(),
2317
+ },
2318
+ },
2319
+ async ({ enrollmentId, expectedUpdatedAt, idempotencyKey }) => {
2320
+ const result = await removeEnrollment(deps.db, {
2321
+ accountId: grant.accountId,
2322
+ workspaceId: grant.workspaceId,
2323
+ enrollmentId,
2324
+ operationKey: idempotencyKey?.trim() || randomUUID(),
2325
+ ...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
2326
+ subjectId: grant.subjectId,
2327
+ });
2328
+ if (!result) {
2329
+ throw new Error("machine enrollment not found in this workspace");
2330
+ }
2331
+ return json({ revoked: result.removed, ...result });
2332
+ },
2333
+ );
2334
+ }
2335
+
2003
2336
  async function beginMcpRigVerificationAttempt(
2004
2337
  deps: ApiRouteDeps,
2005
2338
  workspaceId: string,
@@ -2101,12 +2434,60 @@ function registerRigTools(
2101
2434
  sessionId ? { proposedBy: `session:${sessionId}` } : {},
2102
2435
  );
2103
2436
  const verifying = await beginMcpRigVerificationAttempt(deps, grant.workspaceId, change.id);
2104
- await deps.workflowClient.startRigVerification({
2105
- workspaceId: grant.workspaceId,
2106
- changeId: change.id,
2107
- workflowId: `rig-verification-change-${change.id}-attempt-${verificationAttempt(verifying)}`,
2108
- });
2109
- return json({ change: verifying, verificationStarted: true });
2437
+ const attempt = verificationAttempt(verifying);
2438
+ try {
2439
+ await deps.workflowClient.startRigVerification({
2440
+ workspaceId: grant.workspaceId,
2441
+ changeId: change.id,
2442
+ workflowId: `rig-verification-change-${change.id}-attempt-${attempt}`,
2443
+ });
2444
+ } catch {
2445
+ return json(
2446
+ mcpMutationReceipt({
2447
+ operation: "rig_propose_change",
2448
+ committed: true,
2449
+ outcome: "partial_failure",
2450
+ changed: true,
2451
+ resource: {
2452
+ type: "rig_change",
2453
+ id: verifying.id,
2454
+ version: verifying.updatedAt,
2455
+ state: verifying.status,
2456
+ },
2457
+ relatedResources: [{ type: "rig", id: rig.id }],
2458
+ timestamp: verifying.updatedAt,
2459
+ idempotency: { status: "not_supported" },
2460
+ partialFailure: {
2461
+ stage: "verification_workflow_start",
2462
+ retryable: true,
2463
+ },
2464
+ warnings: [
2465
+ "The rig change and verifying transition committed, but verification workflow start failed.",
2466
+ ],
2467
+ facts: { verificationAttempt: attempt },
2468
+ nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
2469
+ }),
2470
+ );
2471
+ }
2472
+ return json(
2473
+ mcpMutationReceipt({
2474
+ operation: "rig_propose_change",
2475
+ committed: true,
2476
+ outcome: "created",
2477
+ changed: true,
2478
+ resource: {
2479
+ type: "rig_change",
2480
+ id: verifying.id,
2481
+ version: verifying.updatedAt,
2482
+ state: verifying.status,
2483
+ },
2484
+ relatedResources: [{ type: "rig", id: rig.id }],
2485
+ timestamp: verifying.updatedAt,
2486
+ idempotency: { status: "not_supported" },
2487
+ facts: { verificationStarted: true, verificationAttempt: attempt },
2488
+ nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
2489
+ }),
2490
+ );
2110
2491
  },
2111
2492
  );
2112
2493
 
@@ -2129,12 +2510,63 @@ function registerRigTools(
2129
2510
  grant.workspaceId,
2130
2511
  change.id,
2131
2512
  );
2132
- await deps.workflowClient.startRigVerification({
2133
- workspaceId: grant.workspaceId,
2134
- changeId: change.id,
2135
- workflowId: `rig-verification-change-${change.id}-attempt-${verificationAttempt(verifying)}`,
2136
- });
2137
- return json({ ok: true, changeId: change.id });
2513
+ const attempt = verificationAttempt(verifying);
2514
+ try {
2515
+ await deps.workflowClient.startRigVerification({
2516
+ workspaceId: grant.workspaceId,
2517
+ changeId: change.id,
2518
+ workflowId: `rig-verification-change-${change.id}-attempt-${attempt}`,
2519
+ });
2520
+ } catch {
2521
+ return json(
2522
+ mcpMutationReceipt({
2523
+ operation: "rig_verify",
2524
+ committed: true,
2525
+ outcome: "partial_failure",
2526
+ changed: true,
2527
+ resource: {
2528
+ type: "rig_change",
2529
+ id: verifying.id,
2530
+ version: verifying.updatedAt,
2531
+ state: verifying.status,
2532
+ },
2533
+ relatedResources: [{ type: "rig", id: rig.id }],
2534
+ timestamp: verifying.updatedAt,
2535
+ idempotency: { status: "not_supported" },
2536
+ partialFailure: {
2537
+ stage: "verification_workflow_start",
2538
+ retryable: true,
2539
+ },
2540
+ warnings: [
2541
+ "The verifying transition committed, but verification workflow start failed.",
2542
+ ],
2543
+ facts: { verificationAttempt: attempt },
2544
+ nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
2545
+ }),
2546
+ );
2547
+ }
2548
+ return json(
2549
+ mcpMutationReceipt({
2550
+ operation: "rig_verify",
2551
+ committed: true,
2552
+ outcome: "accepted",
2553
+ changed: true,
2554
+ resource: {
2555
+ type: "rig_change",
2556
+ id: verifying.id,
2557
+ version: verifying.updatedAt,
2558
+ state: verifying.status,
2559
+ },
2560
+ relatedResources: [{ type: "rig", id: rig.id }],
2561
+ timestamp: verifying.updatedAt,
2562
+ idempotency: { status: "not_supported" },
2563
+ facts: {
2564
+ verificationStarted: true,
2565
+ verificationAttempt: attempt,
2566
+ },
2567
+ nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
2568
+ }),
2569
+ );
2138
2570
  }
2139
2571
  if (!rig.activeVersion) {
2140
2572
  throw new Error("rig has no active version");
@@ -2144,7 +2576,23 @@ function registerRigTools(
2144
2576
  versionId: rig.activeVersion.id,
2145
2577
  workflowId: `rig-verification-version-${rig.activeVersion.id}-${crypto.randomUUID()}`,
2146
2578
  });
2147
- return json({ ok: true, versionId: rig.activeVersion.id });
2579
+ return json(
2580
+ mcpMutationReceipt({
2581
+ operation: "rig_verify",
2582
+ committed: true,
2583
+ outcome: "accepted",
2584
+ changed: true,
2585
+ resource: {
2586
+ type: "rig_version",
2587
+ id: rig.activeVersion.id,
2588
+ version: rig.activeVersion.version,
2589
+ state: "verification_started",
2590
+ },
2591
+ relatedResources: [{ type: "rig", id: rig.id }],
2592
+ idempotency: { status: "not_supported" },
2593
+ nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
2594
+ }),
2595
+ );
2148
2596
  },
2149
2597
  );
2150
2598
  }
@@ -2163,8 +2611,36 @@ function registerRigTools(
2163
2611
  async ({ rigId, changeId }) => {
2164
2612
  const rig = await requireRigForApi(deps.db, grant.workspaceId, rigId);
2165
2613
  const change = await requireRigChangeForApi(deps.db, grant.workspaceId, rig.id, changeId);
2614
+ const promoted = await promoteVerifiedDefinitionEditChangeForApi(
2615
+ { db: deps.db },
2616
+ grant,
2617
+ rig,
2618
+ change,
2619
+ );
2166
2620
  return json(
2167
- await promoteVerifiedDefinitionEditChangeForApi({ db: deps.db }, grant, rig, change),
2621
+ mcpMutationReceipt({
2622
+ operation: "rig_promote",
2623
+ committed: true,
2624
+ outcome: "updated",
2625
+ changed: true,
2626
+ resource: {
2627
+ type: "rig_version",
2628
+ id: promoted.version.id,
2629
+ version: promoted.version.version,
2630
+ state: "active",
2631
+ },
2632
+ relatedResources: [
2633
+ {
2634
+ type: "rig_change",
2635
+ id: promoted.change.id,
2636
+ state: promoted.change.status,
2637
+ },
2638
+ { type: "rig", id: rig.id },
2639
+ ],
2640
+ timestamp: promoted.version.createdAt,
2641
+ idempotency: { status: "not_supported" },
2642
+ nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
2643
+ }),
2168
2644
  );
2169
2645
  },
2170
2646
  );
@@ -2384,7 +2860,10 @@ function registerWorkspaceOrchestrationTools(
2384
2860
  compactSessionEventResult(
2385
2861
  event,
2386
2862
  latestClass!,
2387
- dbPage.coveredSequence ?? { first: event.sequence, last: event.sequence },
2863
+ dbPage.coveredSequence ?? {
2864
+ first: event.sequence,
2865
+ last: event.sequence,
2866
+ },
2388
2867
  ),
2389
2868
  )
2390
2869
  : null,
@@ -2508,8 +2987,13 @@ function registerWorkspaceOrchestrationTools(
2508
2987
  if (callerSessionId !== null) {
2509
2988
  await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
2510
2989
  }
2511
- const created = await createSessionForRequest(deps, grant, grant.workspaceId, args);
2512
- return json(await withMcpEffectivePolicy(deps, grant.workspaceId, created));
2990
+ const result = await createSessionForRequestWithOutcome(
2991
+ deps,
2992
+ grant,
2993
+ grant.workspaceId,
2994
+ args,
2995
+ );
2996
+ return json(sessionCreateMutationReceipt(result, Boolean(args.idempotencyKey)));
2513
2997
  } catch (error) {
2514
2998
  if (error instanceof SessionSpawnDeniedError) {
2515
2999
  return {
@@ -2549,17 +3033,33 @@ function registerWorkspaceOrchestrationTools(
2549
3033
  exactAgentCommandContext(grant, callerSessionId),
2550
3034
  { targetSessionId, text, idempotencyKey },
2551
3035
  );
2552
- return json({
2553
- delivered: true,
2554
- updateId: result.updateId,
2555
- delivery: "coalesced_internal_update",
2556
- effectiveState: result.effectiveState,
2557
- wakeRequested: result.wakeRevision !== null,
2558
- resumeRequired: result.effectiveState === "paused",
2559
- replay: result.replay,
2560
- });
3036
+ return json(
3037
+ mcpMutationReceipt({
3038
+ operation: "session_send_message",
3039
+ committed: true,
3040
+ outcome: result.replay ? "replayed" : "accepted",
3041
+ changed: !result.replay,
3042
+ resource: {
3043
+ type: "session_system_update",
3044
+ id: result.updateId,
3045
+ state: result.effectiveState,
3046
+ },
3047
+ relatedResources: [{ type: "session", id: targetSessionId }],
3048
+ timestamp: result.receipt.createdAt.toISOString(),
3049
+ idempotency: { status: result.replay ? "replayed" : "applied" },
3050
+ facts: {
3051
+ delivery: "coalesced_internal_update",
3052
+ wakeRequested: result.wakeRevision !== null,
3053
+ resumeRequired: result.effectiveState === "paused",
3054
+ },
3055
+ nextAction: {
3056
+ tool: "session_get",
3057
+ arguments: { sessionId: targetSessionId },
3058
+ },
3059
+ }),
3060
+ );
2561
3061
  }
2562
- const { accepted, turn } = await acceptSessionUserMessage(
3062
+ const { accepted, turn, replay } = await acceptSessionUserMessageWithOutcome(
2563
3063
  deps,
2564
3064
  grant,
2565
3065
  grant.workspaceId,
@@ -2574,7 +3074,30 @@ function registerWorkspaceOrchestrationTools(
2574
3074
  ),
2575
3075
  },
2576
3076
  );
2577
- return json({ event: accepted, turnId: turn.id });
3077
+ return json(
3078
+ mcpMutationReceipt({
3079
+ operation: "session_send_message",
3080
+ committed: true,
3081
+ outcome: replay ? "replayed" : "accepted",
3082
+ changed: !replay,
3083
+ resource: {
3084
+ type: "session_turn",
3085
+ id: turn.id,
3086
+ version: turn.version,
3087
+ state: turn.status,
3088
+ },
3089
+ relatedResources: [
3090
+ { type: "session", id: targetSessionId },
3091
+ { type: "session_event", id: accepted.id, state: accepted.type },
3092
+ ],
3093
+ timestamp: accepted.occurredAt,
3094
+ idempotency: { status: replay ? "replayed" : "applied" },
3095
+ nextAction: {
3096
+ tool: "session_get",
3097
+ arguments: { sessionId: targetSessionId },
3098
+ },
3099
+ }),
3100
+ );
2578
3101
  },
2579
3102
  );
2580
3103
 
@@ -2701,13 +3224,28 @@ function registerWorkspaceOrchestrationTools(
2701
3224
  exactAgentCommandContext(grant, callerSessionId, "first_party_mcp"),
2702
3225
  { targetSessionId: sessionId, instruction, idempotencyKey },
2703
3226
  );
2704
- return json({
2705
- updateId: result.updateId,
2706
- interruptionCount: result.interruptionCount,
2707
- stoppingPreviousAttempt: result.interruptionCount > 0,
2708
- effectiveState: result.effectiveState,
2709
- replay: result.replay,
2710
- });
3227
+ return json(
3228
+ mcpMutationReceipt({
3229
+ operation: "session_steer",
3230
+ committed: true,
3231
+ outcome: result.replay ? "replayed" : "updated",
3232
+ changed: !result.replay,
3233
+ resource: {
3234
+ type: "session_system_update",
3235
+ id: result.updateId,
3236
+ state: result.effectiveState,
3237
+ },
3238
+ relatedResources: [{ type: "session", id: sessionId }],
3239
+ timestamp: result.receipt.createdAt.toISOString(),
3240
+ idempotency: { status: result.replay ? "replayed" : "applied" },
3241
+ facts: {
3242
+ interruptionCount: result.interruptionCount,
3243
+ stoppingPreviousAttempt: result.interruptionCount > 0,
3244
+ },
3245
+ updateId: result.updateId,
3246
+ nextAction: { tool: "session_get", arguments: { sessionId } },
3247
+ }),
3248
+ );
2711
3249
  },
2712
3250
  );
2713
3251
  }
@@ -2736,15 +3274,16 @@ function registerWorkspaceOrchestrationTools(
2736
3274
  }
2737
3275
  }
2738
3276
 
2739
- // VariableSet management for manager-style agents. v1 deliberately accepts
2740
- // variable VALUES in plain tool arguments: the calling model is trusted with
2741
- // the secrets it is persisting (see docs/variable-sets.md). Reads stay
2742
- // write-only — responses carry names and metadata, never values.
3277
+ // Variable-set management for agents. Generic reads remain metadata-only.
3278
+ // Plaintext has one dedicated tool that additionally requires literal
3279
+ // secrets:read plus exact live attempt/session authorization.
2743
3280
  function registerVariableSetTools(
2744
3281
  server: McpServer,
2745
3282
  deps: ApiRouteDeps,
2746
3283
  grant: AccessGrant,
2747
3284
  can: (permission: Permission) => boolean,
3285
+ sessionId: string | null,
3286
+ toolspaceMode: boolean,
2748
3287
  json: JsonResult,
2749
3288
  ): void {
2750
3289
  const registerListTool = (name: string, description: string): void => {
@@ -2760,93 +3299,114 @@ function registerVariableSetTools(
2760
3299
  },
2761
3300
  );
2762
3301
  };
2763
- const setVariableHandler = async ({
2764
- variableSetId,
2765
- variableSetName,
2766
- environmentId,
2767
- environmentName,
2768
- name,
2769
- value,
2770
- }: {
2771
- variableSetId?: string | undefined;
2772
- variableSetName?: string | undefined;
2773
- environmentId?: string | undefined;
2774
- environmentName?: string | undefined;
2775
- name: string;
2776
- value: string;
2777
- }) => {
2778
- const key = requireVariableSetEncryption(deps.settings);
2779
- const parsedName = VariableSetVariableName.safeParse(name);
2780
- if (!parsedName.success) {
2781
- throw new Error("variable set/environment variable names must match ^[A-Z][A-Z0-9_]*$");
2782
- }
2783
- assertAllowedVariableSetVariableName(parsedName.data);
2784
- const targetId = variableSetId ?? environmentId;
2785
- const targetName = variableSetName ?? environmentName;
2786
- if ((targetId === undefined) === (targetName === undefined)) {
2787
- throw new Error(
2788
- "provide exactly one of variableSetId or variableSetName; deprecated aliases must provide exactly one of environmentId or environmentName",
2789
- );
2790
- }
2791
- const trimmedVariableSetName = targetName?.trim();
2792
- if (targetName !== undefined && !trimmedVariableSetName) {
2793
- throw new Error("variable set name is required");
2794
- }
2795
- let created = false;
2796
- let variableSet =
2797
- targetId !== undefined
2798
- ? await getVariableSet(deps.db, grant.workspaceId, targetId)
2799
- : await getVariableSetByName(deps.db, grant.workspaceId, trimmedVariableSetName!);
2800
- if (!variableSet && targetId !== undefined) {
2801
- throw new Error("variable set/environment not found");
2802
- }
2803
- if (!variableSet) {
2804
- if ((await countVariableSets(deps.db, grant.workspaceId)) >= MAX_ENVIRONMENTS_PER_WORKSPACE) {
3302
+ const setVariableHandler =
3303
+ (operation: "variable_set_set_variable" | "environment_set_variable") =>
3304
+ async ({
3305
+ variableSetId,
3306
+ variableSetName,
3307
+ environmentId,
3308
+ environmentName,
3309
+ name,
3310
+ value,
3311
+ }: {
3312
+ variableSetId?: string | undefined;
3313
+ variableSetName?: string | undefined;
3314
+ environmentId?: string | undefined;
3315
+ environmentName?: string | undefined;
3316
+ name: string;
3317
+ value: string;
3318
+ }) => {
3319
+ const key = requireVariableSetEncryption(deps.settings);
3320
+ const parsedName = VariableSetVariableName.safeParse(name);
3321
+ if (!parsedName.success) {
3322
+ throw new Error("variable set/environment variable names must match ^[A-Z][A-Z0-9_]*$");
3323
+ }
3324
+ assertAllowedVariableSetVariableName(parsedName.data);
3325
+ const targetId = variableSetId ?? environmentId;
3326
+ const targetName = variableSetName ?? environmentName;
3327
+ if ((targetId === undefined) === (targetName === undefined)) {
2805
3328
  throw new Error(
2806
- `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE} variable sets`,
3329
+ "provide exactly one of variableSetId or variableSetName; deprecated aliases must provide exactly one of environmentId or environmentName",
2807
3330
  );
2808
3331
  }
2809
- variableSet = await createVariableSet(deps.db, {
3332
+ const trimmedVariableSetName = targetName?.trim();
3333
+ if (targetName !== undefined && !trimmedVariableSetName) {
3334
+ throw new Error("variable set name is required");
3335
+ }
3336
+ let created = false;
3337
+ let variableSet =
3338
+ targetId !== undefined
3339
+ ? await getVariableSet(deps.db, grant.workspaceId, targetId)
3340
+ : await getVariableSetByName(deps.db, grant.workspaceId, trimmedVariableSetName!);
3341
+ if (!variableSet && targetId !== undefined) {
3342
+ throw new Error("variable set/environment not found");
3343
+ }
3344
+ if (!variableSet) {
3345
+ if (
3346
+ (await countVariableSets(deps.db, grant.workspaceId)) >= MAX_ENVIRONMENTS_PER_WORKSPACE
3347
+ ) {
3348
+ throw new Error(
3349
+ `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE} variable sets`,
3350
+ );
3351
+ }
3352
+ variableSet = await createVariableSet(deps.db, {
3353
+ accountId: grant.accountId,
3354
+ workspaceId: grant.workspaceId,
3355
+ name: trimmedVariableSetName!,
3356
+ });
3357
+ created = true;
3358
+ await recordVariableSetAuditEvent(deps.db, {
3359
+ grant,
3360
+ action: "variable_set.created",
3361
+ variableSetId: variableSet.id,
3362
+ });
3363
+ }
3364
+ const exists = variableSet.variables.some((variable) => variable.name === parsedName.data);
3365
+ if (!exists && variableSet.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT) {
3366
+ throw new Error(
3367
+ `a variable set supports at most ${MAX_VARIABLES_PER_ENVIRONMENT} variables`,
3368
+ );
3369
+ }
3370
+ const metadata = await setVariableSetVariable(deps.db, {
2810
3371
  accountId: grant.accountId,
2811
3372
  workspaceId: grant.workspaceId,
2812
- name: trimmedVariableSetName!,
3373
+ variableSetId: variableSet.id,
3374
+ name: parsedName.data,
3375
+ valueEncrypted: encryptVariableSetValue(key, value),
2813
3376
  });
2814
- created = true;
2815
3377
  await recordVariableSetAuditEvent(deps.db, {
2816
3378
  grant,
2817
- action: "variable_set.created",
3379
+ action: "variable_set.variable.set",
2818
3380
  variableSetId: variableSet.id,
3381
+ variableName: parsedName.data,
2819
3382
  });
2820
- }
2821
- const exists = variableSet.variables.some((variable) => variable.name === parsedName.data);
2822
- if (!exists && variableSet.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT) {
2823
- throw new Error(`a variable set supports at most ${MAX_VARIABLES_PER_ENVIRONMENT} variables`);
2824
- }
2825
- const metadata = await setVariableSetVariable(deps.db, {
2826
- accountId: grant.accountId,
2827
- workspaceId: grant.workspaceId,
2828
- variableSetId: variableSet.id,
2829
- name: parsedName.data,
2830
- valueEncrypted: encryptVariableSetValue(key, value),
2831
- });
2832
- await recordVariableSetAuditEvent(deps.db, {
2833
- grant,
2834
- action: "variable_set.variable.set",
2835
- variableSetId: variableSet.id,
2836
- variableName: parsedName.data,
2837
- });
2838
- const responseVariableSet = {
2839
- id: variableSet.id,
2840
- name: variableSet.name,
2841
- created,
3383
+ return json(
3384
+ mcpMutationReceipt({
3385
+ operation,
3386
+ committed: true,
3387
+ outcome: exists ? "updated" : "created",
3388
+ changed: true,
3389
+ resource: {
3390
+ type: "variable_set",
3391
+ id: variableSet.id,
3392
+ version: metadata.version,
3393
+ state: "variable_written",
3394
+ },
3395
+ timestamp: metadata.updatedAt,
3396
+ idempotency: { status: "not_supported" },
3397
+ facts: {
3398
+ variableCreated: !exists,
3399
+ variableSetCreated: created,
3400
+ deprecatedAlias: operation === "environment_set_variable",
3401
+ },
3402
+ nextAction: { tool: "variable_set_list", arguments: {} },
3403
+ }),
3404
+ );
2842
3405
  };
2843
- return json({
2844
- variableSet: responseVariableSet,
2845
- environment: responseVariableSet,
2846
- variable: metadata,
2847
- });
2848
- };
2849
- const registerSetTool = (name: string, description: string): void => {
3406
+ const registerSetTool = (
3407
+ name: "variable_set_set_variable" | "environment_set_variable",
3408
+ description: string,
3409
+ ): void => {
2850
3410
  server.registerTool(
2851
3411
  name,
2852
3412
  {
@@ -2860,28 +3420,97 @@ function registerVariableSetTools(
2860
3420
  value: z4.string().min(1).max(32768),
2861
3421
  },
2862
3422
  },
2863
- setVariableHandler,
3423
+ setVariableHandler(name),
2864
3424
  );
2865
3425
  };
2866
- if (can("variable-sets:use")) {
3426
+ if (can("variable-sets:list") && can("secrets:list")) {
2867
3427
  registerListTool(
2868
3428
  "variable_set_list",
2869
- "List variable sets with variable names and metadata (versions, timestamps). Values are write-only and never returned.",
3429
+ "List variable sets with variable names and metadata (versions, timestamps). Plaintext values are never returned by list operations.",
2870
3430
  );
2871
3431
  registerListTool(
2872
3432
  "environment_list",
2873
- "(deprecated alias of variable_set_list) List variable sets with variable names and metadata (versions, timestamps). Values are write-only and never returned.",
3433
+ "(deprecated alias of variable_set_list) List variable sets with variable names and metadata (versions, timestamps). Plaintext values are never returned by list operations.",
3434
+ );
3435
+ }
3436
+
3437
+ if (
3438
+ !toolspaceMode &&
3439
+ sessionId !== null &&
3440
+ can("variable-sets:read") &&
3441
+ hasLiteralPermission(grant.permissions, "secrets:read")
3442
+ ) {
3443
+ server.registerTool(
3444
+ "variable_set_get_variable",
3445
+ {
3446
+ description:
3447
+ "Retrieve one exact plaintext variable value. This is a dedicated high-trust read: use it only when the current task requires the configured value. The access is audited against this exact live attempt.",
3448
+ inputSchema: {
3449
+ variableSetId: z4.string().uuid().optional(),
3450
+ variableSetName: z4.string().min(1).max(120).optional(),
3451
+ name: z4.string().min(1),
3452
+ },
3453
+ },
3454
+ async ({ variableSetId, variableSetName, name }) => {
3455
+ let target: { variableSetId: string } | { variableSetName: string };
3456
+ if (variableSetId !== undefined) {
3457
+ if (variableSetName !== undefined) {
3458
+ throw new Error("provide exactly one of variableSetId or variableSetName");
3459
+ }
3460
+ target = { variableSetId };
3461
+ } else {
3462
+ if (variableSetName === undefined) {
3463
+ throw new Error("provide exactly one of variableSetId or variableSetName");
3464
+ }
3465
+ target = { variableSetName };
3466
+ }
3467
+ if (
3468
+ ("variableSetId" in target && target.variableSetId.length === 0) ||
3469
+ ("variableSetName" in target && target.variableSetName.trim().length === 0)
3470
+ ) {
3471
+ throw new Error("provide exactly one of variableSetId or variableSetName");
3472
+ }
3473
+ const parsedName = VariableSetVariableName.safeParse(name);
3474
+ if (!parsedName.success) {
3475
+ throw new Error("variable set variable names must match ^[A-Z][A-Z0-9_]*$");
3476
+ }
3477
+ assertAllowedVariableSetVariableName(parsedName.data);
3478
+ const claims = preferenceAttemptClaims(grant);
3479
+ if (!claims || claims.sessionId !== sessionId) {
3480
+ throw new Error("Exact signed secret-read attempt authority is required.");
3481
+ }
3482
+ await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
3483
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.secret.read");
3484
+ const key = requireVariableSetEncryption(deps.settings);
3485
+ const secret = await readVariableSetSecretAtomically(deps.db, {
3486
+ accountId: grant.accountId,
3487
+ workspaceId: grant.workspaceId,
3488
+ subjectId: grant.subjectId,
3489
+ ...target,
3490
+ name: parsedName.data,
3491
+ actor: {
3492
+ kind: "agent_attempt",
3493
+ sessionId: claims.sessionId,
3494
+ turnId: claims.turnId,
3495
+ attemptId: claims.attemptId,
3496
+ executionGeneration: claims.executionGeneration,
3497
+ },
3498
+ decrypt: (valueEncrypted) => decryptVariableSetValue(key, valueEncrypted),
3499
+ });
3500
+ if (!secret) throw new Error("variable set variable not found");
3501
+ return json(secret);
3502
+ },
2874
3503
  );
2875
3504
  }
2876
3505
 
2877
- if (can("variable-sets:manage")) {
3506
+ if (can("variable-sets:write") && can("secrets:write")) {
2878
3507
  registerSetTool(
2879
3508
  "variable_set_set_variable",
2880
- "Set or rotate one variable in a variable set. Target by variableSetId, or by variableSetName (created if it does not exist). The value is encrypted at rest and injected into sandboxes of sessions the variable set is attached to; it is never readable back through any API.",
3509
+ "Set or rotate one variable in a variable set. Target by variableSetId, or by variableSetName (created if it does not exist). The value is encrypted at rest and injected into sandboxes of sessions the variable set is attached to. Reading it requires the dedicated permissioned secret-read operation.",
2881
3510
  );
2882
3511
  registerSetTool(
2883
3512
  "environment_set_variable",
2884
- "(deprecated alias of variable_set_set_variable) Set or rotate one variable in a variable set. Target by variableSetId, or by variableSetName (created if it does not exist). The value is encrypted at rest and injected into sandboxes of sessions the variable set is attached to; it is never readable back through any API.",
3513
+ "(deprecated alias of variable_set_set_variable) Set or rotate one variable in a variable set. Target by variableSetId, or by variableSetName (created if it does not exist). The value is encrypted at rest and injected into sandboxes of sessions the variable set is attached to. Reading it requires the dedicated permissioned secret-read operation.",
2885
3514
  );
2886
3515
  }
2887
3516
  }
@@ -2976,7 +3605,8 @@ function repositoryWithScheduledTaskResource(
2976
3605
  kind: "repository",
2977
3606
  uri,
2978
3607
  ref: repository.defaultBranch,
2979
- mountPath: defaultRepositoryMountPath(uri),
3608
+ provider: "github",
3609
+ mountPath: defaultRepositoryMountPath(uri, "github"),
2980
3610
  ...(repository.private
2981
3611
  ? {
2982
3612
  githubInstallationId: repository.installationId,