@opengeni/api-router 0.22.2 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1824 -30
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +30 -1
- package/dist/integrations/provider-oauth.d.ts +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +14 -2
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +518 -0
- package/dist/mcp/server.d.ts +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +59 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +277 -41
- package/src/auth/managed-auth.ts +0 -16
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +2 -0
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +101 -6
- package/src/index.ts +147 -23
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +159 -89
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +610 -42
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +74 -26
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +642 -0
- package/src/mcp/server.ts +1718 -310
- package/src/memory-slack-delivery.ts +209 -0
- package/src/observability.ts +3 -3
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/codex.ts +7 -4
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +358 -102
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +277 -65
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +38 -2
- package/src/routes/integration-features.ts +258 -0
- package/src/routes/machines.ts +1 -1
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +94 -42
- package/src/routes/sessions.ts +475 -234
- package/src/routes/skills.ts +174 -0
- package/src/routes/transcription-recordings.ts +65 -33
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +809 -85
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +58 -29
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-HWXJW5C7.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -62
- package/src/mcp/toolspace.ts +0 -1186
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,
|
|
@@ -12,27 +12,34 @@ import {
|
|
|
12
12
|
SessionEventResultMode,
|
|
13
13
|
SessionEventSemanticClass,
|
|
14
14
|
SessionEventType,
|
|
15
|
+
stableJson,
|
|
15
16
|
compactSessionEventResult,
|
|
16
17
|
sessionEventLatestClassToSemanticClass,
|
|
18
|
+
MemorySlackPublicationDistribution,
|
|
17
19
|
SessionMcpCredentialUpdateInput,
|
|
20
|
+
ToolAuthNeededPayload,
|
|
18
21
|
VariableSetVariableName,
|
|
22
|
+
capabilityCatalogItemIsTrustedForExposure,
|
|
19
23
|
type AccessGrant,
|
|
24
|
+
type CapabilityCatalogItem,
|
|
20
25
|
type GitHubRepository,
|
|
21
26
|
type FirstPartyMcpToolName,
|
|
22
27
|
type Permission,
|
|
23
28
|
type ResourceRef,
|
|
24
29
|
type SessionAuthorizationOperation,
|
|
30
|
+
type SessionAuthorizationActor,
|
|
25
31
|
type SessionAuthorizationSurface,
|
|
26
32
|
type Session,
|
|
33
|
+
type ScheduledTask,
|
|
27
34
|
UpdateScheduledTaskRequest,
|
|
28
35
|
normalizeWorkspaceArtifactSlug,
|
|
29
36
|
WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES,
|
|
30
37
|
} from "@opengeni/contracts";
|
|
31
38
|
import {
|
|
32
|
-
correctWorkspaceMemory,
|
|
33
39
|
countVariableSets,
|
|
34
40
|
beginRigChangeVerificationAttempt,
|
|
35
41
|
createVariableSet,
|
|
42
|
+
decryptVariableSetValue,
|
|
36
43
|
deleteScheduledTask,
|
|
37
44
|
encryptVariableSetValue,
|
|
38
45
|
getSession,
|
|
@@ -56,6 +63,8 @@ import {
|
|
|
56
63
|
listRigVersionMonitoringSummaries,
|
|
57
64
|
listSocialPosts,
|
|
58
65
|
recordAuditEvent,
|
|
66
|
+
removeEnrollment,
|
|
67
|
+
readVariableSetSecretAtomically,
|
|
59
68
|
recordSyncedSocialPosts,
|
|
60
69
|
listVariableSets,
|
|
61
70
|
MEMORY_CORRECT_TOOL_DESCRIPTION,
|
|
@@ -63,7 +72,6 @@ import {
|
|
|
63
72
|
MEMORY_SEARCH_TOOL_DESCRIPTION,
|
|
64
73
|
requireScheduledTask,
|
|
65
74
|
requireSession,
|
|
66
|
-
saveWorkspaceMemory,
|
|
67
75
|
searchWorkspaceMemories,
|
|
68
76
|
serializeEffectiveSessionControl,
|
|
69
77
|
setSessionGoalStatusWithEvent,
|
|
@@ -71,7 +79,6 @@ import {
|
|
|
71
79
|
updateScheduledTask,
|
|
72
80
|
updateSessionGoalWithEvent,
|
|
73
81
|
upsertSessionGoalWithEvent,
|
|
74
|
-
RigChangeAlreadyVerifyingError,
|
|
75
82
|
RigChangeTransitionError,
|
|
76
83
|
createWorkspaceArtifact,
|
|
77
84
|
getWorkspaceArtifact,
|
|
@@ -80,7 +87,11 @@ import {
|
|
|
80
87
|
publishWorkspaceArtifactVersion,
|
|
81
88
|
rollbackWorkspaceArtifact,
|
|
82
89
|
} from "@opengeni/db";
|
|
83
|
-
import {
|
|
90
|
+
import {
|
|
91
|
+
appendAndPublishEvents,
|
|
92
|
+
appendAndPublishTurnEventsFenced,
|
|
93
|
+
publishDurableSessionEvents,
|
|
94
|
+
} from "@opengeni/events";
|
|
84
95
|
import {
|
|
85
96
|
createSignedState,
|
|
86
97
|
GitHubAppConfigurationError,
|
|
@@ -95,10 +106,18 @@ import type { AnySchema, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/ser
|
|
|
95
106
|
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
96
107
|
import * as z4 from "zod/v4";
|
|
97
108
|
import {
|
|
109
|
+
hasLiteralPermission,
|
|
98
110
|
hasPermission,
|
|
99
111
|
authorizedSocialConnectionsForGrant,
|
|
112
|
+
authorizedAtlassianConnectionsForGrant,
|
|
113
|
+
buildCapabilityCatalog,
|
|
114
|
+
nativeConnectionCapabilityRecommendations,
|
|
115
|
+
correctWorkspaceMemoryWithSlackPublication,
|
|
116
|
+
requireLiveAgentAttemptAuthorization,
|
|
100
117
|
requireSessionAuthorization,
|
|
101
118
|
requireSessionAuthorizationListScope,
|
|
119
|
+
saveWorkspaceMemoryWithSlackPublication,
|
|
120
|
+
searchCapabilityCatalogItems,
|
|
102
121
|
type ResolvedSessionAuthorization,
|
|
103
122
|
} from "@opengeni/core";
|
|
104
123
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
@@ -110,12 +129,14 @@ import {
|
|
|
110
129
|
} from "../github-access";
|
|
111
130
|
import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
|
|
112
131
|
import {
|
|
132
|
+
assertSocialConnectionProvider,
|
|
113
133
|
socialMentionsLive,
|
|
114
134
|
socialOwnPostsLive,
|
|
115
135
|
socialPostReply,
|
|
116
136
|
socialSearchLive,
|
|
117
137
|
socialThreadLive,
|
|
118
138
|
} from "../integrations/social-api";
|
|
139
|
+
import { revokeKnowledgeSourceScheduleAuthorization } from "../integrations/google-drive";
|
|
119
140
|
import {
|
|
120
141
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
121
142
|
proposeRigChangeForApi,
|
|
@@ -132,17 +153,21 @@ import {
|
|
|
132
153
|
createValidatedScheduledTask,
|
|
133
154
|
manualScheduledTaskTriggerUsageKey,
|
|
134
155
|
manualScheduledTaskTriggerWorkflowId,
|
|
156
|
+
scheduledTaskForGrant,
|
|
157
|
+
scheduledTaskRunForGrant,
|
|
135
158
|
scheduledTaskToolsProvided,
|
|
136
159
|
scheduledTaskTriggerToken,
|
|
160
|
+
ScheduledTaskSyncError,
|
|
137
161
|
syncCreatedScheduledTask,
|
|
138
162
|
syncUpdatedScheduledTask,
|
|
163
|
+
validateScheduledTaskTarget,
|
|
139
164
|
validatedScheduledTaskUpdate,
|
|
140
165
|
} from "@opengeni/core";
|
|
141
166
|
import {
|
|
142
|
-
|
|
167
|
+
acceptSessionUserMessageWithOutcome,
|
|
143
168
|
controlAgentSessionWorkstream,
|
|
144
169
|
controlHumanSessionWorkstream,
|
|
145
|
-
|
|
170
|
+
createSessionForRequestWithOutcome,
|
|
146
171
|
SessionSpawnDeniedError,
|
|
147
172
|
sessionSpawnDenialEnvelope,
|
|
148
173
|
sendAgentSessionMessage,
|
|
@@ -170,18 +195,30 @@ import {
|
|
|
170
195
|
boundRigDetailMcp,
|
|
171
196
|
SESSION_EVENT_MCP_MAX_BYTES,
|
|
172
197
|
} from "./session-view";
|
|
173
|
-
import
|
|
198
|
+
import { mcpMutationReceipt, sessionCreateMutationReceipt } from "./receipts";
|
|
199
|
+
import {
|
|
200
|
+
boundScheduledTaskDetailMcp,
|
|
201
|
+
boundScheduledTaskMcpPage,
|
|
202
|
+
scheduledTaskMcpSummary,
|
|
203
|
+
} from "./scheduled-task-view";
|
|
174
204
|
import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
|
|
175
205
|
import {
|
|
176
206
|
createOpenGeniSlackBotClient,
|
|
177
207
|
resolveSlackBotConnectionForTool,
|
|
178
208
|
} from "../integrations/slack-bot";
|
|
209
|
+
import {
|
|
210
|
+
browseAtlassianSources,
|
|
211
|
+
getAtlassianLiveItem,
|
|
212
|
+
revokeAtlassianScheduleAuthorization,
|
|
213
|
+
searchAtlassianLive,
|
|
214
|
+
} from "../integrations/atlassian";
|
|
215
|
+
import { AtlassianConnectionMetadata } from "@opengeni/contracts/atlassian";
|
|
216
|
+
import { registerEditableArtifactAgentTools } from "./editable-artifacts";
|
|
179
217
|
|
|
180
218
|
export type McpServerOptions = {
|
|
181
219
|
// Origin of the HTTP request that reached the MCP route. Browser-oriented
|
|
182
220
|
// tools use it only when no configured public base URL is available.
|
|
183
221
|
requestOrigin?: string | null;
|
|
184
|
-
toolspace?: ToolspaceMcpSurface | null;
|
|
185
222
|
workspaceMemoryEnabled?: boolean | undefined;
|
|
186
223
|
};
|
|
187
224
|
|
|
@@ -205,13 +242,17 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
205
242
|
memory_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
206
243
|
memory_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
207
244
|
memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
|
|
208
|
-
preference_registry_summary: {
|
|
245
|
+
preference_registry_summary: {
|
|
246
|
+
sessionRequired: true,
|
|
247
|
+
allOf: ["workspace:read"],
|
|
248
|
+
},
|
|
209
249
|
preference_registry_get: { sessionRequired: true, allOf: ["workspace:read"] },
|
|
210
250
|
sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
211
251
|
sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
212
252
|
sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
213
253
|
run_on: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
214
254
|
sandbox_provision: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
255
|
+
connected_machine_remove: { allOf: ["enrollments:manage"] },
|
|
215
256
|
rig_list: { allOf: ["rigs:use"] },
|
|
216
257
|
rig_get: { allOf: ["rigs:use"] },
|
|
217
258
|
rig_propose_change: { allOf: ["rigs:use"] },
|
|
@@ -226,10 +267,35 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
226
267
|
session_resume: { allOf: ["sessions:control"] },
|
|
227
268
|
session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
228
269
|
set_other_session_title: { allOf: ["sessions:control"] },
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
270
|
+
interaction_discover: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
271
|
+
browser_open: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
272
|
+
browser_tabs: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
273
|
+
browser_observe: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
274
|
+
browser_act: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
275
|
+
browser_debug: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
276
|
+
browser_identity: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
277
|
+
browser_publish: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
278
|
+
browser_lifecycle: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
279
|
+
computer_open: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
280
|
+
computer_targets: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
281
|
+
computer_observe: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
282
|
+
computer_act: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
283
|
+
computer_lifecycle: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
284
|
+
variable_set_list: { allOf: ["variable-sets:list", "secrets:list"] },
|
|
285
|
+
environment_list: { allOf: ["variable-sets:list", "secrets:list"] },
|
|
286
|
+
variable_set_get_variable: {
|
|
287
|
+
sessionRequired: true,
|
|
288
|
+
allOf: ["variable-sets:read", "secrets:read"],
|
|
289
|
+
},
|
|
290
|
+
variable_set_set_variable: {
|
|
291
|
+
allOf: ["variable-sets:write", "secrets:write"],
|
|
292
|
+
},
|
|
293
|
+
environment_set_variable: { allOf: ["variable-sets:write", "secrets:write"] },
|
|
294
|
+
capability_catalog_search: { sessionRequired: true, allOf: ["workspace:read"] },
|
|
295
|
+
capability_authorization_request: {
|
|
296
|
+
sessionRequired: true,
|
|
297
|
+
allOf: ["workspace:read"],
|
|
298
|
+
},
|
|
233
299
|
github_connect_link: { allOf: ["github:use"] },
|
|
234
300
|
github_repositories_list: { allOf: ["github:use"] },
|
|
235
301
|
social_connections_list: { allOf: ["connections:read"] },
|
|
@@ -244,15 +310,33 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
244
310
|
// Publishes under the user's identity: connections:write keeps it out of the
|
|
245
311
|
// default agent permission set, unlike the read-only social tools above.
|
|
246
312
|
social_post_reply: { allOf: ["connections:write"] },
|
|
247
|
-
|
|
248
|
-
|
|
313
|
+
x_accounts_list: { allOf: ["connections:read"] },
|
|
314
|
+
x_search_live: { allOf: ["connections:read"] },
|
|
315
|
+
x_mentions_live: { allOf: ["connections:read"] },
|
|
316
|
+
x_thread_fetch: { allOf: ["connections:read"] },
|
|
317
|
+
x_posts_sync: { allOf: ["connections:write"] },
|
|
318
|
+
x_post_reply: { allOf: ["connections:write"] },
|
|
319
|
+
reddit_accounts_list: { allOf: ["connections:read"] },
|
|
320
|
+
reddit_search_live: { allOf: ["connections:read"] },
|
|
321
|
+
reddit_mentions_live: { allOf: ["connections:read"] },
|
|
322
|
+
reddit_thread_fetch: { allOf: ["connections:read"] },
|
|
323
|
+
reddit_posts_sync: { allOf: ["connections:write"] },
|
|
324
|
+
reddit_post_reply: { allOf: ["connections:write"] },
|
|
325
|
+
scheduled_tasks_list: {
|
|
326
|
+
anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
|
|
327
|
+
},
|
|
328
|
+
scheduled_tasks_get: {
|
|
329
|
+
anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
|
|
330
|
+
},
|
|
249
331
|
scheduled_tasks_create: { allOf: ["scheduled_tasks:manage"] },
|
|
250
332
|
scheduled_tasks_update: { allOf: ["scheduled_tasks:manage"] },
|
|
251
333
|
scheduled_tasks_pause: { allOf: ["scheduled_tasks:manage"] },
|
|
252
334
|
scheduled_tasks_resume: { allOf: ["scheduled_tasks:manage"] },
|
|
253
335
|
scheduled_tasks_trigger: { allOf: ["scheduled_tasks:run"] },
|
|
254
336
|
scheduled_tasks_delete: { allOf: ["scheduled_tasks:manage"] },
|
|
255
|
-
scheduled_task_runs_list: {
|
|
337
|
+
scheduled_task_runs_list: {
|
|
338
|
+
anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
|
|
339
|
+
},
|
|
256
340
|
slack_bot_list_channels: { allOf: ["connections:read"] },
|
|
257
341
|
slack_bot_channel_history: { allOf: ["connections:read"] },
|
|
258
342
|
slack_bot_thread_replies: { allOf: ["connections:read"] },
|
|
@@ -262,11 +346,31 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
262
346
|
slack_bot_file_content: { allOf: ["connections:read"] },
|
|
263
347
|
slack_bot_post_message: { allOf: ["connections:read"] },
|
|
264
348
|
slack_bot_delete_message: { allOf: ["connections:read"] },
|
|
349
|
+
atlassian_sources_list: { allOf: ["connections:read"] },
|
|
350
|
+
atlassian_search: { allOf: ["connections:read"] },
|
|
351
|
+
atlassian_get: { allOf: ["connections:read"] },
|
|
265
352
|
artifacts_list: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
266
353
|
artifacts_get_source: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
267
354
|
artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
268
355
|
artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
269
356
|
artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
357
|
+
editable_artifact_list: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
358
|
+
editable_artifact_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
359
|
+
editable_artifact_import: {
|
|
360
|
+
sessionRequired: true,
|
|
361
|
+
allOf: ["artifacts:publish", "files:read"],
|
|
362
|
+
},
|
|
363
|
+
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"] },
|
|
366
|
+
editable_artifact_export: {
|
|
367
|
+
sessionRequired: true,
|
|
368
|
+
allOf: ["artifacts:read"],
|
|
369
|
+
},
|
|
370
|
+
editable_artifact_export_status: {
|
|
371
|
+
sessionRequired: true,
|
|
372
|
+
allOf: ["artifacts:read", "files:upload"],
|
|
373
|
+
},
|
|
270
374
|
} satisfies Record<FirstPartyMcpToolName, FirstPartyToolAuthorization>;
|
|
271
375
|
|
|
272
376
|
const FIRST_PARTY_MCP_TOOL_NAME_SET = new Set<string>(FIRST_PARTY_MCP_TOOL_NAMES);
|
|
@@ -278,7 +382,6 @@ class PolicyMcpServer extends McpServer {
|
|
|
278
382
|
private readonly grant: AccessGrant,
|
|
279
383
|
private readonly sessionId: string | null,
|
|
280
384
|
private readonly selectedTools: ReadonlySet<FirstPartyMcpToolName> | null,
|
|
281
|
-
private readonly allowUncatalogued: boolean,
|
|
282
385
|
) {
|
|
283
386
|
super({ name: "opengeni", version: "1.0.0" });
|
|
284
387
|
}
|
|
@@ -299,7 +402,7 @@ class PolicyMcpServer extends McpServer {
|
|
|
299
402
|
cb: ToolCallback<InputArgs>,
|
|
300
403
|
): RegisteredTool {
|
|
301
404
|
const catalogued = FIRST_PARTY_MCP_TOOL_NAME_SET.has(name);
|
|
302
|
-
let admitted =
|
|
405
|
+
let admitted = false;
|
|
303
406
|
if (catalogued) {
|
|
304
407
|
const toolName = name as FirstPartyMcpToolName;
|
|
305
408
|
const policy: FirstPartyToolAuthorization = FIRST_PARTY_TOOL_AUTHORIZATION[toolName];
|
|
@@ -356,7 +459,6 @@ export function buildOpenGeniMcpServer(
|
|
|
356
459
|
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
357
460
|
});
|
|
358
461
|
const can = (permission: Permission) => hasPermission(grant.permissions, permission);
|
|
359
|
-
const toolspaceMode = options.toolspace != null;
|
|
360
462
|
let socialConnectionsPromise: ReturnType<typeof authorizedSocialConnectionsForGrant> | undefined;
|
|
361
463
|
const authorizedSocialConnections = () =>
|
|
362
464
|
(socialConnectionsPromise ??= authorizedSocialConnectionsForGrant({
|
|
@@ -371,6 +473,14 @@ export function buildOpenGeniMcpServer(
|
|
|
371
473
|
if (!authority) throw new Error(`Unknown or unavailable social connection: ${connectionId}`);
|
|
372
474
|
return authority;
|
|
373
475
|
};
|
|
476
|
+
const requireAuthorizedSocialConnectionForProvider = async (
|
|
477
|
+
provider: "x" | "reddit",
|
|
478
|
+
connectionId: string,
|
|
479
|
+
) => {
|
|
480
|
+
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
481
|
+
assertSocialConnectionProvider(authority.connection, provider);
|
|
482
|
+
return authority;
|
|
483
|
+
};
|
|
374
484
|
|
|
375
485
|
// Session-scoped tools key off the worker-asserted sessionId claim (signed
|
|
376
486
|
// into the delegated token by the worker, never agent-controlled).
|
|
@@ -379,17 +489,17 @@ export function buildOpenGeniMcpServer(
|
|
|
379
489
|
? (grant.metadata["sessionId"] as string)
|
|
380
490
|
: null;
|
|
381
491
|
const selectedTools =
|
|
382
|
-
sessionId !== null
|
|
492
|
+
sessionId !== null
|
|
383
493
|
? new Set(
|
|
384
494
|
(grant.metadata?.["firstPartyMcpTools"] as FirstPartyMcpToolName[] | undefined) ??
|
|
385
495
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
386
496
|
)
|
|
387
497
|
: null;
|
|
388
|
-
const server = new PolicyMcpServer(grant, sessionId, selectedTools
|
|
498
|
+
const server = new PolicyMcpServer(grant, sessionId, selectedTools);
|
|
389
499
|
// set_session_title names the agent's OWN session — pure session metadata,
|
|
390
500
|
// not a goal operation — so it is available on every session, gated only on
|
|
391
501
|
// the signed sessionId (NOT goals:manage, and NOT on a goal existing).
|
|
392
|
-
if (sessionId !== null
|
|
502
|
+
if (sessionId !== null) {
|
|
393
503
|
server.registerTool(
|
|
394
504
|
"set_session_title",
|
|
395
505
|
{
|
|
@@ -412,18 +522,23 @@ export function buildOpenGeniMcpServer(
|
|
|
412
522
|
if (sessionId !== null && can("goals:manage")) {
|
|
413
523
|
registerGoalTools(server, deps, grant, sessionId, json);
|
|
414
524
|
}
|
|
415
|
-
|
|
416
|
-
// normal first-party worker token, a bare toolspace:call token does not see
|
|
417
|
-
// unpermissioned session tools; memory follows that title/goal parity and
|
|
418
|
-
// stays on the normal first-party MCP surface only.
|
|
419
|
-
if (!toolspaceMode && sessionId !== null && options.workspaceMemoryEnabled === true) {
|
|
525
|
+
if (sessionId !== null && options.workspaceMemoryEnabled === true) {
|
|
420
526
|
registerMemoryTools(server, deps, grant, sessionId, json);
|
|
421
527
|
}
|
|
422
|
-
if (
|
|
528
|
+
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
423
529
|
registerPreferenceRegistryTools(server, deps, grant, json);
|
|
424
530
|
}
|
|
425
|
-
if (
|
|
531
|
+
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
426
532
|
registerWorkspaceArtifactTools(server, deps, grant, sessionId, json);
|
|
533
|
+
registerEditableArtifactAgentTools({
|
|
534
|
+
server,
|
|
535
|
+
deps,
|
|
536
|
+
grant,
|
|
537
|
+
sessionId,
|
|
538
|
+
authorize: async () => {
|
|
539
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
540
|
+
},
|
|
541
|
+
});
|
|
427
542
|
}
|
|
428
543
|
|
|
429
544
|
// Fleet tools (M7 bring-your-own-compute): list / attach / swap / run_on /
|
|
@@ -432,13 +547,15 @@ export function buildOpenGeniMcpServer(
|
|
|
432
547
|
// so they register only when the grant carries the worker-signed sessionId claim
|
|
433
548
|
// (never agent-controlled). Gated on the selfhosted feature flag: the active
|
|
434
549
|
// pointer + swap are only meaningful when bring-your-own-compute is enabled.
|
|
435
|
-
if (
|
|
550
|
+
if (sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
|
|
436
551
|
registerFleetTools(server, deps, grant, sessionId, json);
|
|
437
552
|
}
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
registerSlackBotTools(server, deps, grant, sessionId, json);
|
|
553
|
+
if (can("enrollments:manage") && deps.settings.sandboxSelfhostedEnabled) {
|
|
554
|
+
registerConnectedMachineTools(server, deps, grant, json);
|
|
441
555
|
}
|
|
556
|
+
registerRigTools(server, deps, grant, can, sessionId, json);
|
|
557
|
+
registerSlackBotTools(server, deps, grant, sessionId, json);
|
|
558
|
+
registerAtlassianTools(server, deps, grant, json);
|
|
442
559
|
|
|
443
560
|
// Orchestration, variableSet, and GitHub status tools are permission-gated
|
|
444
561
|
// at registration: a grant without the permission does not see the tool.
|
|
@@ -451,13 +568,16 @@ export function buildOpenGeniMcpServer(
|
|
|
451
568
|
// never returned through a model-visible MCP tool. A user DEMOTES a specific
|
|
452
569
|
// session by setting a narrower session.firstPartyMcpPermissions (capped to
|
|
453
570
|
// the creator's own grant); operators still cap what any session can be given.
|
|
454
|
-
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId,
|
|
455
|
-
registerVariableSetTools(server, deps, grant, can, json);
|
|
571
|
+
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, json);
|
|
572
|
+
registerVariableSetTools(server, deps, grant, can, sessionId, json);
|
|
573
|
+
if (sessionId !== null && can("workspace:read")) {
|
|
574
|
+
registerCapabilityDiscoveryTools(server, deps, grant, sessionId, json);
|
|
575
|
+
}
|
|
456
576
|
if (can("github:use")) {
|
|
457
577
|
registerGitHubConnectTool(server, deps, grant, options, json);
|
|
458
578
|
}
|
|
459
579
|
|
|
460
|
-
if (
|
|
580
|
+
if (can("github:use")) {
|
|
461
581
|
server.registerTool(
|
|
462
582
|
"github_repositories_list",
|
|
463
583
|
{
|
|
@@ -486,7 +606,7 @@ export function buildOpenGeniMcpServer(
|
|
|
486
606
|
);
|
|
487
607
|
}
|
|
488
608
|
|
|
489
|
-
if (
|
|
609
|
+
if (can("connections:read")) {
|
|
490
610
|
server.registerTool(
|
|
491
611
|
"social_connections_list",
|
|
492
612
|
{
|
|
@@ -620,10 +740,17 @@ export function buildOpenGeniMcpServer(
|
|
|
620
740
|
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
621
741
|
const result = await socialSearchLive(
|
|
622
742
|
deps,
|
|
623
|
-
{
|
|
743
|
+
{
|
|
744
|
+
workspaceId: grant.workspaceId,
|
|
745
|
+
connectionId,
|
|
746
|
+
subjectId: authority.subjectId,
|
|
747
|
+
},
|
|
624
748
|
{ query, subreddit, limit },
|
|
625
749
|
);
|
|
626
|
-
return json({
|
|
750
|
+
return json({
|
|
751
|
+
provider: result.connection.provider,
|
|
752
|
+
posts: result.posts,
|
|
753
|
+
});
|
|
627
754
|
},
|
|
628
755
|
);
|
|
629
756
|
|
|
@@ -642,10 +769,17 @@ export function buildOpenGeniMcpServer(
|
|
|
642
769
|
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
643
770
|
const result = await socialMentionsLive(
|
|
644
771
|
deps,
|
|
645
|
-
{
|
|
772
|
+
{
|
|
773
|
+
workspaceId: grant.workspaceId,
|
|
774
|
+
connectionId,
|
|
775
|
+
subjectId: authority.subjectId,
|
|
776
|
+
},
|
|
646
777
|
{ sinceId, limit },
|
|
647
778
|
);
|
|
648
|
-
return json({
|
|
779
|
+
return json({
|
|
780
|
+
provider: result.connection.provider,
|
|
781
|
+
posts: result.posts,
|
|
782
|
+
});
|
|
649
783
|
},
|
|
650
784
|
);
|
|
651
785
|
|
|
@@ -664,18 +798,135 @@ export function buildOpenGeniMcpServer(
|
|
|
664
798
|
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
665
799
|
const result = await socialThreadLive(
|
|
666
800
|
deps,
|
|
667
|
-
{
|
|
801
|
+
{
|
|
802
|
+
workspaceId: grant.workspaceId,
|
|
803
|
+
connectionId,
|
|
804
|
+
subjectId: authority.subjectId,
|
|
805
|
+
},
|
|
668
806
|
{ id, limit },
|
|
669
807
|
);
|
|
670
|
-
return json({
|
|
808
|
+
return json({
|
|
809
|
+
provider: result.connection.provider,
|
|
810
|
+
posts: result.posts,
|
|
811
|
+
});
|
|
671
812
|
},
|
|
672
813
|
);
|
|
814
|
+
|
|
815
|
+
// Provider-scoped aliases are the canonical tools advertised by the X and
|
|
816
|
+
// Reddit Integration cards. The legacy social_* names remain available to
|
|
817
|
+
// existing Packs/sessions, but these names bind the provider in the tool
|
|
818
|
+
// identity and reject a near-identical Connection from the other adapter.
|
|
819
|
+
for (const provider of ["x", "reddit"] as const) {
|
|
820
|
+
const providerName = provider === "x" ? "X" : "Reddit";
|
|
821
|
+
server.registerTool(
|
|
822
|
+
`${provider}_accounts_list`,
|
|
823
|
+
{
|
|
824
|
+
description: `List the exact visible ${providerName} accounts available to this work.`,
|
|
825
|
+
inputSchema: { limit: z4.number().int().positive().optional() },
|
|
826
|
+
},
|
|
827
|
+
async ({ limit }) =>
|
|
828
|
+
json({
|
|
829
|
+
connections: (await authorizedSocialConnections())
|
|
830
|
+
.filter(
|
|
831
|
+
({ connection }) =>
|
|
832
|
+
connection.provider === provider && connection.status !== "disabled",
|
|
833
|
+
)
|
|
834
|
+
.slice(0, boundedMcpLimit(limit))
|
|
835
|
+
.map(({ connection }) => connection),
|
|
836
|
+
}),
|
|
837
|
+
);
|
|
838
|
+
server.registerTool(
|
|
839
|
+
`${provider}_search_live`,
|
|
840
|
+
{
|
|
841
|
+
description:
|
|
842
|
+
provider === "x"
|
|
843
|
+
? "Search recent X conversations through one exact connected X account."
|
|
844
|
+
: "Search Reddit through one exact connected Reddit account; optionally scope to a subreddit.",
|
|
845
|
+
inputSchema: {
|
|
846
|
+
connectionId: z4.string().uuid(),
|
|
847
|
+
query: z4.string().min(1).max(512),
|
|
848
|
+
subreddit: z4.string().min(1).max(100).optional(),
|
|
849
|
+
limit: z4.number().int().positive().optional(),
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
async ({ connectionId, query, subreddit, limit }) => {
|
|
853
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
854
|
+
provider,
|
|
855
|
+
connectionId,
|
|
856
|
+
);
|
|
857
|
+
const result = await socialSearchLive(
|
|
858
|
+
deps,
|
|
859
|
+
{
|
|
860
|
+
workspaceId: grant.workspaceId,
|
|
861
|
+
connectionId,
|
|
862
|
+
subjectId: authority.subjectId,
|
|
863
|
+
},
|
|
864
|
+
{ query, subreddit, limit },
|
|
865
|
+
);
|
|
866
|
+
return json({ provider: result.connection.provider, posts: result.posts });
|
|
867
|
+
},
|
|
868
|
+
);
|
|
869
|
+
server.registerTool(
|
|
870
|
+
`${provider}_mentions_live`,
|
|
871
|
+
{
|
|
872
|
+
description: `Fetch live ${providerName} mentions and replies through one exact connected account.`,
|
|
873
|
+
inputSchema: {
|
|
874
|
+
connectionId: z4.string().uuid(),
|
|
875
|
+
sinceId: z4.string().optional(),
|
|
876
|
+
limit: z4.number().int().positive().optional(),
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
async ({ connectionId, sinceId, limit }) => {
|
|
880
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
881
|
+
provider,
|
|
882
|
+
connectionId,
|
|
883
|
+
);
|
|
884
|
+
const result = await socialMentionsLive(
|
|
885
|
+
deps,
|
|
886
|
+
{
|
|
887
|
+
workspaceId: grant.workspaceId,
|
|
888
|
+
connectionId,
|
|
889
|
+
subjectId: authority.subjectId,
|
|
890
|
+
},
|
|
891
|
+
{ sinceId, limit },
|
|
892
|
+
);
|
|
893
|
+
return json({ provider: result.connection.provider, posts: result.posts });
|
|
894
|
+
},
|
|
895
|
+
);
|
|
896
|
+
server.registerTool(
|
|
897
|
+
`${provider}_thread_fetch`,
|
|
898
|
+
{
|
|
899
|
+
description: `Fetch one live ${providerName} conversation thread through an exact connected account.`,
|
|
900
|
+
inputSchema: {
|
|
901
|
+
connectionId: z4.string().uuid(),
|
|
902
|
+
id: z4.string().min(1).max(100),
|
|
903
|
+
limit: z4.number().int().positive().optional(),
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
async ({ connectionId, id, limit }) => {
|
|
907
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
908
|
+
provider,
|
|
909
|
+
connectionId,
|
|
910
|
+
);
|
|
911
|
+
const result = await socialThreadLive(
|
|
912
|
+
deps,
|
|
913
|
+
{
|
|
914
|
+
workspaceId: grant.workspaceId,
|
|
915
|
+
connectionId,
|
|
916
|
+
subjectId: authority.subjectId,
|
|
917
|
+
},
|
|
918
|
+
{ id, limit },
|
|
919
|
+
);
|
|
920
|
+
return json({ provider: result.connection.provider, posts: result.posts });
|
|
921
|
+
},
|
|
922
|
+
);
|
|
923
|
+
}
|
|
673
924
|
}
|
|
674
925
|
|
|
675
926
|
// Writes are gated on connections:write (never in the default first-party
|
|
676
927
|
// agent permission set) so scheduled tasks must opt in, and deployments can
|
|
677
928
|
// additionally wrap posting in a requireApproval policy.
|
|
678
|
-
if (
|
|
929
|
+
if (can("connections:write")) {
|
|
679
930
|
server.registerTool(
|
|
680
931
|
"social_posts_sync",
|
|
681
932
|
{
|
|
@@ -690,7 +941,11 @@ export function buildOpenGeniMcpServer(
|
|
|
690
941
|
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
691
942
|
const result = await socialOwnPostsLive(
|
|
692
943
|
deps,
|
|
693
|
-
{
|
|
944
|
+
{
|
|
945
|
+
workspaceId: grant.workspaceId,
|
|
946
|
+
connectionId,
|
|
947
|
+
subjectId: authority.subjectId,
|
|
948
|
+
},
|
|
694
949
|
{ limit },
|
|
695
950
|
);
|
|
696
951
|
// A post without a provider timestamp is skipped rather than recorded
|
|
@@ -736,7 +991,11 @@ export function buildOpenGeniMcpServer(
|
|
|
736
991
|
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
737
992
|
const result = await socialPostReply(
|
|
738
993
|
deps,
|
|
739
|
-
{
|
|
994
|
+
{
|
|
995
|
+
workspaceId: grant.workspaceId,
|
|
996
|
+
connectionId,
|
|
997
|
+
subjectId: authority.subjectId,
|
|
998
|
+
},
|
|
740
999
|
{ inReplyToId, text },
|
|
741
1000
|
);
|
|
742
1001
|
// Outbound publishes leave a durable, secret-free receipt (house
|
|
@@ -763,28 +1022,150 @@ export function buildOpenGeniMcpServer(
|
|
|
763
1022
|
});
|
|
764
1023
|
},
|
|
765
1024
|
);
|
|
1025
|
+
|
|
1026
|
+
for (const provider of ["x", "reddit"] as const) {
|
|
1027
|
+
const providerName = provider === "x" ? "X" : "Reddit";
|
|
1028
|
+
server.registerTool(
|
|
1029
|
+
`${provider}_posts_sync`,
|
|
1030
|
+
{
|
|
1031
|
+
description: `Sync one exact connected ${providerName} account's recent posts into OpenGeni (idempotent).`,
|
|
1032
|
+
inputSchema: {
|
|
1033
|
+
connectionId: z4.string().uuid(),
|
|
1034
|
+
limit: z4.number().int().positive().optional(),
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
async ({ connectionId, limit }) => {
|
|
1038
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
1039
|
+
provider,
|
|
1040
|
+
connectionId,
|
|
1041
|
+
);
|
|
1042
|
+
const result = await socialOwnPostsLive(
|
|
1043
|
+
deps,
|
|
1044
|
+
{
|
|
1045
|
+
workspaceId: grant.workspaceId,
|
|
1046
|
+
connectionId,
|
|
1047
|
+
subjectId: authority.subjectId,
|
|
1048
|
+
},
|
|
1049
|
+
{ limit },
|
|
1050
|
+
);
|
|
1051
|
+
const datedPosts = result.posts.filter((post) => post.createdAt !== null);
|
|
1052
|
+
const synced = await recordSyncedSocialPosts(deps.db, {
|
|
1053
|
+
accountId: grant.accountId,
|
|
1054
|
+
workspaceId: grant.workspaceId,
|
|
1055
|
+
connectionId,
|
|
1056
|
+
subjectId: authority.subjectId,
|
|
1057
|
+
posts: datedPosts.map((post) => ({
|
|
1058
|
+
externalPostId: post.id,
|
|
1059
|
+
url: post.url,
|
|
1060
|
+
authorHandle: post.author,
|
|
1061
|
+
text: post.text,
|
|
1062
|
+
publishedAt: new Date(post.createdAt!),
|
|
1063
|
+
metrics: post.metrics,
|
|
1064
|
+
})),
|
|
1065
|
+
});
|
|
1066
|
+
return json({
|
|
1067
|
+
provider: result.connection.provider,
|
|
1068
|
+
fetched: result.posts.length,
|
|
1069
|
+
inserted: synced.inserted,
|
|
1070
|
+
skipped: synced.skipped,
|
|
1071
|
+
skippedMissingDate: result.posts.length - datedPosts.length,
|
|
1072
|
+
});
|
|
1073
|
+
},
|
|
1074
|
+
);
|
|
1075
|
+
server.registerTool(
|
|
1076
|
+
`${provider}_post_reply`,
|
|
1077
|
+
{
|
|
1078
|
+
description: `Publish a reply from one exact connected ${providerName} account. This is a public write and should require approval.`,
|
|
1079
|
+
inputSchema: {
|
|
1080
|
+
connectionId: z4.string().uuid(),
|
|
1081
|
+
inReplyToId: z4.string().min(1).max(100),
|
|
1082
|
+
text: z4.string().min(1).max(10000),
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
async ({ connectionId, inReplyToId, text }) => {
|
|
1086
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
1087
|
+
provider,
|
|
1088
|
+
connectionId,
|
|
1089
|
+
);
|
|
1090
|
+
const result = await socialPostReply(
|
|
1091
|
+
deps,
|
|
1092
|
+
{
|
|
1093
|
+
workspaceId: grant.workspaceId,
|
|
1094
|
+
connectionId,
|
|
1095
|
+
subjectId: authority.subjectId,
|
|
1096
|
+
},
|
|
1097
|
+
{ inReplyToId, text },
|
|
1098
|
+
);
|
|
1099
|
+
await recordAuditEvent(deps.db, {
|
|
1100
|
+
accountId: grant.accountId,
|
|
1101
|
+
workspaceId: grant.workspaceId,
|
|
1102
|
+
subjectId: grant.subjectId,
|
|
1103
|
+
action: "social.post_reply",
|
|
1104
|
+
targetType: "social_connection",
|
|
1105
|
+
targetId: connectionId,
|
|
1106
|
+
metadata: {
|
|
1107
|
+
provider: result.connection.provider,
|
|
1108
|
+
adapterTool: `${provider}_post_reply`,
|
|
1109
|
+
inReplyToId,
|
|
1110
|
+
postedId: result.postedId,
|
|
1111
|
+
url: result.url,
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
return json({
|
|
1115
|
+
provider: result.connection.provider,
|
|
1116
|
+
postedId: result.postedId,
|
|
1117
|
+
url: result.url,
|
|
1118
|
+
});
|
|
1119
|
+
},
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
766
1122
|
}
|
|
767
1123
|
|
|
768
|
-
if (
|
|
1124
|
+
if (can("scheduled_tasks:manage") || can("scheduled_tasks:run")) {
|
|
769
1125
|
server.registerTool(
|
|
770
1126
|
"scheduled_tasks_list",
|
|
771
1127
|
{
|
|
772
|
-
description:
|
|
773
|
-
|
|
1128
|
+
description:
|
|
1129
|
+
"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.",
|
|
1130
|
+
inputSchema: {
|
|
1131
|
+
limit: z4.number().int().positive().max(50).optional(),
|
|
1132
|
+
offset: z4.number().int().nonnegative().max(10_000).optional(),
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
async ({ limit: requestedLimit, offset: requestedOffset }) => {
|
|
1136
|
+
const limit = requestedLimit ?? 25;
|
|
1137
|
+
const offset = requestedOffset ?? 0;
|
|
1138
|
+
const rows = await listScheduledTasks(deps.db, grant.workspaceId, limit + 1, offset);
|
|
1139
|
+
return json(
|
|
1140
|
+
boundScheduledTaskMcpPage({
|
|
1141
|
+
tasks: rows.slice(0, limit).map((task) => scheduledTaskForGrant(task, grant)),
|
|
1142
|
+
limit,
|
|
1143
|
+
offset,
|
|
1144
|
+
sourceHasMore: rows.length > limit,
|
|
1145
|
+
}),
|
|
1146
|
+
);
|
|
774
1147
|
},
|
|
775
|
-
async ({ limit }) =>
|
|
776
|
-
json({
|
|
777
|
-
tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100),
|
|
778
|
-
}),
|
|
779
1148
|
);
|
|
780
1149
|
|
|
781
1150
|
server.registerTool(
|
|
782
1151
|
"scheduled_tasks_get",
|
|
783
1152
|
{
|
|
784
|
-
description:
|
|
785
|
-
|
|
1153
|
+
description:
|
|
1154
|
+
"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.",
|
|
1155
|
+
inputSchema: {
|
|
1156
|
+
id: z4.string().uuid(),
|
|
1157
|
+
includeEntity: z4.boolean().optional(),
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
async ({ id, includeEntity }) => {
|
|
1161
|
+
const task = scheduledTaskForGrant(
|
|
1162
|
+
await requireScheduledTask(deps.db, grant.workspaceId, id),
|
|
1163
|
+
grant,
|
|
1164
|
+
);
|
|
1165
|
+
return json(
|
|
1166
|
+
includeEntity ? boundScheduledTaskDetailMcp(task) : scheduledTaskMcpSummary(task),
|
|
1167
|
+
);
|
|
786
1168
|
},
|
|
787
|
-
async ({ id }) => json(await requireScheduledTask(deps.db, grant.workspaceId, id)),
|
|
788
1169
|
);
|
|
789
1170
|
|
|
790
1171
|
server.registerTool(
|
|
@@ -795,6 +1176,7 @@ export function buildOpenGeniMcpServer(
|
|
|
795
1176
|
name: z4.string(),
|
|
796
1177
|
schedule: z4.unknown(),
|
|
797
1178
|
runMode: z4.string().optional(),
|
|
1179
|
+
targetSessionId: z4.string().uuid().nullable().optional(),
|
|
798
1180
|
overlapPolicy: z4.string().optional(),
|
|
799
1181
|
agentConfig: z4.unknown(),
|
|
800
1182
|
status: z4.string().optional(),
|
|
@@ -823,13 +1205,29 @@ export function buildOpenGeniMcpServer(
|
|
|
823
1205
|
grant,
|
|
824
1206
|
payload,
|
|
825
1207
|
toolsProvided: scheduledTaskToolsProvided(args),
|
|
1208
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
1209
|
+
authorizationSurface: "first_party_mcp",
|
|
826
1210
|
});
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1211
|
+
try {
|
|
1212
|
+
await syncCreatedScheduledTask({
|
|
1213
|
+
db: deps.db,
|
|
1214
|
+
workflowClient: deps.workflowClient,
|
|
1215
|
+
task,
|
|
1216
|
+
});
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
if (!(error instanceof ScheduledTaskSyncError) || error.persistenceRestored) {
|
|
1219
|
+
throw error;
|
|
1220
|
+
}
|
|
1221
|
+
return json(
|
|
1222
|
+
scheduledTaskReceipt("scheduled_tasks_create", task, "partial_failure", true, {
|
|
1223
|
+
partialFailure: { stage: "schedule_sync", retryable: true },
|
|
1224
|
+
warnings: [
|
|
1225
|
+
"The task database record committed, but Temporal schedule synchronization failed.",
|
|
1226
|
+
],
|
|
1227
|
+
}),
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
return json(scheduledTaskReceipt("scheduled_tasks_create", task, "created", true));
|
|
833
1231
|
},
|
|
834
1232
|
);
|
|
835
1233
|
|
|
@@ -842,6 +1240,7 @@ export function buildOpenGeniMcpServer(
|
|
|
842
1240
|
name: z4.string().optional(),
|
|
843
1241
|
schedule: z4.unknown().optional(),
|
|
844
1242
|
runMode: z4.string().optional(),
|
|
1243
|
+
targetSessionId: z4.string().uuid().nullable().optional(),
|
|
845
1244
|
overlapPolicy: z4.string().optional(),
|
|
846
1245
|
agentConfig: z4.unknown().optional(),
|
|
847
1246
|
status: z4.string().optional(),
|
|
@@ -867,7 +1266,12 @@ export function buildOpenGeniMcpServer(
|
|
|
867
1266
|
existing,
|
|
868
1267
|
payload,
|
|
869
1268
|
toolsProvided: scheduledTaskToolsProvided(raw),
|
|
1269
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
1270
|
+
authorizationSurface: "first_party_mcp",
|
|
870
1271
|
});
|
|
1272
|
+
if (!scheduledTaskUpdateChangesState(existing, update)) {
|
|
1273
|
+
return json(scheduledTaskReceipt("scheduled_tasks_update", existing, "unchanged", false));
|
|
1274
|
+
}
|
|
871
1275
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, update);
|
|
872
1276
|
await syncUpdatedScheduledTask({
|
|
873
1277
|
db: deps.db,
|
|
@@ -875,7 +1279,7 @@ export function buildOpenGeniMcpServer(
|
|
|
875
1279
|
previous,
|
|
876
1280
|
task,
|
|
877
1281
|
});
|
|
878
|
-
return json(task);
|
|
1282
|
+
return json(scheduledTaskReceipt("scheduled_tasks_update", task, "updated", true));
|
|
879
1283
|
},
|
|
880
1284
|
);
|
|
881
1285
|
|
|
@@ -887,6 +1291,9 @@ export function buildOpenGeniMcpServer(
|
|
|
887
1291
|
},
|
|
888
1292
|
async ({ id }) => {
|
|
889
1293
|
const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1294
|
+
if (existing.status === "paused") {
|
|
1295
|
+
return json(scheduledTaskReceipt("scheduled_tasks_pause", existing, "unchanged", false));
|
|
1296
|
+
}
|
|
890
1297
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
891
1298
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
|
|
892
1299
|
status: "paused",
|
|
@@ -897,7 +1304,7 @@ export function buildOpenGeniMcpServer(
|
|
|
897
1304
|
previous,
|
|
898
1305
|
task,
|
|
899
1306
|
});
|
|
900
|
-
return json(task);
|
|
1307
|
+
return json(scheduledTaskReceipt("scheduled_tasks_pause", task, "updated", true));
|
|
901
1308
|
},
|
|
902
1309
|
);
|
|
903
1310
|
|
|
@@ -909,6 +1316,9 @@ export function buildOpenGeniMcpServer(
|
|
|
909
1316
|
},
|
|
910
1317
|
async ({ id }) => {
|
|
911
1318
|
const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1319
|
+
if (existing.status === "active") {
|
|
1320
|
+
return json(scheduledTaskReceipt("scheduled_tasks_resume", existing, "unchanged", false));
|
|
1321
|
+
}
|
|
912
1322
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
913
1323
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
|
|
914
1324
|
status: "active",
|
|
@@ -919,7 +1329,7 @@ export function buildOpenGeniMcpServer(
|
|
|
919
1329
|
previous,
|
|
920
1330
|
task,
|
|
921
1331
|
});
|
|
922
|
-
return json(task);
|
|
1332
|
+
return json(scheduledTaskReceipt("scheduled_tasks_resume", task, "updated", true));
|
|
923
1333
|
},
|
|
924
1334
|
);
|
|
925
1335
|
|
|
@@ -935,19 +1345,32 @@ export function buildOpenGeniMcpServer(
|
|
|
935
1345
|
},
|
|
936
1346
|
async ({ id, triggerId }) => {
|
|
937
1347
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1348
|
+
if (task.action.kind === "agent_turn") {
|
|
1349
|
+
await validateScheduledTaskTarget({
|
|
1350
|
+
db: deps.db,
|
|
1351
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
1352
|
+
authorizationSurface: "first_party_mcp",
|
|
1353
|
+
grant,
|
|
1354
|
+
targetSessionId: task.targetSessionId,
|
|
1355
|
+
runMode: task.runMode,
|
|
1356
|
+
variableSetId: task.variableSetId,
|
|
1357
|
+
rigId: task.rigId,
|
|
1358
|
+
agentConfig: task.agentConfig,
|
|
1359
|
+
missingTargetStatus: 404,
|
|
1360
|
+
});
|
|
1361
|
+
await requireLimit(deps, {
|
|
1362
|
+
accountId: grant.accountId,
|
|
1363
|
+
workspaceId: grant.workspaceId,
|
|
1364
|
+
action: "agent_run:create",
|
|
1365
|
+
quantity: 1,
|
|
1366
|
+
model: task.agentConfig.model ?? deps.settings.openaiModel,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
945
1369
|
const triggerToken = scheduledTaskTriggerToken(triggerId);
|
|
946
|
-
const agentRunUsageIdempotencyKey =
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
);
|
|
1370
|
+
const agentRunUsageIdempotencyKey =
|
|
1371
|
+
task.action.kind === "agent_turn"
|
|
1372
|
+
? manualScheduledTaskTriggerUsageKey(grant.workspaceId, task.id, triggerToken)
|
|
1373
|
+
: `knowledge-source-sync:manual:${grant.workspaceId}:${task.id}:${triggerToken}`;
|
|
951
1374
|
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
952
1375
|
await deps.workflowClient.triggerScheduledTask({
|
|
953
1376
|
task,
|
|
@@ -955,18 +1378,44 @@ export function buildOpenGeniMcpServer(
|
|
|
955
1378
|
triggerWorkflowId,
|
|
956
1379
|
initiator: { kind: "subject", subjectId: grant.subjectId },
|
|
957
1380
|
});
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1381
|
+
try {
|
|
1382
|
+
if (task.action.kind !== "agent_turn") {
|
|
1383
|
+
return json(
|
|
1384
|
+
scheduledTaskReceipt("scheduled_tasks_trigger", task, "triggered", true, {
|
|
1385
|
+
idempotencyStatus: triggerId ? "unknown" : "not_requested",
|
|
1386
|
+
facts: { triggerWorkflowId },
|
|
1387
|
+
}),
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
await recordWorkspaceUsage(deps, {
|
|
1391
|
+
accountId: grant.accountId,
|
|
1392
|
+
workspaceId: grant.workspaceId,
|
|
1393
|
+
subjectId: grant.subjectId,
|
|
1394
|
+
eventType: "agent_run.created",
|
|
1395
|
+
quantity: 1,
|
|
1396
|
+
unit: "run",
|
|
1397
|
+
sourceResourceType: "scheduled_task",
|
|
1398
|
+
sourceResourceId: task.id,
|
|
1399
|
+
idempotencyKey: agentRunUsageIdempotencyKey,
|
|
1400
|
+
});
|
|
1401
|
+
} catch {
|
|
1402
|
+
return json(
|
|
1403
|
+
scheduledTaskReceipt("scheduled_tasks_trigger", task, "partial_failure", true, {
|
|
1404
|
+
partialFailure: { stage: "usage_recording", retryable: true },
|
|
1405
|
+
warnings: [
|
|
1406
|
+
"The Temporal run trigger completed, but usage recording failed; retry with the same triggerId.",
|
|
1407
|
+
],
|
|
1408
|
+
idempotencyStatus: triggerId ? "unknown" : "not_requested",
|
|
1409
|
+
facts: { triggerWorkflowId },
|
|
1410
|
+
}),
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
return json(
|
|
1414
|
+
scheduledTaskReceipt("scheduled_tasks_trigger", task, "triggered", true, {
|
|
1415
|
+
idempotencyStatus: triggerId ? "unknown" : "not_requested",
|
|
1416
|
+
facts: { triggerWorkflowId },
|
|
1417
|
+
}),
|
|
1418
|
+
);
|
|
970
1419
|
},
|
|
971
1420
|
);
|
|
972
1421
|
|
|
@@ -978,11 +1427,42 @@ export function buildOpenGeniMcpServer(
|
|
|
978
1427
|
},
|
|
979
1428
|
async ({ id }) => {
|
|
980
1429
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1430
|
+
if (task.metadata.connectorKind === "atlassian") {
|
|
1431
|
+
await revokeAtlassianScheduleAuthorization(deps, {
|
|
1432
|
+
task,
|
|
1433
|
+
subjectId: grant.subjectId,
|
|
1434
|
+
});
|
|
1435
|
+
} else {
|
|
1436
|
+
await revokeKnowledgeSourceScheduleAuthorization(deps, {
|
|
1437
|
+
task,
|
|
1438
|
+
subjectId: grant.subjectId,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
981
1441
|
await deps.workflowClient.deleteScheduledTaskSchedule({
|
|
982
1442
|
temporalScheduleId: task.temporalScheduleId,
|
|
983
1443
|
});
|
|
984
|
-
|
|
985
|
-
|
|
1444
|
+
try {
|
|
1445
|
+
await deleteScheduledTask(deps.db, grant.workspaceId, id);
|
|
1446
|
+
} catch {
|
|
1447
|
+
return json(
|
|
1448
|
+
scheduledTaskReceipt("scheduled_tasks_delete", task, "partial_failure", true, {
|
|
1449
|
+
partialFailure: { stage: "database_delete", retryable: true },
|
|
1450
|
+
warnings: [
|
|
1451
|
+
"The Temporal schedule was deleted, but the task database record remains.",
|
|
1452
|
+
],
|
|
1453
|
+
}),
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
return json(
|
|
1457
|
+
mcpMutationReceipt({
|
|
1458
|
+
operation: "scheduled_tasks_delete",
|
|
1459
|
+
committed: true,
|
|
1460
|
+
outcome: "deleted",
|
|
1461
|
+
changed: true,
|
|
1462
|
+
resource: { type: "scheduled_task", id: task.id, state: "deleted" },
|
|
1463
|
+
idempotency: { status: "not_supported" },
|
|
1464
|
+
}),
|
|
1465
|
+
);
|
|
986
1466
|
},
|
|
987
1467
|
);
|
|
988
1468
|
|
|
@@ -997,12 +1477,13 @@ export function buildOpenGeniMcpServer(
|
|
|
997
1477
|
},
|
|
998
1478
|
async ({ taskId, limit }) =>
|
|
999
1479
|
json({
|
|
1000
|
-
runs: await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100)
|
|
1480
|
+
runs: (await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100)).map(
|
|
1481
|
+
(run) => scheduledTaskRunForGrant(run, grant),
|
|
1482
|
+
),
|
|
1001
1483
|
}),
|
|
1002
1484
|
);
|
|
1003
1485
|
}
|
|
1004
1486
|
|
|
1005
|
-
registerToolspaceProxyTools(server, options.toolspace ?? null);
|
|
1006
1487
|
server.ensureToolsListHandler();
|
|
1007
1488
|
|
|
1008
1489
|
return server;
|
|
@@ -1243,54 +1724,120 @@ function registerSlackBotTools(
|
|
|
1243
1724
|
);
|
|
1244
1725
|
}
|
|
1245
1726
|
|
|
1246
|
-
function
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
server
|
|
1257
|
-
.registerTool(
|
|
1258
|
-
"__opengeni_empty_toolspace_surface__",
|
|
1259
|
-
{
|
|
1260
|
-
description: "Internal disabled placeholder for an empty Toolspace surface.",
|
|
1261
|
-
inputSchema: z4.object({}),
|
|
1262
|
-
},
|
|
1263
|
-
async () => ({
|
|
1264
|
-
content: [{ type: "text" as const, text: '{"unavailable":true}' }],
|
|
1265
|
-
}),
|
|
1266
|
-
)
|
|
1267
|
-
.disable();
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
for (const tool of surface.tools) {
|
|
1271
|
-
server.registerTool(
|
|
1272
|
-
tool.name,
|
|
1273
|
-
{
|
|
1274
|
-
...(tool.description ? { description: tool.description } : {}),
|
|
1275
|
-
inputSchema: z4.object({}).passthrough(),
|
|
1276
|
-
_meta: {
|
|
1277
|
-
opengeni: {
|
|
1278
|
-
origin: "toolspace",
|
|
1279
|
-
subjectId: surface.subjectId,
|
|
1280
|
-
sessionId: surface.sessionId,
|
|
1281
|
-
...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
1282
|
-
},
|
|
1283
|
-
},
|
|
1284
|
-
},
|
|
1285
|
-
async (args) => await tool.call(args),
|
|
1727
|
+
function registerAtlassianTools(
|
|
1728
|
+
server: McpServer,
|
|
1729
|
+
deps: ApiRouteDeps,
|
|
1730
|
+
grant: AccessGrant,
|
|
1731
|
+
json: JsonResult,
|
|
1732
|
+
): void {
|
|
1733
|
+
const connectionFor = async (connectionId?: string) => {
|
|
1734
|
+
const authorized = await authorizedAtlassianConnectionsForGrant({ db: deps.db, grant });
|
|
1735
|
+
const candidates = authorized.filter(({ connection }) =>
|
|
1736
|
+
connectionId ? connection.id === connectionId : true,
|
|
1286
1737
|
);
|
|
1287
|
-
|
|
1288
|
-
|
|
1738
|
+
if (candidates.length === 0) {
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
connectionId
|
|
1741
|
+
? "the requested Atlassian connection is unavailable for this turn"
|
|
1742
|
+
: "no Atlassian connection is available for this turn",
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
if (!connectionId && candidates.length > 1) {
|
|
1746
|
+
throw new Error(
|
|
1747
|
+
"connectionId is required because multiple Atlassian connections are available",
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
const authority = candidates[0]!;
|
|
1751
|
+
const metadata = AtlassianConnectionMetadata.safeParse(authority.connection.metadata);
|
|
1752
|
+
if (!metadata.success) throw new Error("Atlassian connection metadata is invalid");
|
|
1753
|
+
return {
|
|
1754
|
+
connection: authority.connection,
|
|
1755
|
+
metadata: metadata.data,
|
|
1756
|
+
subjectId: authority.subjectId ?? grant.subjectId,
|
|
1757
|
+
};
|
|
1758
|
+
};
|
|
1289
1759
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1760
|
+
server.registerTool(
|
|
1761
|
+
"atlassian_sources_list",
|
|
1762
|
+
{
|
|
1763
|
+
description:
|
|
1764
|
+
"List the Jira projects and Confluence spaces available through the authorized Atlassian connection, including which sources are selected for OpenGeni. Use this before search when the site or boundary is unclear.",
|
|
1765
|
+
inputSchema: { connectionId: z4.string().uuid().optional() },
|
|
1766
|
+
},
|
|
1767
|
+
async ({ connectionId }) => {
|
|
1768
|
+
const authority = await connectionFor(connectionId);
|
|
1769
|
+
const response = await browseAtlassianSources(deps, {
|
|
1770
|
+
workspaceId: grant.workspaceId,
|
|
1771
|
+
subjectId: authority.subjectId,
|
|
1772
|
+
connectionId: authority.connection.id,
|
|
1773
|
+
});
|
|
1774
|
+
const selected = new Set(authority.metadata.selectedSources.map((source) => source.id));
|
|
1775
|
+
return json({
|
|
1776
|
+
connectionId: authority.connection.id,
|
|
1777
|
+
account: authority.metadata.displayName,
|
|
1778
|
+
items: response.items.map((item) => ({ ...item, selected: selected.has(item.id) })),
|
|
1779
|
+
});
|
|
1780
|
+
},
|
|
1781
|
+
);
|
|
1782
|
+
|
|
1783
|
+
server.registerTool(
|
|
1784
|
+
"atlassian_search",
|
|
1785
|
+
{
|
|
1786
|
+
description:
|
|
1787
|
+
"Search Jira issues and Confluence pages live within the projects and spaces selected for OpenGeni. Results reflect current Atlassian data and permissions, independent of the knowledge sync index.",
|
|
1788
|
+
inputSchema: {
|
|
1789
|
+
connectionId: z4.string().uuid().optional(),
|
|
1790
|
+
query: z4.string().min(1).max(500),
|
|
1791
|
+
product: z4.enum(["jira", "confluence"]).optional(),
|
|
1792
|
+
limit: z4.number().int().min(1).max(50).optional(),
|
|
1793
|
+
},
|
|
1794
|
+
},
|
|
1795
|
+
async ({ connectionId, query, product, limit }) => {
|
|
1796
|
+
const authority = await connectionFor(connectionId);
|
|
1797
|
+
return json({
|
|
1798
|
+
connectionId: authority.connection.id,
|
|
1799
|
+
results: await searchAtlassianLive(deps, {
|
|
1800
|
+
workspaceId: grant.workspaceId,
|
|
1801
|
+
subjectId: authority.subjectId,
|
|
1802
|
+
connectionId: authority.connection.id,
|
|
1803
|
+
query,
|
|
1804
|
+
...(product ? { product } : {}),
|
|
1805
|
+
limit: limit ?? 20,
|
|
1806
|
+
}),
|
|
1807
|
+
});
|
|
1808
|
+
},
|
|
1809
|
+
);
|
|
1810
|
+
|
|
1811
|
+
server.registerTool(
|
|
1812
|
+
"atlassian_get",
|
|
1813
|
+
{
|
|
1814
|
+
description:
|
|
1815
|
+
"Open one current Jira issue or Confluence page, including description or page content and comments. The item must belong to a project or space selected for OpenGeni.",
|
|
1816
|
+
inputSchema: {
|
|
1817
|
+
connectionId: z4.string().uuid().optional(),
|
|
1818
|
+
kind: z4.enum(["jira_issue", "confluence_page"]),
|
|
1819
|
+
id: z4.string().min(1).max(256),
|
|
1820
|
+
},
|
|
1821
|
+
},
|
|
1822
|
+
async ({ connectionId, kind, id }) => {
|
|
1823
|
+
const authority = await connectionFor(connectionId);
|
|
1824
|
+
return json(
|
|
1825
|
+
await getAtlassianLiveItem(deps, {
|
|
1826
|
+
workspaceId: grant.workspaceId,
|
|
1827
|
+
subjectId: authority.subjectId,
|
|
1828
|
+
connectionId: authority.connection.id,
|
|
1829
|
+
kind,
|
|
1830
|
+
id,
|
|
1831
|
+
}),
|
|
1832
|
+
);
|
|
1833
|
+
},
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/** Only a prompt explicitly supplied through the human/API channel may redirect a user-paused goal. */
|
|
1838
|
+
export function isHumanDirectedTurn(turn: { source: string }): boolean {
|
|
1839
|
+
return turn.source === "user" || turn.source === "api";
|
|
1840
|
+
}
|
|
1294
1841
|
|
|
1295
1842
|
/**
|
|
1296
1843
|
* Sacred user pause: a goal a human paused (pausedReason 'user_pause') must
|
|
@@ -1358,7 +1905,7 @@ function registerGoalTools(
|
|
|
1358
1905
|
? (grant.metadata["turnId"] as string)
|
|
1359
1906
|
: null;
|
|
1360
1907
|
await assertGoalReactivationAllowed(deps, grant.workspaceId, sessionId, callerTurnId);
|
|
1361
|
-
const { goal, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
1908
|
+
const { goal, replaced, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
1362
1909
|
accountId: grant.accountId,
|
|
1363
1910
|
workspaceId: grant.workspaceId,
|
|
1364
1911
|
sessionId,
|
|
@@ -1371,7 +1918,24 @@ function registerGoalTools(
|
|
|
1371
1918
|
if (events.length > 0) {
|
|
1372
1919
|
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
1373
1920
|
}
|
|
1374
|
-
return json(
|
|
1921
|
+
return json(
|
|
1922
|
+
mcpMutationReceipt({
|
|
1923
|
+
operation: "goal_set",
|
|
1924
|
+
committed: true,
|
|
1925
|
+
outcome: replaced ? "updated" : "created",
|
|
1926
|
+
changed: true,
|
|
1927
|
+
resource: {
|
|
1928
|
+
type: "session_goal",
|
|
1929
|
+
id: goal.id,
|
|
1930
|
+
version: goal.version,
|
|
1931
|
+
state: goal.status,
|
|
1932
|
+
},
|
|
1933
|
+
timestamp: goal.updatedAt,
|
|
1934
|
+
idempotency: { status: "not_supported" },
|
|
1935
|
+
facts: { replaced },
|
|
1936
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
1937
|
+
}),
|
|
1938
|
+
);
|
|
1375
1939
|
},
|
|
1376
1940
|
);
|
|
1377
1941
|
|
|
@@ -1441,10 +2005,27 @@ function registerGoalTools(
|
|
|
1441
2005
|
event: { type: "goal.completed", evidence },
|
|
1442
2006
|
},
|
|
1443
2007
|
);
|
|
2008
|
+
const changed = events.length > 0;
|
|
1444
2009
|
if (events.length > 0) {
|
|
1445
2010
|
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
1446
2011
|
}
|
|
1447
|
-
return json(
|
|
2012
|
+
return json(
|
|
2013
|
+
mcpMutationReceipt({
|
|
2014
|
+
operation: "goal_complete",
|
|
2015
|
+
committed: true,
|
|
2016
|
+
outcome: changed ? "updated" : "unchanged",
|
|
2017
|
+
changed,
|
|
2018
|
+
resource: {
|
|
2019
|
+
type: "session_goal",
|
|
2020
|
+
id: goal.id,
|
|
2021
|
+
version: goal.version,
|
|
2022
|
+
state: goal.status,
|
|
2023
|
+
},
|
|
2024
|
+
timestamp: goal.updatedAt,
|
|
2025
|
+
idempotency: { status: "not_supported" },
|
|
2026
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
2027
|
+
}),
|
|
2028
|
+
);
|
|
1448
2029
|
},
|
|
1449
2030
|
);
|
|
1450
2031
|
|
|
@@ -1478,10 +2059,27 @@ function registerGoalTools(
|
|
|
1478
2059
|
},
|
|
1479
2060
|
},
|
|
1480
2061
|
);
|
|
2062
|
+
const changed = events.length > 0;
|
|
1481
2063
|
if (events.length > 0) {
|
|
1482
2064
|
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
1483
2065
|
}
|
|
1484
|
-
return json(
|
|
2066
|
+
return json(
|
|
2067
|
+
mcpMutationReceipt({
|
|
2068
|
+
operation: "goal_pause",
|
|
2069
|
+
committed: true,
|
|
2070
|
+
outcome: changed ? "updated" : "unchanged",
|
|
2071
|
+
changed,
|
|
2072
|
+
resource: {
|
|
2073
|
+
type: "session_goal",
|
|
2074
|
+
id: goal.id,
|
|
2075
|
+
version: goal.version,
|
|
2076
|
+
state: goal.status,
|
|
2077
|
+
},
|
|
2078
|
+
timestamp: goal.updatedAt,
|
|
2079
|
+
idempotency: { status: "not_supported" },
|
|
2080
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
2081
|
+
}),
|
|
2082
|
+
);
|
|
1485
2083
|
},
|
|
1486
2084
|
);
|
|
1487
2085
|
}
|
|
@@ -1511,7 +2109,7 @@ function registerWorkspaceArtifactTools(
|
|
|
1511
2109
|
json: JsonResult,
|
|
1512
2110
|
): void {
|
|
1513
2111
|
const attempt = () => {
|
|
1514
|
-
const claims =
|
|
2112
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
1515
2113
|
if (!claims) throw new Error("Exact signed artifact attempt authority is required.");
|
|
1516
2114
|
return claims;
|
|
1517
2115
|
};
|
|
@@ -1599,7 +2197,11 @@ function registerWorkspaceArtifactTools(
|
|
|
1599
2197
|
const actualHash = createHash("sha256").update(object.bytes).digest("hex");
|
|
1600
2198
|
if (actualHash !== ref.version.contentSha256)
|
|
1601
2199
|
throw new Error("Artifact content failed integrity verification");
|
|
1602
|
-
return json({
|
|
2200
|
+
return json({
|
|
2201
|
+
detail,
|
|
2202
|
+
version: ref.version,
|
|
2203
|
+
html: new TextDecoder().decode(object.bytes),
|
|
2204
|
+
});
|
|
1603
2205
|
},
|
|
1604
2206
|
);
|
|
1605
2207
|
|
|
@@ -1695,7 +2297,7 @@ function registerWorkspaceArtifactTools(
|
|
|
1695
2297
|
);
|
|
1696
2298
|
}
|
|
1697
2299
|
|
|
1698
|
-
function
|
|
2300
|
+
function exactAgentAttemptClaims(grant: AccessGrant): {
|
|
1699
2301
|
sessionId: string;
|
|
1700
2302
|
turnId: string;
|
|
1701
2303
|
attemptId: string;
|
|
@@ -1728,7 +2330,7 @@ function registerPreferenceRegistryTools(
|
|
|
1728
2330
|
json: JsonResult,
|
|
1729
2331
|
): void {
|
|
1730
2332
|
const attemptClaims = () => {
|
|
1731
|
-
const resolved =
|
|
2333
|
+
const resolved = exactAgentAttemptClaims(grant);
|
|
1732
2334
|
if (!resolved) throw new Error("Exact signed preference attempt authority is required.");
|
|
1733
2335
|
return {
|
|
1734
2336
|
accountId: grant.accountId,
|
|
@@ -1761,11 +2363,101 @@ function registerPreferenceRegistryTools(
|
|
|
1761
2363
|
|
|
1762
2364
|
const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
|
|
1763
2365
|
|
|
2366
|
+
function scheduledTaskReceipt(
|
|
2367
|
+
operation: string,
|
|
2368
|
+
task: ScheduledTask,
|
|
2369
|
+
outcome: "created" | "updated" | "unchanged" | "triggered" | "partial_failure",
|
|
2370
|
+
changed: boolean,
|
|
2371
|
+
options: {
|
|
2372
|
+
partialFailure?: { stage: string; retryable: boolean };
|
|
2373
|
+
warnings?: string[];
|
|
2374
|
+
idempotencyStatus?: "not_supported" | "not_requested" | "applied" | "replayed" | "unknown";
|
|
2375
|
+
facts?: Record<string, string | number | boolean | null>;
|
|
2376
|
+
} = {},
|
|
2377
|
+
) {
|
|
2378
|
+
return mcpMutationReceipt({
|
|
2379
|
+
operation,
|
|
2380
|
+
committed: true,
|
|
2381
|
+
outcome,
|
|
2382
|
+
changed,
|
|
2383
|
+
resource: {
|
|
2384
|
+
type: "scheduled_task",
|
|
2385
|
+
id: task.id,
|
|
2386
|
+
version: task.updatedAt,
|
|
2387
|
+
state: task.status,
|
|
2388
|
+
},
|
|
2389
|
+
timestamp: task.updatedAt,
|
|
2390
|
+
idempotency: { status: options.idempotencyStatus ?? "not_supported" },
|
|
2391
|
+
...(options.partialFailure ? { partialFailure: options.partialFailure } : {}),
|
|
2392
|
+
...(options.warnings ? { warnings: options.warnings } : {}),
|
|
2393
|
+
...(options.facts ? { facts: options.facts } : {}),
|
|
2394
|
+
nextAction: { tool: "scheduled_tasks_get", arguments: { id: task.id } },
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function scheduledTaskUpdateChangesState(
|
|
2399
|
+
task: ScheduledTask,
|
|
2400
|
+
update: Awaited<ReturnType<typeof validatedScheduledTaskUpdate>>,
|
|
2401
|
+
): boolean {
|
|
2402
|
+
if (update.name !== undefined && update.name !== task.name) return true;
|
|
2403
|
+
if (update.status !== undefined && update.status !== task.status) return true;
|
|
2404
|
+
if (update.schedule !== undefined && stableJson(update.schedule) !== stableJson(task.schedule)) {
|
|
2405
|
+
return true;
|
|
2406
|
+
}
|
|
2407
|
+
if (update.runMode !== undefined && update.runMode !== task.runMode) return true;
|
|
2408
|
+
if (update.overlapPolicy !== undefined && update.overlapPolicy !== task.overlapPolicy)
|
|
2409
|
+
return true;
|
|
2410
|
+
if (
|
|
2411
|
+
update.agentConfig !== undefined &&
|
|
2412
|
+
stableJson(update.agentConfig) !== stableJson(task.agentConfig)
|
|
2413
|
+
) {
|
|
2414
|
+
return true;
|
|
2415
|
+
}
|
|
2416
|
+
if (update.targetSessionId !== undefined && update.targetSessionId !== task.targetSessionId) {
|
|
2417
|
+
return true;
|
|
2418
|
+
}
|
|
2419
|
+
if (
|
|
2420
|
+
update.reusableSessionId !== undefined &&
|
|
2421
|
+
update.reusableSessionId !== task.reusableSessionId
|
|
2422
|
+
) {
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
if (update.variableSetId !== undefined && update.variableSetId !== task.variableSetId)
|
|
2426
|
+
return true;
|
|
2427
|
+
if (update.rigId !== undefined && update.rigId !== task.rigId) return true;
|
|
2428
|
+
if (update.metadata !== undefined && stableJson(update.metadata) !== stableJson(task.metadata)) {
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
// Personal-connection delegations are recomputed with agentConfig and can
|
|
2432
|
+
// change even when the visible config is byte-identical (for example after a
|
|
2433
|
+
// connection rotation), so preserve that refresh as a real mutation.
|
|
2434
|
+
if (update.personalConnectionDelegations !== undefined) return true;
|
|
2435
|
+
return false;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
1764
2438
|
function memoryPreview(text: string): string {
|
|
1765
2439
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1766
2440
|
return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
|
|
1767
2441
|
}
|
|
1768
2442
|
|
|
2443
|
+
export function memorySlackPublicationActor(
|
|
2444
|
+
actor: Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>,
|
|
2445
|
+
sessionId: string,
|
|
2446
|
+
fallbackOwnerLabel: string | null,
|
|
2447
|
+
) {
|
|
2448
|
+
return {
|
|
2449
|
+
actor: {
|
|
2450
|
+
kind: actor.initiator.kind === "subject" ? ("human" as const) : ("service" as const),
|
|
2451
|
+
subjectId: actor.initiator.subjectId,
|
|
2452
|
+
initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
|
|
2453
|
+
sessionId,
|
|
2454
|
+
turnId: actor.turnId,
|
|
2455
|
+
attemptId: actor.attemptId,
|
|
2456
|
+
},
|
|
2457
|
+
ownerLabel: actor.initiator.label ?? fallbackOwnerLabel,
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
|
|
1769
2461
|
function registerMemoryTools(
|
|
1770
2462
|
server: McpServer,
|
|
1771
2463
|
deps: ApiRouteDeps,
|
|
@@ -1773,6 +2465,17 @@ function registerMemoryTools(
|
|
|
1773
2465
|
sessionId: string,
|
|
1774
2466
|
json: JsonResult,
|
|
1775
2467
|
): void {
|
|
2468
|
+
const publicationActor = async () => {
|
|
2469
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
2470
|
+
return memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null);
|
|
2471
|
+
};
|
|
2472
|
+
const publicationInputSchema = z4.object({
|
|
2473
|
+
importance: z4.enum(["major", "normal", "minor"]),
|
|
2474
|
+
audience: z4.literal("workspace"),
|
|
2475
|
+
slackMode: z4.enum(["auto", "review", "never"]),
|
|
2476
|
+
shareSummary: z4.string().trim().min(1).max(4_096),
|
|
2477
|
+
});
|
|
2478
|
+
|
|
1776
2479
|
server.registerTool(
|
|
1777
2480
|
"memory_search",
|
|
1778
2481
|
{
|
|
@@ -1807,10 +2510,12 @@ function registerMemoryTools(
|
|
|
1807
2510
|
kind: MemoryKindSchema,
|
|
1808
2511
|
confidence: z4.number().min(0).max(1).optional(),
|
|
1809
2512
|
replaces_id: z4.string().min(1).optional(),
|
|
2513
|
+
slack_publication: publicationInputSchema.optional(),
|
|
1810
2514
|
},
|
|
1811
2515
|
},
|
|
1812
|
-
async ({ text, kind, confidence, replaces_id }) => {
|
|
1813
|
-
const
|
|
2516
|
+
async ({ text, kind, confidence, replaces_id, slack_publication }) => {
|
|
2517
|
+
const principal = slack_publication ? await publicationActor() : null;
|
|
2518
|
+
const result = await saveWorkspaceMemoryWithSlackPublication(
|
|
1814
2519
|
deps.db,
|
|
1815
2520
|
{
|
|
1816
2521
|
accountId: grant.accountId,
|
|
@@ -1822,6 +2527,13 @@ function registerMemoryTools(
|
|
|
1822
2527
|
...(replaces_id ? { replacesId: replaces_id } : {}),
|
|
1823
2528
|
origin: "agent",
|
|
1824
2529
|
},
|
|
2530
|
+
slack_publication
|
|
2531
|
+
? {
|
|
2532
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
2533
|
+
actor: principal!.actor,
|
|
2534
|
+
ownerLabel: principal!.ownerLabel,
|
|
2535
|
+
}
|
|
2536
|
+
: null,
|
|
1825
2537
|
deps.getDocumentServices().embedder,
|
|
1826
2538
|
);
|
|
1827
2539
|
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
@@ -1836,7 +2548,59 @@ function registerMemoryTools(
|
|
|
1836
2548
|
},
|
|
1837
2549
|
},
|
|
1838
2550
|
]);
|
|
1839
|
-
|
|
2551
|
+
const changed = !result.deduped || result.updated || result.superseded !== null;
|
|
2552
|
+
const outcome =
|
|
2553
|
+
result.updated || result.superseded !== null
|
|
2554
|
+
? "updated"
|
|
2555
|
+
: result.deduped
|
|
2556
|
+
? "unchanged"
|
|
2557
|
+
: "created";
|
|
2558
|
+
return json(
|
|
2559
|
+
mcpMutationReceipt({
|
|
2560
|
+
operation: "memory_save",
|
|
2561
|
+
committed: true,
|
|
2562
|
+
outcome,
|
|
2563
|
+
changed,
|
|
2564
|
+
resource: {
|
|
2565
|
+
type: "knowledge_memory",
|
|
2566
|
+
id: result.memory.id,
|
|
2567
|
+
state: result.memory.status,
|
|
2568
|
+
},
|
|
2569
|
+
relatedResources: result.superseded
|
|
2570
|
+
? [
|
|
2571
|
+
{
|
|
2572
|
+
type: "knowledge_memory",
|
|
2573
|
+
id: result.superseded.id,
|
|
2574
|
+
state: result.superseded.status,
|
|
2575
|
+
},
|
|
2576
|
+
]
|
|
2577
|
+
: undefined,
|
|
2578
|
+
timestamp: result.memory.updatedAt,
|
|
2579
|
+
idempotency: { status: "not_supported" },
|
|
2580
|
+
warnings: !result.embedded
|
|
2581
|
+
? ["Memory committed without a vector embedding; keyword search remains available."]
|
|
2582
|
+
: [],
|
|
2583
|
+
facts: {
|
|
2584
|
+
deduped: result.deduped,
|
|
2585
|
+
dedupeReason: result.dedupeReason,
|
|
2586
|
+
updatedInPlace: result.updated,
|
|
2587
|
+
embedded: result.embedded,
|
|
2588
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
2589
|
+
? "eligible"
|
|
2590
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
2591
|
+
slackPublicationId:
|
|
2592
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
2593
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
2594
|
+
? result.slackPublication.enqueue.publication.id
|
|
2595
|
+
: null,
|
|
2596
|
+
slackPublicationState:
|
|
2597
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
2598
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
2599
|
+
? result.slackPublication.enqueue.publication.state
|
|
2600
|
+
: null,
|
|
2601
|
+
},
|
|
2602
|
+
}),
|
|
2603
|
+
);
|
|
1840
2604
|
},
|
|
1841
2605
|
);
|
|
1842
2606
|
|
|
@@ -1848,10 +2612,12 @@ function registerMemoryTools(
|
|
|
1848
2612
|
id: z4.string().min(1),
|
|
1849
2613
|
reason: z4.string().min(1).optional(),
|
|
1850
2614
|
replacement_text: z4.string().min(1).optional(),
|
|
2615
|
+
slack_publication: publicationInputSchema.optional(),
|
|
1851
2616
|
},
|
|
1852
2617
|
},
|
|
1853
|
-
async ({ id, reason, replacement_text }) => {
|
|
1854
|
-
const
|
|
2618
|
+
async ({ id, reason, replacement_text, slack_publication }) => {
|
|
2619
|
+
const principal = slack_publication ? await publicationActor() : null;
|
|
2620
|
+
const result = await correctWorkspaceMemoryWithSlackPublication(
|
|
1855
2621
|
deps.db,
|
|
1856
2622
|
{
|
|
1857
2623
|
accountId: grant.accountId,
|
|
@@ -1861,6 +2627,13 @@ function registerMemoryTools(
|
|
|
1861
2627
|
...(reason ? { reason } : {}),
|
|
1862
2628
|
...(replacement_text ? { replacementText: replacement_text } : {}),
|
|
1863
2629
|
},
|
|
2630
|
+
slack_publication
|
|
2631
|
+
? {
|
|
2632
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
2633
|
+
actor: principal!.actor,
|
|
2634
|
+
ownerLabel: principal!.ownerLabel,
|
|
2635
|
+
}
|
|
2636
|
+
: null,
|
|
1864
2637
|
deps.getDocumentServices().embedder,
|
|
1865
2638
|
);
|
|
1866
2639
|
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
@@ -1881,7 +2654,46 @@ function registerMemoryTools(
|
|
|
1881
2654
|
},
|
|
1882
2655
|
},
|
|
1883
2656
|
]);
|
|
1884
|
-
return json(
|
|
2657
|
+
return json(
|
|
2658
|
+
mcpMutationReceipt({
|
|
2659
|
+
operation: "memory_correct",
|
|
2660
|
+
committed: true,
|
|
2661
|
+
outcome: "updated",
|
|
2662
|
+
changed: true,
|
|
2663
|
+
resource: {
|
|
2664
|
+
type: "knowledge_memory",
|
|
2665
|
+
id: result.memory.id,
|
|
2666
|
+
state: result.memory.status,
|
|
2667
|
+
},
|
|
2668
|
+
relatedResources: result.replacement
|
|
2669
|
+
? [
|
|
2670
|
+
{
|
|
2671
|
+
type: "knowledge_memory",
|
|
2672
|
+
id: result.replacement.id,
|
|
2673
|
+
state: result.replacement.status,
|
|
2674
|
+
},
|
|
2675
|
+
]
|
|
2676
|
+
: undefined,
|
|
2677
|
+
timestamp: (result.replacement ?? result.memory).updatedAt,
|
|
2678
|
+
idempotency: { status: "not_supported" },
|
|
2679
|
+
facts: {
|
|
2680
|
+
correctionAction: result.action,
|
|
2681
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
2682
|
+
? "eligible"
|
|
2683
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
2684
|
+
slackPublicationId:
|
|
2685
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
2686
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
2687
|
+
? result.slackPublication.enqueue.publication.id
|
|
2688
|
+
: null,
|
|
2689
|
+
slackPublicationState:
|
|
2690
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
2691
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
2692
|
+
? result.slackPublication.enqueue.publication.state
|
|
2693
|
+
: null,
|
|
2694
|
+
},
|
|
2695
|
+
}),
|
|
2696
|
+
);
|
|
1885
2697
|
},
|
|
1886
2698
|
);
|
|
1887
2699
|
}
|
|
@@ -1928,7 +2740,7 @@ function registerFleetTools(
|
|
|
1928
2740
|
"sandboxes_list",
|
|
1929
2741
|
{
|
|
1930
2742
|
description:
|
|
1931
|
-
"List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `
|
|
2743
|
+
"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
2744
|
inputSchema: {},
|
|
1933
2745
|
},
|
|
1934
2746
|
async () => json(await listFleet(services, await fleetContext())),
|
|
@@ -2000,6 +2812,45 @@ function registerFleetTools(
|
|
|
2000
2812
|
);
|
|
2001
2813
|
}
|
|
2002
2814
|
|
|
2815
|
+
// Workspace-admin/operator surface for removing a connected machine. This is
|
|
2816
|
+
// deliberately separate from the session-scoped fleet tools: a worker can list,
|
|
2817
|
+
// attach, or run on a machine only with session authority, while removal requires
|
|
2818
|
+
// the explicit high-trust enrollments:manage permission and never accepts a
|
|
2819
|
+
// sandbox id. A Modal record therefore cannot be removed through this operation.
|
|
2820
|
+
function registerConnectedMachineTools(
|
|
2821
|
+
server: McpServer,
|
|
2822
|
+
deps: ApiRouteDeps,
|
|
2823
|
+
grant: AccessGrant,
|
|
2824
|
+
json: JsonResult,
|
|
2825
|
+
): void {
|
|
2826
|
+
server.registerTool(
|
|
2827
|
+
"connected_machine_remove",
|
|
2828
|
+
{
|
|
2829
|
+
description:
|
|
2830
|
+
"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 every dependent session and the action needed before retrying. Move each dependent session through the canonical sandbox_swap target=default path before retrying removal; the removal authority never rewrites routes directly.",
|
|
2831
|
+
inputSchema: {
|
|
2832
|
+
enrollmentId: z4.string().uuid(),
|
|
2833
|
+
expectedUpdatedAt: z4.string().datetime({ offset: true }).optional(),
|
|
2834
|
+
idempotencyKey: z4.string().trim().min(1).max(200).optional(),
|
|
2835
|
+
},
|
|
2836
|
+
},
|
|
2837
|
+
async ({ enrollmentId, expectedUpdatedAt, idempotencyKey }) => {
|
|
2838
|
+
const result = await removeEnrollment(deps.db, {
|
|
2839
|
+
accountId: grant.accountId,
|
|
2840
|
+
workspaceId: grant.workspaceId,
|
|
2841
|
+
enrollmentId,
|
|
2842
|
+
operationKey: idempotencyKey?.trim() || randomUUID(),
|
|
2843
|
+
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
2844
|
+
subjectId: grant.subjectId,
|
|
2845
|
+
});
|
|
2846
|
+
if (!result) {
|
|
2847
|
+
throw new Error("machine enrollment not found in this workspace");
|
|
2848
|
+
}
|
|
2849
|
+
return json({ revoked: result.removed, ...result });
|
|
2850
|
+
},
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2003
2854
|
async function beginMcpRigVerificationAttempt(
|
|
2004
2855
|
deps: ApiRouteDeps,
|
|
2005
2856
|
workspaceId: string,
|
|
@@ -2008,12 +2859,10 @@ async function beginMcpRigVerificationAttempt(
|
|
|
2008
2859
|
try {
|
|
2009
2860
|
return await beginRigChangeVerificationAttempt(deps.db, workspaceId, changeId, {
|
|
2010
2861
|
startedAt: new Date().toISOString(),
|
|
2862
|
+
allowAlreadyVerifying: true,
|
|
2011
2863
|
});
|
|
2012
2864
|
} catch (error) {
|
|
2013
|
-
if (
|
|
2014
|
-
error instanceof RigChangeAlreadyVerifyingError ||
|
|
2015
|
-
error instanceof RigChangeTransitionError
|
|
2016
|
-
) {
|
|
2865
|
+
if (error instanceof RigChangeTransitionError) {
|
|
2017
2866
|
throw new Error(error.message, { cause: error });
|
|
2018
2867
|
}
|
|
2019
2868
|
throw error;
|
|
@@ -2101,12 +2950,60 @@ function registerRigTools(
|
|
|
2101
2950
|
sessionId ? { proposedBy: `session:${sessionId}` } : {},
|
|
2102
2951
|
);
|
|
2103
2952
|
const verifying = await beginMcpRigVerificationAttempt(deps, grant.workspaceId, change.id);
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2953
|
+
const attempt = verificationAttempt(verifying);
|
|
2954
|
+
try {
|
|
2955
|
+
await deps.workflowClient.startRigVerification({
|
|
2956
|
+
workspaceId: grant.workspaceId,
|
|
2957
|
+
changeId: change.id,
|
|
2958
|
+
workflowId: `rig-verification-change-${change.id}-attempt-${attempt}`,
|
|
2959
|
+
});
|
|
2960
|
+
} catch {
|
|
2961
|
+
return json(
|
|
2962
|
+
mcpMutationReceipt({
|
|
2963
|
+
operation: "rig_propose_change",
|
|
2964
|
+
committed: true,
|
|
2965
|
+
outcome: "partial_failure",
|
|
2966
|
+
changed: true,
|
|
2967
|
+
resource: {
|
|
2968
|
+
type: "rig_change",
|
|
2969
|
+
id: verifying.id,
|
|
2970
|
+
version: verifying.updatedAt,
|
|
2971
|
+
state: verifying.status,
|
|
2972
|
+
},
|
|
2973
|
+
relatedResources: [{ type: "rig", id: rig.id }],
|
|
2974
|
+
timestamp: verifying.updatedAt,
|
|
2975
|
+
idempotency: { status: "not_supported" },
|
|
2976
|
+
partialFailure: {
|
|
2977
|
+
stage: "verification_workflow_start",
|
|
2978
|
+
retryable: true,
|
|
2979
|
+
},
|
|
2980
|
+
warnings: [
|
|
2981
|
+
"The rig change and verifying transition committed, but verification workflow start failed.",
|
|
2982
|
+
],
|
|
2983
|
+
facts: { verificationAttempt: attempt },
|
|
2984
|
+
nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
|
|
2985
|
+
}),
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2988
|
+
return json(
|
|
2989
|
+
mcpMutationReceipt({
|
|
2990
|
+
operation: "rig_propose_change",
|
|
2991
|
+
committed: true,
|
|
2992
|
+
outcome: "created",
|
|
2993
|
+
changed: true,
|
|
2994
|
+
resource: {
|
|
2995
|
+
type: "rig_change",
|
|
2996
|
+
id: verifying.id,
|
|
2997
|
+
version: verifying.updatedAt,
|
|
2998
|
+
state: verifying.status,
|
|
2999
|
+
},
|
|
3000
|
+
relatedResources: [{ type: "rig", id: rig.id }],
|
|
3001
|
+
timestamp: verifying.updatedAt,
|
|
3002
|
+
idempotency: { status: "not_supported" },
|
|
3003
|
+
facts: { verificationStarted: true, verificationAttempt: attempt },
|
|
3004
|
+
nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
|
|
3005
|
+
}),
|
|
3006
|
+
);
|
|
2110
3007
|
},
|
|
2111
3008
|
);
|
|
2112
3009
|
|
|
@@ -2129,12 +3026,63 @@ function registerRigTools(
|
|
|
2129
3026
|
grant.workspaceId,
|
|
2130
3027
|
change.id,
|
|
2131
3028
|
);
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
3029
|
+
const attempt = verificationAttempt(verifying);
|
|
3030
|
+
try {
|
|
3031
|
+
await deps.workflowClient.startRigVerification({
|
|
3032
|
+
workspaceId: grant.workspaceId,
|
|
3033
|
+
changeId: change.id,
|
|
3034
|
+
workflowId: `rig-verification-change-${change.id}-attempt-${attempt}`,
|
|
3035
|
+
});
|
|
3036
|
+
} catch {
|
|
3037
|
+
return json(
|
|
3038
|
+
mcpMutationReceipt({
|
|
3039
|
+
operation: "rig_verify",
|
|
3040
|
+
committed: true,
|
|
3041
|
+
outcome: "partial_failure",
|
|
3042
|
+
changed: true,
|
|
3043
|
+
resource: {
|
|
3044
|
+
type: "rig_change",
|
|
3045
|
+
id: verifying.id,
|
|
3046
|
+
version: verifying.updatedAt,
|
|
3047
|
+
state: verifying.status,
|
|
3048
|
+
},
|
|
3049
|
+
relatedResources: [{ type: "rig", id: rig.id }],
|
|
3050
|
+
timestamp: verifying.updatedAt,
|
|
3051
|
+
idempotency: { status: "not_supported" },
|
|
3052
|
+
partialFailure: {
|
|
3053
|
+
stage: "verification_workflow_start",
|
|
3054
|
+
retryable: true,
|
|
3055
|
+
},
|
|
3056
|
+
warnings: [
|
|
3057
|
+
"The verifying transition committed, but verification workflow start failed.",
|
|
3058
|
+
],
|
|
3059
|
+
facts: { verificationAttempt: attempt },
|
|
3060
|
+
nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
|
|
3061
|
+
}),
|
|
3062
|
+
);
|
|
3063
|
+
}
|
|
3064
|
+
return json(
|
|
3065
|
+
mcpMutationReceipt({
|
|
3066
|
+
operation: "rig_verify",
|
|
3067
|
+
committed: true,
|
|
3068
|
+
outcome: "accepted",
|
|
3069
|
+
changed: true,
|
|
3070
|
+
resource: {
|
|
3071
|
+
type: "rig_change",
|
|
3072
|
+
id: verifying.id,
|
|
3073
|
+
version: verifying.updatedAt,
|
|
3074
|
+
state: verifying.status,
|
|
3075
|
+
},
|
|
3076
|
+
relatedResources: [{ type: "rig", id: rig.id }],
|
|
3077
|
+
timestamp: verifying.updatedAt,
|
|
3078
|
+
idempotency: { status: "not_supported" },
|
|
3079
|
+
facts: {
|
|
3080
|
+
verificationStarted: true,
|
|
3081
|
+
verificationAttempt: attempt,
|
|
3082
|
+
},
|
|
3083
|
+
nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
|
|
3084
|
+
}),
|
|
3085
|
+
);
|
|
2138
3086
|
}
|
|
2139
3087
|
if (!rig.activeVersion) {
|
|
2140
3088
|
throw new Error("rig has no active version");
|
|
@@ -2144,7 +3092,23 @@ function registerRigTools(
|
|
|
2144
3092
|
versionId: rig.activeVersion.id,
|
|
2145
3093
|
workflowId: `rig-verification-version-${rig.activeVersion.id}-${crypto.randomUUID()}`,
|
|
2146
3094
|
});
|
|
2147
|
-
return json(
|
|
3095
|
+
return json(
|
|
3096
|
+
mcpMutationReceipt({
|
|
3097
|
+
operation: "rig_verify",
|
|
3098
|
+
committed: true,
|
|
3099
|
+
outcome: "accepted",
|
|
3100
|
+
changed: true,
|
|
3101
|
+
resource: {
|
|
3102
|
+
type: "rig_version",
|
|
3103
|
+
id: rig.activeVersion.id,
|
|
3104
|
+
version: rig.activeVersion.version,
|
|
3105
|
+
state: "verification_started",
|
|
3106
|
+
},
|
|
3107
|
+
relatedResources: [{ type: "rig", id: rig.id }],
|
|
3108
|
+
idempotency: { status: "not_supported" },
|
|
3109
|
+
nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
|
|
3110
|
+
}),
|
|
3111
|
+
);
|
|
2148
3112
|
},
|
|
2149
3113
|
);
|
|
2150
3114
|
}
|
|
@@ -2163,8 +3127,36 @@ function registerRigTools(
|
|
|
2163
3127
|
async ({ rigId, changeId }) => {
|
|
2164
3128
|
const rig = await requireRigForApi(deps.db, grant.workspaceId, rigId);
|
|
2165
3129
|
const change = await requireRigChangeForApi(deps.db, grant.workspaceId, rig.id, changeId);
|
|
3130
|
+
const promoted = await promoteVerifiedDefinitionEditChangeForApi(
|
|
3131
|
+
{ db: deps.db },
|
|
3132
|
+
grant,
|
|
3133
|
+
rig,
|
|
3134
|
+
change,
|
|
3135
|
+
);
|
|
2166
3136
|
return json(
|
|
2167
|
-
|
|
3137
|
+
mcpMutationReceipt({
|
|
3138
|
+
operation: "rig_promote",
|
|
3139
|
+
committed: true,
|
|
3140
|
+
outcome: "updated",
|
|
3141
|
+
changed: true,
|
|
3142
|
+
resource: {
|
|
3143
|
+
type: "rig_version",
|
|
3144
|
+
id: promoted.version.id,
|
|
3145
|
+
version: promoted.version.version,
|
|
3146
|
+
state: "active",
|
|
3147
|
+
},
|
|
3148
|
+
relatedResources: [
|
|
3149
|
+
{
|
|
3150
|
+
type: "rig_change",
|
|
3151
|
+
id: promoted.change.id,
|
|
3152
|
+
state: promoted.change.status,
|
|
3153
|
+
},
|
|
3154
|
+
{ type: "rig", id: rig.id },
|
|
3155
|
+
],
|
|
3156
|
+
timestamp: promoted.version.createdAt,
|
|
3157
|
+
idempotency: { status: "not_supported" },
|
|
3158
|
+
nextAction: { tool: "rig_get", arguments: { rigId: rig.id } },
|
|
3159
|
+
}),
|
|
2168
3160
|
);
|
|
2169
3161
|
},
|
|
2170
3162
|
);
|
|
@@ -2209,7 +3201,6 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2209
3201
|
grant: AccessGrant,
|
|
2210
3202
|
can: (permission: Permission) => boolean,
|
|
2211
3203
|
callerSessionId: string | null,
|
|
2212
|
-
toolspaceMode: boolean,
|
|
2213
3204
|
json: JsonResult,
|
|
2214
3205
|
): void {
|
|
2215
3206
|
if (can("sessions:read")) {
|
|
@@ -2287,7 +3278,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2287
3278
|
);
|
|
2288
3279
|
return json(
|
|
2289
3280
|
boundSessionDetailMcp(
|
|
2290
|
-
await withMcpEffectivePolicy(deps, grant.workspaceId, {
|
|
3281
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, grant.subjectId, {
|
|
2291
3282
|
...projected,
|
|
2292
3283
|
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
|
|
2293
3284
|
}),
|
|
@@ -2384,7 +3375,10 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2384
3375
|
compactSessionEventResult(
|
|
2385
3376
|
event,
|
|
2386
3377
|
latestClass!,
|
|
2387
|
-
dbPage.coveredSequence ?? {
|
|
3378
|
+
dbPage.coveredSequence ?? {
|
|
3379
|
+
first: event.sequence,
|
|
3380
|
+
last: event.sequence,
|
|
3381
|
+
},
|
|
2388
3382
|
),
|
|
2389
3383
|
)
|
|
2390
3384
|
: null,
|
|
@@ -2432,9 +3426,21 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2432
3426
|
// Bind the spawned session to a rig (freezes its active version);
|
|
2433
3427
|
// declared so MCP validation doesn't strip it before the domain reads it.
|
|
2434
3428
|
rigId: z4.string().uuid().optional(),
|
|
2435
|
-
model: z4
|
|
2436
|
-
|
|
2437
|
-
|
|
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."),
|
|
2438
3444
|
sandboxBackend: z4.string().optional(),
|
|
2439
3445
|
// Create-time machine targeting: an enrolled sandbox id (from
|
|
2440
3446
|
// sandboxes_list) to run the spawned session on. Seeds the active-sandbox
|
|
@@ -2508,8 +3514,13 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2508
3514
|
if (callerSessionId !== null) {
|
|
2509
3515
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
2510
3516
|
}
|
|
2511
|
-
const
|
|
2512
|
-
|
|
3517
|
+
const result = await createSessionForRequestWithOutcome(
|
|
3518
|
+
deps,
|
|
3519
|
+
grant,
|
|
3520
|
+
grant.workspaceId,
|
|
3521
|
+
args,
|
|
3522
|
+
);
|
|
3523
|
+
return json(sessionCreateMutationReceipt(result, Boolean(args.idempotencyKey)));
|
|
2513
3524
|
} catch (error) {
|
|
2514
3525
|
if (error instanceof SessionSpawnDeniedError) {
|
|
2515
3526
|
return {
|
|
@@ -2523,7 +3534,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2523
3534
|
);
|
|
2524
3535
|
}
|
|
2525
3536
|
|
|
2526
|
-
if (can("sessions:control")
|
|
3537
|
+
if (can("sessions:control")) {
|
|
2527
3538
|
server.registerTool(
|
|
2528
3539
|
"session_send_message",
|
|
2529
3540
|
{
|
|
@@ -2549,17 +3560,33 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2549
3560
|
exactAgentCommandContext(grant, callerSessionId),
|
|
2550
3561
|
{ targetSessionId, text, idempotencyKey },
|
|
2551
3562
|
);
|
|
2552
|
-
return json(
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
3563
|
+
return json(
|
|
3564
|
+
mcpMutationReceipt({
|
|
3565
|
+
operation: "session_send_message",
|
|
3566
|
+
committed: true,
|
|
3567
|
+
outcome: result.replay ? "replayed" : "accepted",
|
|
3568
|
+
changed: !result.replay,
|
|
3569
|
+
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",
|
|
3581
|
+
},
|
|
3582
|
+
nextAction: {
|
|
3583
|
+
tool: "session_get",
|
|
3584
|
+
arguments: { sessionId: targetSessionId },
|
|
3585
|
+
},
|
|
3586
|
+
}),
|
|
3587
|
+
);
|
|
2561
3588
|
}
|
|
2562
|
-
const { accepted, turn } = await
|
|
3589
|
+
const { accepted, turn, replay } = await acceptSessionUserMessageWithOutcome(
|
|
2563
3590
|
deps,
|
|
2564
3591
|
grant,
|
|
2565
3592
|
grant.workspaceId,
|
|
@@ -2574,7 +3601,30 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2574
3601
|
),
|
|
2575
3602
|
},
|
|
2576
3603
|
);
|
|
2577
|
-
return json(
|
|
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
|
+
);
|
|
2578
3628
|
},
|
|
2579
3629
|
);
|
|
2580
3630
|
|
|
@@ -2600,16 +3650,25 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2600
3650
|
reason: reason ?? "agent_mcp_pause",
|
|
2601
3651
|
},
|
|
2602
3652
|
);
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
3653
|
+
const effectiveControl = projectEffectiveControlForRelatedAccess(
|
|
3654
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
3655
|
+
sessionId,
|
|
3656
|
+
controlled.authorization?.relatedSessionAccess ?? "root",
|
|
3657
|
+
);
|
|
3658
|
+
return json(
|
|
3659
|
+
mcpMutationReceipt({
|
|
3660
|
+
operation: "session_pause",
|
|
3661
|
+
committed: true,
|
|
3662
|
+
outcome: controlled.replay ? "replayed" : "updated",
|
|
3663
|
+
changed: !controlled.replay,
|
|
3664
|
+
resource: { type: "session", id: sessionId, state: effectiveControl.state },
|
|
3665
|
+
relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
|
|
3666
|
+
timestamp: controlled.receipt.createdAt.toISOString(),
|
|
3667
|
+
idempotency: { status: controlled.replay ? "replayed" : "applied" },
|
|
3668
|
+
facts: { interruptionCount: controlled.interruptionCount },
|
|
3669
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
3670
|
+
}),
|
|
3671
|
+
);
|
|
2613
3672
|
}
|
|
2614
3673
|
const controlled = await controlHumanSessionWorkstream(
|
|
2615
3674
|
deps,
|
|
@@ -2653,16 +3712,25 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2653
3712
|
reason: reason ?? "agent_mcp_resume",
|
|
2654
3713
|
},
|
|
2655
3714
|
);
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
3715
|
+
const effectiveControl = projectEffectiveControlForRelatedAccess(
|
|
3716
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
3717
|
+
sessionId,
|
|
3718
|
+
controlled.authorization?.relatedSessionAccess ?? "root",
|
|
3719
|
+
);
|
|
3720
|
+
return json(
|
|
3721
|
+
mcpMutationReceipt({
|
|
3722
|
+
operation: "session_resume",
|
|
3723
|
+
committed: true,
|
|
3724
|
+
outcome: controlled.replay ? "replayed" : "updated",
|
|
3725
|
+
changed: !controlled.replay,
|
|
3726
|
+
resource: { type: "session", id: sessionId, state: effectiveControl.state },
|
|
3727
|
+
relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
|
|
3728
|
+
timestamp: controlled.receipt.createdAt.toISOString(),
|
|
3729
|
+
idempotency: { status: controlled.replay ? "replayed" : "applied" },
|
|
3730
|
+
facts: { interruptionCount: controlled.interruptionCount },
|
|
3731
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
3732
|
+
}),
|
|
3733
|
+
);
|
|
2666
3734
|
}
|
|
2667
3735
|
const controlled = await controlHumanSessionWorkstream(
|
|
2668
3736
|
deps,
|
|
@@ -2701,13 +3769,28 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2701
3769
|
exactAgentCommandContext(grant, callerSessionId, "first_party_mcp"),
|
|
2702
3770
|
{ targetSessionId: sessionId, instruction, idempotencyKey },
|
|
2703
3771
|
);
|
|
2704
|
-
return json(
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
3772
|
+
return json(
|
|
3773
|
+
mcpMutationReceipt({
|
|
3774
|
+
operation: "session_steer",
|
|
3775
|
+
committed: true,
|
|
3776
|
+
outcome: result.replay ? "replayed" : "updated",
|
|
3777
|
+
changed: !result.replay,
|
|
3778
|
+
resource: {
|
|
3779
|
+
type: "session_system_update",
|
|
3780
|
+
id: result.updateId,
|
|
3781
|
+
state: result.effectiveState,
|
|
3782
|
+
},
|
|
3783
|
+
relatedResources: [{ type: "session", id: sessionId }],
|
|
3784
|
+
timestamp: result.receipt.createdAt.toISOString(),
|
|
3785
|
+
idempotency: { status: result.replay ? "replayed" : "applied" },
|
|
3786
|
+
facts: {
|
|
3787
|
+
interruptionCount: result.interruptionCount,
|
|
3788
|
+
stoppingPreviousAttempt: result.interruptionCount > 0,
|
|
3789
|
+
},
|
|
3790
|
+
updateId: result.updateId,
|
|
3791
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
3792
|
+
}),
|
|
3793
|
+
);
|
|
2711
3794
|
},
|
|
2712
3795
|
);
|
|
2713
3796
|
}
|
|
@@ -2736,15 +3819,15 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2736
3819
|
}
|
|
2737
3820
|
}
|
|
2738
3821
|
|
|
2739
|
-
//
|
|
2740
|
-
//
|
|
2741
|
-
//
|
|
2742
|
-
// write-only — responses carry names and metadata, never values.
|
|
3822
|
+
// Variable-set management for agents. Generic reads remain metadata-only.
|
|
3823
|
+
// Plaintext has one dedicated tool that additionally requires literal
|
|
3824
|
+
// secrets:read plus exact live attempt/session authorization.
|
|
2743
3825
|
function registerVariableSetTools(
|
|
2744
3826
|
server: McpServer,
|
|
2745
3827
|
deps: ApiRouteDeps,
|
|
2746
3828
|
grant: AccessGrant,
|
|
2747
3829
|
can: (permission: Permission) => boolean,
|
|
3830
|
+
sessionId: string | null,
|
|
2748
3831
|
json: JsonResult,
|
|
2749
3832
|
): void {
|
|
2750
3833
|
const registerListTool = (name: string, description: string): void => {
|
|
@@ -2760,93 +3843,114 @@ function registerVariableSetTools(
|
|
|
2760
3843
|
},
|
|
2761
3844
|
);
|
|
2762
3845
|
};
|
|
2763
|
-
const setVariableHandler =
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3846
|
+
const setVariableHandler =
|
|
3847
|
+
(operation: "variable_set_set_variable" | "environment_set_variable") =>
|
|
3848
|
+
async ({
|
|
3849
|
+
variableSetId,
|
|
3850
|
+
variableSetName,
|
|
3851
|
+
environmentId,
|
|
3852
|
+
environmentName,
|
|
3853
|
+
name,
|
|
3854
|
+
value,
|
|
3855
|
+
}: {
|
|
3856
|
+
variableSetId?: string | undefined;
|
|
3857
|
+
variableSetName?: string | undefined;
|
|
3858
|
+
environmentId?: string | undefined;
|
|
3859
|
+
environmentName?: string | undefined;
|
|
3860
|
+
name: string;
|
|
3861
|
+
value: string;
|
|
3862
|
+
}) => {
|
|
3863
|
+
const key = requireVariableSetEncryption(deps.settings);
|
|
3864
|
+
const parsedName = VariableSetVariableName.safeParse(name);
|
|
3865
|
+
if (!parsedName.success) {
|
|
3866
|
+
throw new Error("variable set/environment variable names must match ^[A-Z][A-Z0-9_]*$");
|
|
3867
|
+
}
|
|
3868
|
+
assertAllowedVariableSetVariableName(parsedName.data);
|
|
3869
|
+
const targetId = variableSetId ?? environmentId;
|
|
3870
|
+
const targetName = variableSetName ?? environmentName;
|
|
3871
|
+
if ((targetId === undefined) === (targetName === undefined)) {
|
|
3872
|
+
throw new Error(
|
|
3873
|
+
"provide exactly one of variableSetId or variableSetName; deprecated aliases must provide exactly one of environmentId or environmentName",
|
|
3874
|
+
);
|
|
3875
|
+
}
|
|
3876
|
+
const trimmedVariableSetName = targetName?.trim();
|
|
3877
|
+
if (targetName !== undefined && !trimmedVariableSetName) {
|
|
3878
|
+
throw new Error("variable set name is required");
|
|
3879
|
+
}
|
|
3880
|
+
let created = false;
|
|
3881
|
+
let variableSet =
|
|
3882
|
+
targetId !== undefined
|
|
3883
|
+
? await getVariableSet(deps.db, grant.workspaceId, targetId)
|
|
3884
|
+
: await getVariableSetByName(deps.db, grant.workspaceId, trimmedVariableSetName!);
|
|
3885
|
+
if (!variableSet && targetId !== undefined) {
|
|
3886
|
+
throw new Error("variable set/environment not found");
|
|
3887
|
+
}
|
|
3888
|
+
if (!variableSet) {
|
|
3889
|
+
if (
|
|
3890
|
+
(await countVariableSets(deps.db, grant.workspaceId)) >= MAX_ENVIRONMENTS_PER_WORKSPACE
|
|
3891
|
+
) {
|
|
3892
|
+
throw new Error(
|
|
3893
|
+
`a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE} variable sets`,
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
variableSet = await createVariableSet(deps.db, {
|
|
3897
|
+
accountId: grant.accountId,
|
|
3898
|
+
workspaceId: grant.workspaceId,
|
|
3899
|
+
name: trimmedVariableSetName!,
|
|
3900
|
+
});
|
|
3901
|
+
created = true;
|
|
3902
|
+
await recordVariableSetAuditEvent(deps.db, {
|
|
3903
|
+
grant,
|
|
3904
|
+
action: "variable_set.created",
|
|
3905
|
+
variableSetId: variableSet.id,
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
const exists = variableSet.variables.some((variable) => variable.name === parsedName.data);
|
|
3909
|
+
if (!exists && variableSet.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT) {
|
|
2805
3910
|
throw new Error(
|
|
2806
|
-
`a
|
|
3911
|
+
`a variable set supports at most ${MAX_VARIABLES_PER_ENVIRONMENT} variables`,
|
|
2807
3912
|
);
|
|
2808
3913
|
}
|
|
2809
|
-
|
|
3914
|
+
const metadata = await setVariableSetVariable(deps.db, {
|
|
2810
3915
|
accountId: grant.accountId,
|
|
2811
3916
|
workspaceId: grant.workspaceId,
|
|
2812
|
-
|
|
3917
|
+
variableSetId: variableSet.id,
|
|
3918
|
+
name: parsedName.data,
|
|
3919
|
+
valueEncrypted: encryptVariableSetValue(key, value),
|
|
2813
3920
|
});
|
|
2814
|
-
created = true;
|
|
2815
3921
|
await recordVariableSetAuditEvent(deps.db, {
|
|
2816
3922
|
grant,
|
|
2817
|
-
action: "variable_set.
|
|
3923
|
+
action: "variable_set.variable.set",
|
|
2818
3924
|
variableSetId: variableSet.id,
|
|
3925
|
+
variableName: parsedName.data,
|
|
2819
3926
|
});
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3927
|
+
return json(
|
|
3928
|
+
mcpMutationReceipt({
|
|
3929
|
+
operation,
|
|
3930
|
+
committed: true,
|
|
3931
|
+
outcome: exists ? "updated" : "created",
|
|
3932
|
+
changed: true,
|
|
3933
|
+
resource: {
|
|
3934
|
+
type: "variable_set",
|
|
3935
|
+
id: variableSet.id,
|
|
3936
|
+
version: metadata.version,
|
|
3937
|
+
state: "variable_written",
|
|
3938
|
+
},
|
|
3939
|
+
timestamp: metadata.updatedAt,
|
|
3940
|
+
idempotency: { status: "not_supported" },
|
|
3941
|
+
facts: {
|
|
3942
|
+
variableCreated: !exists,
|
|
3943
|
+
variableSetCreated: created,
|
|
3944
|
+
deprecatedAlias: operation === "environment_set_variable",
|
|
3945
|
+
},
|
|
3946
|
+
nextAction: { tool: "variable_set_list", arguments: {} },
|
|
3947
|
+
}),
|
|
3948
|
+
);
|
|
2842
3949
|
};
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
});
|
|
2848
|
-
};
|
|
2849
|
-
const registerSetTool = (name: string, description: string): void => {
|
|
3950
|
+
const registerSetTool = (
|
|
3951
|
+
name: "variable_set_set_variable" | "environment_set_variable",
|
|
3952
|
+
description: string,
|
|
3953
|
+
): void => {
|
|
2850
3954
|
server.registerTool(
|
|
2851
3955
|
name,
|
|
2852
3956
|
{
|
|
@@ -2860,32 +3964,334 @@ function registerVariableSetTools(
|
|
|
2860
3964
|
value: z4.string().min(1).max(32768),
|
|
2861
3965
|
},
|
|
2862
3966
|
},
|
|
2863
|
-
setVariableHandler,
|
|
3967
|
+
setVariableHandler(name),
|
|
2864
3968
|
);
|
|
2865
3969
|
};
|
|
2866
|
-
if (can("variable-sets:
|
|
3970
|
+
if (can("variable-sets:list") && can("secrets:list")) {
|
|
2867
3971
|
registerListTool(
|
|
2868
3972
|
"variable_set_list",
|
|
2869
|
-
"List variable sets with variable names and metadata (versions, timestamps).
|
|
3973
|
+
"List variable sets with variable names and metadata (versions, timestamps). Plaintext values are never returned by list operations.",
|
|
2870
3974
|
);
|
|
2871
3975
|
registerListTool(
|
|
2872
3976
|
"environment_list",
|
|
2873
|
-
"(deprecated alias of variable_set_list) List variable sets with variable names and metadata (versions, timestamps).
|
|
3977
|
+
"(deprecated alias of variable_set_list) List variable sets with variable names and metadata (versions, timestamps). Plaintext values are never returned by list operations.",
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
if (
|
|
3982
|
+
sessionId !== null &&
|
|
3983
|
+
can("variable-sets:read") &&
|
|
3984
|
+
hasLiteralPermission(grant.permissions, "secrets:read")
|
|
3985
|
+
) {
|
|
3986
|
+
server.registerTool(
|
|
3987
|
+
"variable_set_get_variable",
|
|
3988
|
+
{
|
|
3989
|
+
description:
|
|
3990
|
+
"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.",
|
|
3991
|
+
inputSchema: {
|
|
3992
|
+
variableSetId: z4.string().uuid().optional(),
|
|
3993
|
+
variableSetName: z4.string().min(1).max(120).optional(),
|
|
3994
|
+
name: z4.string().min(1),
|
|
3995
|
+
},
|
|
3996
|
+
},
|
|
3997
|
+
async ({ variableSetId, variableSetName, name }) => {
|
|
3998
|
+
let target: { variableSetId: string } | { variableSetName: string };
|
|
3999
|
+
if (variableSetId !== undefined) {
|
|
4000
|
+
if (variableSetName !== undefined) {
|
|
4001
|
+
throw new Error("provide exactly one of variableSetId or variableSetName");
|
|
4002
|
+
}
|
|
4003
|
+
target = { variableSetId };
|
|
4004
|
+
} else {
|
|
4005
|
+
if (variableSetName === undefined) {
|
|
4006
|
+
throw new Error("provide exactly one of variableSetId or variableSetName");
|
|
4007
|
+
}
|
|
4008
|
+
target = { variableSetName };
|
|
4009
|
+
}
|
|
4010
|
+
if (
|
|
4011
|
+
("variableSetId" in target && target.variableSetId.length === 0) ||
|
|
4012
|
+
("variableSetName" in target && target.variableSetName.trim().length === 0)
|
|
4013
|
+
) {
|
|
4014
|
+
throw new Error("provide exactly one of variableSetId or variableSetName");
|
|
4015
|
+
}
|
|
4016
|
+
const parsedName = VariableSetVariableName.safeParse(name);
|
|
4017
|
+
if (!parsedName.success) {
|
|
4018
|
+
throw new Error("variable set variable names must match ^[A-Z][A-Z0-9_]*$");
|
|
4019
|
+
}
|
|
4020
|
+
assertAllowedVariableSetVariableName(parsedName.data);
|
|
4021
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
4022
|
+
if (!claims || claims.sessionId !== sessionId) {
|
|
4023
|
+
throw new Error("Exact signed secret-read attempt authority is required.");
|
|
4024
|
+
}
|
|
4025
|
+
await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
4026
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.secret.read");
|
|
4027
|
+
const key = requireVariableSetEncryption(deps.settings);
|
|
4028
|
+
const secret = await readVariableSetSecretAtomically(deps.db, {
|
|
4029
|
+
accountId: grant.accountId,
|
|
4030
|
+
workspaceId: grant.workspaceId,
|
|
4031
|
+
subjectId: grant.subjectId,
|
|
4032
|
+
...target,
|
|
4033
|
+
name: parsedName.data,
|
|
4034
|
+
actor: {
|
|
4035
|
+
kind: "agent_attempt",
|
|
4036
|
+
sessionId: claims.sessionId,
|
|
4037
|
+
turnId: claims.turnId,
|
|
4038
|
+
attemptId: claims.attemptId,
|
|
4039
|
+
executionGeneration: claims.executionGeneration,
|
|
4040
|
+
},
|
|
4041
|
+
decrypt: (valueEncrypted) => decryptVariableSetValue(key, valueEncrypted),
|
|
4042
|
+
});
|
|
4043
|
+
if (!secret) throw new Error("variable set variable not found");
|
|
4044
|
+
return json(secret);
|
|
4045
|
+
},
|
|
2874
4046
|
);
|
|
2875
4047
|
}
|
|
2876
4048
|
|
|
2877
|
-
if (can("variable-sets:
|
|
4049
|
+
if (can("variable-sets:write") && can("secrets:write")) {
|
|
2878
4050
|
registerSetTool(
|
|
2879
4051
|
"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
|
|
4052
|
+
"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
4053
|
);
|
|
2882
4054
|
registerSetTool(
|
|
2883
4055
|
"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
|
|
4056
|
+
"(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
4057
|
);
|
|
2886
4058
|
}
|
|
2887
4059
|
}
|
|
2888
4060
|
|
|
4061
|
+
type CapabilitySetupProjection =
|
|
4062
|
+
| { status: "ready"; action: null; detail: string }
|
|
4063
|
+
| {
|
|
4064
|
+
status: "authorization_required";
|
|
4065
|
+
action: "connect" | "add_credentials" | "enable";
|
|
4066
|
+
detail: string;
|
|
4067
|
+
}
|
|
4068
|
+
| { status: "unavailable"; action: null; detail: string };
|
|
4069
|
+
|
|
4070
|
+
function registerCapabilityDiscoveryTools(
|
|
4071
|
+
server: McpServer,
|
|
4072
|
+
deps: ApiRouteDeps,
|
|
4073
|
+
grant: AccessGrant,
|
|
4074
|
+
sessionId: string,
|
|
4075
|
+
json: JsonResult,
|
|
4076
|
+
): void {
|
|
4077
|
+
const catalog = () =>
|
|
4078
|
+
buildCapabilityCatalog({
|
|
4079
|
+
db: deps.db,
|
|
4080
|
+
workspaceId: grant.workspaceId,
|
|
4081
|
+
settings: deps.settings,
|
|
4082
|
+
subjectId: grant.subjectId,
|
|
4083
|
+
});
|
|
4084
|
+
const authorize = async () => {
|
|
4085
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
4086
|
+
};
|
|
4087
|
+
|
|
4088
|
+
server.registerTool(
|
|
4089
|
+
"capability_catalog_search",
|
|
4090
|
+
{
|
|
4091
|
+
description:
|
|
4092
|
+
"Search OpenGeni's reviewed workspace capability catalog when the user asks to add an integration or the task needs a capability that is not currently usable. Search by the outcome needed (for example `GitHub repositories`, `product analytics`, or `Slack notifications`), compare the returned candidates, and prefer a ready or verified exact match. This only reads secret-free metadata; it never installs, connects, or authorizes anything.",
|
|
4093
|
+
inputSchema: {
|
|
4094
|
+
query: z4.string().min(1).max(500),
|
|
4095
|
+
limit: z4.number().int().min(1).max(20).optional(),
|
|
4096
|
+
},
|
|
4097
|
+
},
|
|
4098
|
+
async ({ query, limit }) => {
|
|
4099
|
+
await authorize();
|
|
4100
|
+
const current = await catalog();
|
|
4101
|
+
const ranked = searchCapabilityCatalogItems(
|
|
4102
|
+
[...current.items, ...nativeConnectionCapabilityRecommendations()],
|
|
4103
|
+
query,
|
|
4104
|
+
limit ?? 8,
|
|
4105
|
+
);
|
|
4106
|
+
const matches = await Promise.all(
|
|
4107
|
+
ranked.map(async ({ item, matchedOn }) => ({
|
|
4108
|
+
capabilityId: item.id,
|
|
4109
|
+
name: item.name,
|
|
4110
|
+
description: item.description,
|
|
4111
|
+
kind: item.kind,
|
|
4112
|
+
source: item.source,
|
|
4113
|
+
category: item.category,
|
|
4114
|
+
tags: item.tags.slice(0, 16),
|
|
4115
|
+
providerDomain: item.providerDomain,
|
|
4116
|
+
authKind: item.authKind,
|
|
4117
|
+
tier: item.tier,
|
|
4118
|
+
matchedOn,
|
|
4119
|
+
setup: {
|
|
4120
|
+
...(await capabilitySetupProjection(deps, grant.workspaceId, item)),
|
|
4121
|
+
requiredVariables: capabilityRequiredVariables(item),
|
|
4122
|
+
},
|
|
4123
|
+
})),
|
|
4124
|
+
);
|
|
4125
|
+
return json({ query, matches });
|
|
4126
|
+
},
|
|
4127
|
+
);
|
|
4128
|
+
|
|
4129
|
+
server.registerTool(
|
|
4130
|
+
"capability_authorization_request",
|
|
4131
|
+
{
|
|
4132
|
+
description:
|
|
4133
|
+
"After capability_catalog_search and after explaining one chosen recommendation to the user, post exactly one in-session human authorization card for that catalog capability. This tool never grants access, enables a capability, reads a secret, or mints provider credentials; the authenticated user must click and confirm the provider/domain flow. Do not call it for a candidate reported ready or unavailable.",
|
|
4134
|
+
inputSchema: {
|
|
4135
|
+
capabilityId: z4.string().min(1).max(512),
|
|
4136
|
+
rationale: z4.string().min(1).max(2000),
|
|
4137
|
+
},
|
|
4138
|
+
},
|
|
4139
|
+
async ({ capabilityId, rationale }) => {
|
|
4140
|
+
await authorize();
|
|
4141
|
+
const current = await catalog();
|
|
4142
|
+
const item = [...current.items, ...nativeConnectionCapabilityRecommendations()].find(
|
|
4143
|
+
(candidate) => candidate.id === capabilityId,
|
|
4144
|
+
);
|
|
4145
|
+
if (!item || !capabilityCatalogItemIsTrustedForExposure(item)) {
|
|
4146
|
+
throw new Error("Unknown or untrusted capability; search the catalog again.");
|
|
4147
|
+
}
|
|
4148
|
+
const setup = await capabilitySetupProjection(deps, grant.workspaceId, item);
|
|
4149
|
+
if (setup.status === "ready") {
|
|
4150
|
+
return json({
|
|
4151
|
+
capabilityId: item.id,
|
|
4152
|
+
status: "ready",
|
|
4153
|
+
message: setup.detail,
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
if (setup.status === "unavailable") {
|
|
4157
|
+
return json({
|
|
4158
|
+
capabilityId: item.id,
|
|
4159
|
+
status: "unavailable",
|
|
4160
|
+
message: setup.detail,
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
const claims = exactAgentCommandContext(grant, sessionId);
|
|
4164
|
+
const payload = ToolAuthNeededPayload.parse({
|
|
4165
|
+
serverId: item.runtime.mcpServerId ?? "opengeni",
|
|
4166
|
+
toolName: "capability_authorization_request",
|
|
4167
|
+
providerDomain: capabilityProviderDomain(item),
|
|
4168
|
+
reason: "missing_connection",
|
|
4169
|
+
capability: {
|
|
4170
|
+
id: item.id,
|
|
4171
|
+
name: item.name,
|
|
4172
|
+
kind: item.kind,
|
|
4173
|
+
source: item.source,
|
|
4174
|
+
action: setup.action,
|
|
4175
|
+
rationale,
|
|
4176
|
+
requiredVariables: capabilityRequiredVariables(item),
|
|
4177
|
+
},
|
|
4178
|
+
});
|
|
4179
|
+
const appended = await appendAndPublishTurnEventsFenced(
|
|
4180
|
+
deps.db,
|
|
4181
|
+
deps.bus,
|
|
4182
|
+
grant.workspaceId,
|
|
4183
|
+
sessionId,
|
|
4184
|
+
claims.callerTurnId,
|
|
4185
|
+
claims.callerExecutionGeneration,
|
|
4186
|
+
claims.callerAttemptId,
|
|
4187
|
+
[{ type: "tool.auth_needed", payload }],
|
|
4188
|
+
);
|
|
4189
|
+
if (!appended.accepted) {
|
|
4190
|
+
throw new Error(
|
|
4191
|
+
"The calling turn was replaced before the authorization request committed.",
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
return json({
|
|
4195
|
+
capabilityId: item.id,
|
|
4196
|
+
status: "authorization_requested",
|
|
4197
|
+
action: setup.action,
|
|
4198
|
+
eventId: appended.events[0]?.id ?? null,
|
|
4199
|
+
message:
|
|
4200
|
+
"The recommendation was posted for human confirmation. No access has been granted yet.",
|
|
4201
|
+
});
|
|
4202
|
+
},
|
|
4203
|
+
);
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
async function capabilitySetupProjection(
|
|
4207
|
+
deps: ApiRouteDeps,
|
|
4208
|
+
workspaceId: string,
|
|
4209
|
+
item: CapabilityCatalogItem,
|
|
4210
|
+
): Promise<CapabilitySetupProjection> {
|
|
4211
|
+
if (item.id === "api:github-app" || item.surfaceType === "first_party_github") {
|
|
4212
|
+
const missing = githubAppMissingSettings(deps.settings);
|
|
4213
|
+
if (missing.length > 0) {
|
|
4214
|
+
return {
|
|
4215
|
+
status: "unavailable",
|
|
4216
|
+
action: null,
|
|
4217
|
+
detail:
|
|
4218
|
+
deps.settings.productAccessMode === "managed"
|
|
4219
|
+
? "GitHub is not available on this deployment."
|
|
4220
|
+
: `The operator must configure the GitHub App first (${missing.join(", ")}).`,
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
const installations = await listWorkspaceGitHubInstallationBindings(deps, workspaceId);
|
|
4224
|
+
if (githubBindingStatus(true, installations) === "bound") {
|
|
4225
|
+
return { status: "ready", action: null, detail: "GitHub is connected and ready." };
|
|
4226
|
+
}
|
|
4227
|
+
return {
|
|
4228
|
+
status: "authorization_required",
|
|
4229
|
+
action: "connect",
|
|
4230
|
+
detail: "A GitHub owner must approve an installation and repository allowlist.",
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
if (item.surfaceType === "codex_apps") {
|
|
4234
|
+
return item.enabled
|
|
4235
|
+
? { status: "ready", action: null, detail: "Codex Apps is connected and ready." }
|
|
4236
|
+
: {
|
|
4237
|
+
status: "authorization_required",
|
|
4238
|
+
action: "connect",
|
|
4239
|
+
detail: "A workspace admin must designate an authorized Codex Apps subscription.",
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
if (item.enabled) {
|
|
4243
|
+
return { status: "ready", action: null, detail: "This capability is already enabled." };
|
|
4244
|
+
}
|
|
4245
|
+
if (!item.runtime.available) {
|
|
4246
|
+
return {
|
|
4247
|
+
status: "unavailable",
|
|
4248
|
+
action: null,
|
|
4249
|
+
detail: item.runtime.notes ?? "This catalog entry has no executable runtime adapter.",
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
if (item.authKind === "oauth2" || item.surfaceType === "first_party_social") {
|
|
4253
|
+
return {
|
|
4254
|
+
status: "authorization_required",
|
|
4255
|
+
action: "connect",
|
|
4256
|
+
detail: "The user must confirm the provider domain and complete sign-in.",
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
if (item.authKind === "api_key" || item.authModel?.toLowerCase().includes("key")) {
|
|
4260
|
+
return {
|
|
4261
|
+
status: "authorization_required",
|
|
4262
|
+
action: "add_credentials",
|
|
4263
|
+
detail: "The user must provide the required credential through the protected setup form.",
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
return {
|
|
4267
|
+
status: "authorization_required",
|
|
4268
|
+
action: "enable",
|
|
4269
|
+
detail: "A workspace admin must review and enable this capability.",
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
function capabilityRequiredVariables(item: CapabilityCatalogItem): string[] {
|
|
4274
|
+
const variableSet = item.metadata.variableSet;
|
|
4275
|
+
if (!variableSet || typeof variableSet !== "object" || Array.isArray(variableSet)) return [];
|
|
4276
|
+
const required = (variableSet as Record<string, unknown>).requiredVariables;
|
|
4277
|
+
return Array.isArray(required)
|
|
4278
|
+
? required.filter((name): name is string => typeof name === "string").slice(0, 64)
|
|
4279
|
+
: [];
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
function capabilityProviderDomain(item: CapabilityCatalogItem): string {
|
|
4283
|
+
if (item.providerDomain?.trim()) return item.providerDomain.trim();
|
|
4284
|
+
for (const candidate of [item.homepageUrl, item.endpointUrl, item.mcpUrl]) {
|
|
4285
|
+
if (!candidate) continue;
|
|
4286
|
+
try {
|
|
4287
|
+
return new URL(candidate).hostname;
|
|
4288
|
+
} catch {
|
|
4289
|
+
// Continue to the local, non-provider fallback below.
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
return "opengeni.local";
|
|
4293
|
+
}
|
|
4294
|
+
|
|
2889
4295
|
function registerGitHubConnectTool(
|
|
2890
4296
|
server: McpServer,
|
|
2891
4297
|
deps: ApiRouteDeps,
|
|
@@ -2976,7 +4382,8 @@ function repositoryWithScheduledTaskResource(
|
|
|
2976
4382
|
kind: "repository",
|
|
2977
4383
|
uri,
|
|
2978
4384
|
ref: repository.defaultBranch,
|
|
2979
|
-
|
|
4385
|
+
provider: "github",
|
|
4386
|
+
mountPath: defaultRepositoryMountPath(uri, "github"),
|
|
2980
4387
|
...(repository.private
|
|
2981
4388
|
? {
|
|
2982
4389
|
githubInstallationId: repository.installationId,
|
|
@@ -3421,11 +4828,12 @@ function parseMcpDate(raw: string, label: string): Date {
|
|
|
3421
4828
|
async function withMcpEffectivePolicy(
|
|
3422
4829
|
deps: ApiRouteDeps,
|
|
3423
4830
|
workspaceId: string,
|
|
4831
|
+
subjectId: string,
|
|
3424
4832
|
session: Session,
|
|
3425
4833
|
): Promise<Session> {
|
|
3426
4834
|
const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
|
|
3427
|
-
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings),
|
|
3428
|
-
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings),
|
|
4835
|
+
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings, subjectId),
|
|
4836
|
+
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings, subjectId),
|
|
3429
4837
|
]);
|
|
3430
4838
|
return sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds);
|
|
3431
4839
|
}
|