@opengeni/api-router 0.23.1 → 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/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
- 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/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- 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 +22 -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 +8 -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/scheduled-task-view.d.ts +168 -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/channel-a.d.ts +55 -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 +274 -37
- 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/index.ts +119 -20
- 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 +123 -33
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +487 -27
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +32 -1
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +923 -145
- package/src/memory-slack-delivery.ts +209 -0
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +284 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-features.ts +258 -0
- 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 +67 -40
- package/src/routes/sessions.ts +421 -245
- package/src/routes/skills.ts +174 -0
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/channel-a.ts +723 -116
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +29 -9
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
package/src/mcp/server.ts
CHANGED
|
@@ -12,16 +12,22 @@ 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,
|
|
27
33
|
type ScheduledTask,
|
|
@@ -30,7 +36,6 @@ import {
|
|
|
30
36
|
WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES,
|
|
31
37
|
} from "@opengeni/contracts";
|
|
32
38
|
import {
|
|
33
|
-
correctWorkspaceMemory,
|
|
34
39
|
countVariableSets,
|
|
35
40
|
beginRigChangeVerificationAttempt,
|
|
36
41
|
createVariableSet,
|
|
@@ -67,7 +72,6 @@ import {
|
|
|
67
72
|
MEMORY_SEARCH_TOOL_DESCRIPTION,
|
|
68
73
|
requireScheduledTask,
|
|
69
74
|
requireSession,
|
|
70
|
-
saveWorkspaceMemory,
|
|
71
75
|
searchWorkspaceMemories,
|
|
72
76
|
serializeEffectiveSessionControl,
|
|
73
77
|
setSessionGoalStatusWithEvent,
|
|
@@ -75,7 +79,6 @@ import {
|
|
|
75
79
|
updateScheduledTask,
|
|
76
80
|
updateSessionGoalWithEvent,
|
|
77
81
|
upsertSessionGoalWithEvent,
|
|
78
|
-
RigChangeAlreadyVerifyingError,
|
|
79
82
|
RigChangeTransitionError,
|
|
80
83
|
createWorkspaceArtifact,
|
|
81
84
|
getWorkspaceArtifact,
|
|
@@ -84,7 +87,11 @@ import {
|
|
|
84
87
|
publishWorkspaceArtifactVersion,
|
|
85
88
|
rollbackWorkspaceArtifact,
|
|
86
89
|
} from "@opengeni/db";
|
|
87
|
-
import {
|
|
90
|
+
import {
|
|
91
|
+
appendAndPublishEvents,
|
|
92
|
+
appendAndPublishTurnEventsFenced,
|
|
93
|
+
publishDurableSessionEvents,
|
|
94
|
+
} from "@opengeni/events";
|
|
88
95
|
import {
|
|
89
96
|
createSignedState,
|
|
90
97
|
GitHubAppConfigurationError,
|
|
@@ -102,9 +109,15 @@ import {
|
|
|
102
109
|
hasLiteralPermission,
|
|
103
110
|
hasPermission,
|
|
104
111
|
authorizedSocialConnectionsForGrant,
|
|
112
|
+
authorizedAtlassianConnectionsForGrant,
|
|
113
|
+
buildCapabilityCatalog,
|
|
114
|
+
nativeConnectionCapabilityRecommendations,
|
|
115
|
+
correctWorkspaceMemoryWithSlackPublication,
|
|
105
116
|
requireLiveAgentAttemptAuthorization,
|
|
106
117
|
requireSessionAuthorization,
|
|
107
118
|
requireSessionAuthorizationListScope,
|
|
119
|
+
saveWorkspaceMemoryWithSlackPublication,
|
|
120
|
+
searchCapabilityCatalogItems,
|
|
108
121
|
type ResolvedSessionAuthorization,
|
|
109
122
|
} from "@opengeni/core";
|
|
110
123
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
@@ -116,12 +129,14 @@ import {
|
|
|
116
129
|
} from "../github-access";
|
|
117
130
|
import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
|
|
118
131
|
import {
|
|
132
|
+
assertSocialConnectionProvider,
|
|
119
133
|
socialMentionsLive,
|
|
120
134
|
socialOwnPostsLive,
|
|
121
135
|
socialPostReply,
|
|
122
136
|
socialSearchLive,
|
|
123
137
|
socialThreadLive,
|
|
124
138
|
} from "../integrations/social-api";
|
|
139
|
+
import { revokeKnowledgeSourceScheduleAuthorization } from "../integrations/google-drive";
|
|
125
140
|
import {
|
|
126
141
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
127
142
|
proposeRigChangeForApi,
|
|
@@ -186,18 +201,24 @@ import {
|
|
|
186
201
|
boundScheduledTaskMcpPage,
|
|
187
202
|
scheduledTaskMcpSummary,
|
|
188
203
|
} from "./scheduled-task-view";
|
|
189
|
-
import type { ToolspaceMcpSurface } from "./toolspace";
|
|
190
204
|
import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
|
|
191
205
|
import {
|
|
192
206
|
createOpenGeniSlackBotClient,
|
|
193
207
|
resolveSlackBotConnectionForTool,
|
|
194
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";
|
|
195
217
|
|
|
196
218
|
export type McpServerOptions = {
|
|
197
219
|
// Origin of the HTTP request that reached the MCP route. Browser-oriented
|
|
198
220
|
// tools use it only when no configured public base URL is available.
|
|
199
221
|
requestOrigin?: string | null;
|
|
200
|
-
toolspace?: ToolspaceMcpSurface | null;
|
|
201
222
|
workspaceMemoryEnabled?: boolean | undefined;
|
|
202
223
|
};
|
|
203
224
|
|
|
@@ -246,6 +267,20 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
246
267
|
session_resume: { allOf: ["sessions:control"] },
|
|
247
268
|
session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
248
269
|
set_other_session_title: { allOf: ["sessions:control"] },
|
|
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"] },
|
|
249
284
|
variable_set_list: { allOf: ["variable-sets:list", "secrets:list"] },
|
|
250
285
|
environment_list: { allOf: ["variable-sets:list", "secrets:list"] },
|
|
251
286
|
variable_set_get_variable: {
|
|
@@ -256,6 +291,11 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
256
291
|
allOf: ["variable-sets:write", "secrets:write"],
|
|
257
292
|
},
|
|
258
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
|
+
},
|
|
259
299
|
github_connect_link: { allOf: ["github:use"] },
|
|
260
300
|
github_repositories_list: { allOf: ["github:use"] },
|
|
261
301
|
social_connections_list: { allOf: ["connections:read"] },
|
|
@@ -270,6 +310,18 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
270
310
|
// Publishes under the user's identity: connections:write keeps it out of the
|
|
271
311
|
// default agent permission set, unlike the read-only social tools above.
|
|
272
312
|
social_post_reply: { allOf: ["connections:write"] },
|
|
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"] },
|
|
273
325
|
scheduled_tasks_list: {
|
|
274
326
|
anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
|
|
275
327
|
},
|
|
@@ -294,11 +346,31 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
294
346
|
slack_bot_file_content: { allOf: ["connections:read"] },
|
|
295
347
|
slack_bot_post_message: { allOf: ["connections:read"] },
|
|
296
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"] },
|
|
297
352
|
artifacts_list: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
298
353
|
artifacts_get_source: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
299
354
|
artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
300
355
|
artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
301
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
|
+
},
|
|
302
374
|
} satisfies Record<FirstPartyMcpToolName, FirstPartyToolAuthorization>;
|
|
303
375
|
|
|
304
376
|
const FIRST_PARTY_MCP_TOOL_NAME_SET = new Set<string>(FIRST_PARTY_MCP_TOOL_NAMES);
|
|
@@ -310,7 +382,6 @@ class PolicyMcpServer extends McpServer {
|
|
|
310
382
|
private readonly grant: AccessGrant,
|
|
311
383
|
private readonly sessionId: string | null,
|
|
312
384
|
private readonly selectedTools: ReadonlySet<FirstPartyMcpToolName> | null,
|
|
313
|
-
private readonly allowUncatalogued: boolean,
|
|
314
385
|
) {
|
|
315
386
|
super({ name: "opengeni", version: "1.0.0" });
|
|
316
387
|
}
|
|
@@ -331,7 +402,7 @@ class PolicyMcpServer extends McpServer {
|
|
|
331
402
|
cb: ToolCallback<InputArgs>,
|
|
332
403
|
): RegisteredTool {
|
|
333
404
|
const catalogued = FIRST_PARTY_MCP_TOOL_NAME_SET.has(name);
|
|
334
|
-
let admitted =
|
|
405
|
+
let admitted = false;
|
|
335
406
|
if (catalogued) {
|
|
336
407
|
const toolName = name as FirstPartyMcpToolName;
|
|
337
408
|
const policy: FirstPartyToolAuthorization = FIRST_PARTY_TOOL_AUTHORIZATION[toolName];
|
|
@@ -388,7 +459,6 @@ export function buildOpenGeniMcpServer(
|
|
|
388
459
|
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
389
460
|
});
|
|
390
461
|
const can = (permission: Permission) => hasPermission(grant.permissions, permission);
|
|
391
|
-
const toolspaceMode = options.toolspace != null;
|
|
392
462
|
let socialConnectionsPromise: ReturnType<typeof authorizedSocialConnectionsForGrant> | undefined;
|
|
393
463
|
const authorizedSocialConnections = () =>
|
|
394
464
|
(socialConnectionsPromise ??= authorizedSocialConnectionsForGrant({
|
|
@@ -403,6 +473,14 @@ export function buildOpenGeniMcpServer(
|
|
|
403
473
|
if (!authority) throw new Error(`Unknown or unavailable social connection: ${connectionId}`);
|
|
404
474
|
return authority;
|
|
405
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
|
+
};
|
|
406
484
|
|
|
407
485
|
// Session-scoped tools key off the worker-asserted sessionId claim (signed
|
|
408
486
|
// into the delegated token by the worker, never agent-controlled).
|
|
@@ -411,17 +489,17 @@ export function buildOpenGeniMcpServer(
|
|
|
411
489
|
? (grant.metadata["sessionId"] as string)
|
|
412
490
|
: null;
|
|
413
491
|
const selectedTools =
|
|
414
|
-
sessionId !== null
|
|
492
|
+
sessionId !== null
|
|
415
493
|
? new Set(
|
|
416
494
|
(grant.metadata?.["firstPartyMcpTools"] as FirstPartyMcpToolName[] | undefined) ??
|
|
417
495
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
418
496
|
)
|
|
419
497
|
: null;
|
|
420
|
-
const server = new PolicyMcpServer(grant, sessionId, selectedTools
|
|
498
|
+
const server = new PolicyMcpServer(grant, sessionId, selectedTools);
|
|
421
499
|
// set_session_title names the agent's OWN session — pure session metadata,
|
|
422
500
|
// not a goal operation — so it is available on every session, gated only on
|
|
423
501
|
// the signed sessionId (NOT goals:manage, and NOT on a goal existing).
|
|
424
|
-
if (sessionId !== null
|
|
502
|
+
if (sessionId !== null) {
|
|
425
503
|
server.registerTool(
|
|
426
504
|
"set_session_title",
|
|
427
505
|
{
|
|
@@ -444,18 +522,23 @@ export function buildOpenGeniMcpServer(
|
|
|
444
522
|
if (sessionId !== null && can("goals:manage")) {
|
|
445
523
|
registerGoalTools(server, deps, grant, sessionId, json);
|
|
446
524
|
}
|
|
447
|
-
|
|
448
|
-
// normal first-party worker token, a bare toolspace:call token does not see
|
|
449
|
-
// unpermissioned session tools; memory follows that title/goal parity and
|
|
450
|
-
// stays on the normal first-party MCP surface only.
|
|
451
|
-
if (!toolspaceMode && sessionId !== null && options.workspaceMemoryEnabled === true) {
|
|
525
|
+
if (sessionId !== null && options.workspaceMemoryEnabled === true) {
|
|
452
526
|
registerMemoryTools(server, deps, grant, sessionId, json);
|
|
453
527
|
}
|
|
454
|
-
if (
|
|
528
|
+
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
455
529
|
registerPreferenceRegistryTools(server, deps, grant, json);
|
|
456
530
|
}
|
|
457
|
-
if (
|
|
531
|
+
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
458
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
|
+
});
|
|
459
542
|
}
|
|
460
543
|
|
|
461
544
|
// Fleet tools (M7 bring-your-own-compute): list / attach / swap / run_on /
|
|
@@ -464,16 +547,15 @@ export function buildOpenGeniMcpServer(
|
|
|
464
547
|
// so they register only when the grant carries the worker-signed sessionId claim
|
|
465
548
|
// (never agent-controlled). Gated on the selfhosted feature flag: the active
|
|
466
549
|
// pointer + swap are only meaningful when bring-your-own-compute is enabled.
|
|
467
|
-
if (
|
|
550
|
+
if (sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
|
|
468
551
|
registerFleetTools(server, deps, grant, sessionId, json);
|
|
469
552
|
}
|
|
470
|
-
if (
|
|
553
|
+
if (can("enrollments:manage") && deps.settings.sandboxSelfhostedEnabled) {
|
|
471
554
|
registerConnectedMachineTools(server, deps, grant, json);
|
|
472
555
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
556
|
+
registerRigTools(server, deps, grant, can, sessionId, json);
|
|
557
|
+
registerSlackBotTools(server, deps, grant, sessionId, json);
|
|
558
|
+
registerAtlassianTools(server, deps, grant, json);
|
|
477
559
|
|
|
478
560
|
// Orchestration, variableSet, and GitHub status tools are permission-gated
|
|
479
561
|
// at registration: a grant without the permission does not see the tool.
|
|
@@ -486,13 +568,16 @@ export function buildOpenGeniMcpServer(
|
|
|
486
568
|
// never returned through a model-visible MCP tool. A user DEMOTES a specific
|
|
487
569
|
// session by setting a narrower session.firstPartyMcpPermissions (capped to
|
|
488
570
|
// the creator's own grant); operators still cap what any session can be given.
|
|
489
|
-
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId,
|
|
490
|
-
registerVariableSetTools(server, deps, grant, can, sessionId,
|
|
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
|
+
}
|
|
491
576
|
if (can("github:use")) {
|
|
492
577
|
registerGitHubConnectTool(server, deps, grant, options, json);
|
|
493
578
|
}
|
|
494
579
|
|
|
495
|
-
if (
|
|
580
|
+
if (can("github:use")) {
|
|
496
581
|
server.registerTool(
|
|
497
582
|
"github_repositories_list",
|
|
498
583
|
{
|
|
@@ -521,7 +606,7 @@ export function buildOpenGeniMcpServer(
|
|
|
521
606
|
);
|
|
522
607
|
}
|
|
523
608
|
|
|
524
|
-
if (
|
|
609
|
+
if (can("connections:read")) {
|
|
525
610
|
server.registerTool(
|
|
526
611
|
"social_connections_list",
|
|
527
612
|
{
|
|
@@ -726,12 +811,122 @@ export function buildOpenGeniMcpServer(
|
|
|
726
811
|
});
|
|
727
812
|
},
|
|
728
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
|
+
}
|
|
729
924
|
}
|
|
730
925
|
|
|
731
926
|
// Writes are gated on connections:write (never in the default first-party
|
|
732
927
|
// agent permission set) so scheduled tasks must opt in, and deployments can
|
|
733
928
|
// additionally wrap posting in a requireApproval policy.
|
|
734
|
-
if (
|
|
929
|
+
if (can("connections:write")) {
|
|
735
930
|
server.registerTool(
|
|
736
931
|
"social_posts_sync",
|
|
737
932
|
{
|
|
@@ -827,9 +1022,106 @@ export function buildOpenGeniMcpServer(
|
|
|
827
1022
|
});
|
|
828
1023
|
},
|
|
829
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
|
+
}
|
|
830
1122
|
}
|
|
831
1123
|
|
|
832
|
-
if (
|
|
1124
|
+
if (can("scheduled_tasks:manage") || can("scheduled_tasks:run")) {
|
|
833
1125
|
server.registerTool(
|
|
834
1126
|
"scheduled_tasks_list",
|
|
835
1127
|
{
|
|
@@ -977,6 +1269,9 @@ export function buildOpenGeniMcpServer(
|
|
|
977
1269
|
sessionAuthorization: deps.sessionAuthorization,
|
|
978
1270
|
authorizationSurface: "first_party_mcp",
|
|
979
1271
|
});
|
|
1272
|
+
if (!scheduledTaskUpdateChangesState(existing, update)) {
|
|
1273
|
+
return json(scheduledTaskReceipt("scheduled_tasks_update", existing, "unchanged", false));
|
|
1274
|
+
}
|
|
980
1275
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, update);
|
|
981
1276
|
await syncUpdatedScheduledTask({
|
|
982
1277
|
db: deps.db,
|
|
@@ -984,7 +1279,7 @@ export function buildOpenGeniMcpServer(
|
|
|
984
1279
|
previous,
|
|
985
1280
|
task,
|
|
986
1281
|
});
|
|
987
|
-
return json(
|
|
1282
|
+
return json(scheduledTaskReceipt("scheduled_tasks_update", task, "updated", true));
|
|
988
1283
|
},
|
|
989
1284
|
);
|
|
990
1285
|
|
|
@@ -996,6 +1291,9 @@ export function buildOpenGeniMcpServer(
|
|
|
996
1291
|
},
|
|
997
1292
|
async ({ id }) => {
|
|
998
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
|
+
}
|
|
999
1297
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
1000
1298
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
|
|
1001
1299
|
status: "paused",
|
|
@@ -1006,7 +1304,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1006
1304
|
previous,
|
|
1007
1305
|
task,
|
|
1008
1306
|
});
|
|
1009
|
-
return json(
|
|
1307
|
+
return json(scheduledTaskReceipt("scheduled_tasks_pause", task, "updated", true));
|
|
1010
1308
|
},
|
|
1011
1309
|
);
|
|
1012
1310
|
|
|
@@ -1018,6 +1316,9 @@ export function buildOpenGeniMcpServer(
|
|
|
1018
1316
|
},
|
|
1019
1317
|
async ({ id }) => {
|
|
1020
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
|
+
}
|
|
1021
1322
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
1022
1323
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
|
|
1023
1324
|
status: "active",
|
|
@@ -1028,7 +1329,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1028
1329
|
previous,
|
|
1029
1330
|
task,
|
|
1030
1331
|
});
|
|
1031
|
-
return json(
|
|
1332
|
+
return json(scheduledTaskReceipt("scheduled_tasks_resume", task, "updated", true));
|
|
1032
1333
|
},
|
|
1033
1334
|
);
|
|
1034
1335
|
|
|
@@ -1044,31 +1345,32 @@ export function buildOpenGeniMcpServer(
|
|
|
1044
1345
|
},
|
|
1045
1346
|
async ({ id, triggerId }) => {
|
|
1046
1347
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
+
}
|
|
1066
1369
|
const triggerToken = scheduledTaskTriggerToken(triggerId);
|
|
1067
|
-
const agentRunUsageIdempotencyKey =
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
);
|
|
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}`;
|
|
1072
1374
|
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
1073
1375
|
await deps.workflowClient.triggerScheduledTask({
|
|
1074
1376
|
task,
|
|
@@ -1077,6 +1379,14 @@ export function buildOpenGeniMcpServer(
|
|
|
1077
1379
|
initiator: { kind: "subject", subjectId: grant.subjectId },
|
|
1078
1380
|
});
|
|
1079
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
|
+
}
|
|
1080
1390
|
await recordWorkspaceUsage(deps, {
|
|
1081
1391
|
accountId: grant.accountId,
|
|
1082
1392
|
workspaceId: grant.workspaceId,
|
|
@@ -1117,6 +1427,17 @@ export function buildOpenGeniMcpServer(
|
|
|
1117
1427
|
},
|
|
1118
1428
|
async ({ id }) => {
|
|
1119
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
|
+
}
|
|
1120
1441
|
await deps.workflowClient.deleteScheduledTaskSchedule({
|
|
1121
1442
|
temporalScheduleId: task.temporalScheduleId,
|
|
1122
1443
|
});
|
|
@@ -1163,7 +1484,6 @@ export function buildOpenGeniMcpServer(
|
|
|
1163
1484
|
);
|
|
1164
1485
|
}
|
|
1165
1486
|
|
|
1166
|
-
registerToolspaceProxyTools(server, options.toolspace ?? null);
|
|
1167
1487
|
server.ensureToolsListHandler();
|
|
1168
1488
|
|
|
1169
1489
|
return server;
|
|
@@ -1404,48 +1724,114 @@ function registerSlackBotTools(
|
|
|
1404
1724
|
);
|
|
1405
1725
|
}
|
|
1406
1726
|
|
|
1407
|
-
function
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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,
|
|
1737
|
+
);
|
|
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
|
+
};
|
|
1759
|
+
|
|
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,
|
|
1426
1806
|
}),
|
|
1427
|
-
)
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
subjectId: surface.subjectId,
|
|
1441
|
-
sessionId: surface.sessionId,
|
|
1442
|
-
...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
1443
|
-
},
|
|
1444
|
-
},
|
|
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),
|
|
1445
1820
|
},
|
|
1446
|
-
|
|
1447
|
-
)
|
|
1448
|
-
|
|
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
|
+
);
|
|
1449
1835
|
}
|
|
1450
1836
|
|
|
1451
1837
|
/** Only a prompt explicitly supplied through the human/API channel may redirect a user-paused goal. */
|
|
@@ -1519,7 +1905,7 @@ function registerGoalTools(
|
|
|
1519
1905
|
? (grant.metadata["turnId"] as string)
|
|
1520
1906
|
: null;
|
|
1521
1907
|
await assertGoalReactivationAllowed(deps, grant.workspaceId, sessionId, callerTurnId);
|
|
1522
|
-
const { goal, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
1908
|
+
const { goal, replaced, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
1523
1909
|
accountId: grant.accountId,
|
|
1524
1910
|
workspaceId: grant.workspaceId,
|
|
1525
1911
|
sessionId,
|
|
@@ -1532,7 +1918,24 @@ function registerGoalTools(
|
|
|
1532
1918
|
if (events.length > 0) {
|
|
1533
1919
|
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
1534
1920
|
}
|
|
1535
|
-
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
|
+
);
|
|
1536
1939
|
},
|
|
1537
1940
|
);
|
|
1538
1941
|
|
|
@@ -1706,7 +2109,7 @@ function registerWorkspaceArtifactTools(
|
|
|
1706
2109
|
json: JsonResult,
|
|
1707
2110
|
): void {
|
|
1708
2111
|
const attempt = () => {
|
|
1709
|
-
const claims =
|
|
2112
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
1710
2113
|
if (!claims) throw new Error("Exact signed artifact attempt authority is required.");
|
|
1711
2114
|
return claims;
|
|
1712
2115
|
};
|
|
@@ -1894,7 +2297,7 @@ function registerWorkspaceArtifactTools(
|
|
|
1894
2297
|
);
|
|
1895
2298
|
}
|
|
1896
2299
|
|
|
1897
|
-
function
|
|
2300
|
+
function exactAgentAttemptClaims(grant: AccessGrant): {
|
|
1898
2301
|
sessionId: string;
|
|
1899
2302
|
turnId: string;
|
|
1900
2303
|
attemptId: string;
|
|
@@ -1927,7 +2330,7 @@ function registerPreferenceRegistryTools(
|
|
|
1927
2330
|
json: JsonResult,
|
|
1928
2331
|
): void {
|
|
1929
2332
|
const attemptClaims = () => {
|
|
1930
|
-
const resolved =
|
|
2333
|
+
const resolved = exactAgentAttemptClaims(grant);
|
|
1931
2334
|
if (!resolved) throw new Error("Exact signed preference attempt authority is required.");
|
|
1932
2335
|
return {
|
|
1933
2336
|
accountId: grant.accountId,
|
|
@@ -1992,11 +2395,69 @@ function scheduledTaskReceipt(
|
|
|
1992
2395
|
});
|
|
1993
2396
|
}
|
|
1994
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
|
+
|
|
1995
2438
|
function memoryPreview(text: string): string {
|
|
1996
2439
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1997
2440
|
return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
|
|
1998
2441
|
}
|
|
1999
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
|
+
|
|
2000
2461
|
function registerMemoryTools(
|
|
2001
2462
|
server: McpServer,
|
|
2002
2463
|
deps: ApiRouteDeps,
|
|
@@ -2004,6 +2465,17 @@ function registerMemoryTools(
|
|
|
2004
2465
|
sessionId: string,
|
|
2005
2466
|
json: JsonResult,
|
|
2006
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
|
+
|
|
2007
2479
|
server.registerTool(
|
|
2008
2480
|
"memory_search",
|
|
2009
2481
|
{
|
|
@@ -2038,10 +2510,12 @@ function registerMemoryTools(
|
|
|
2038
2510
|
kind: MemoryKindSchema,
|
|
2039
2511
|
confidence: z4.number().min(0).max(1).optional(),
|
|
2040
2512
|
replaces_id: z4.string().min(1).optional(),
|
|
2513
|
+
slack_publication: publicationInputSchema.optional(),
|
|
2041
2514
|
},
|
|
2042
2515
|
},
|
|
2043
|
-
async ({ text, kind, confidence, replaces_id }) => {
|
|
2044
|
-
const
|
|
2516
|
+
async ({ text, kind, confidence, replaces_id, slack_publication }) => {
|
|
2517
|
+
const principal = slack_publication ? await publicationActor() : null;
|
|
2518
|
+
const result = await saveWorkspaceMemoryWithSlackPublication(
|
|
2045
2519
|
deps.db,
|
|
2046
2520
|
{
|
|
2047
2521
|
accountId: grant.accountId,
|
|
@@ -2053,6 +2527,13 @@ function registerMemoryTools(
|
|
|
2053
2527
|
...(replaces_id ? { replacesId: replaces_id } : {}),
|
|
2054
2528
|
origin: "agent",
|
|
2055
2529
|
},
|
|
2530
|
+
slack_publication
|
|
2531
|
+
? {
|
|
2532
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
2533
|
+
actor: principal!.actor,
|
|
2534
|
+
ownerLabel: principal!.ownerLabel,
|
|
2535
|
+
}
|
|
2536
|
+
: null,
|
|
2056
2537
|
deps.getDocumentServices().embedder,
|
|
2057
2538
|
);
|
|
2058
2539
|
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
@@ -2104,6 +2585,19 @@ function registerMemoryTools(
|
|
|
2104
2585
|
dedupeReason: result.dedupeReason,
|
|
2105
2586
|
updatedInPlace: result.updated,
|
|
2106
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,
|
|
2107
2601
|
},
|
|
2108
2602
|
}),
|
|
2109
2603
|
);
|
|
@@ -2118,10 +2612,12 @@ function registerMemoryTools(
|
|
|
2118
2612
|
id: z4.string().min(1),
|
|
2119
2613
|
reason: z4.string().min(1).optional(),
|
|
2120
2614
|
replacement_text: z4.string().min(1).optional(),
|
|
2615
|
+
slack_publication: publicationInputSchema.optional(),
|
|
2121
2616
|
},
|
|
2122
2617
|
},
|
|
2123
|
-
async ({ id, reason, replacement_text }) => {
|
|
2124
|
-
const
|
|
2618
|
+
async ({ id, reason, replacement_text, slack_publication }) => {
|
|
2619
|
+
const principal = slack_publication ? await publicationActor() : null;
|
|
2620
|
+
const result = await correctWorkspaceMemoryWithSlackPublication(
|
|
2125
2621
|
deps.db,
|
|
2126
2622
|
{
|
|
2127
2623
|
accountId: grant.accountId,
|
|
@@ -2131,6 +2627,13 @@ function registerMemoryTools(
|
|
|
2131
2627
|
...(reason ? { reason } : {}),
|
|
2132
2628
|
...(replacement_text ? { replacementText: replacement_text } : {}),
|
|
2133
2629
|
},
|
|
2630
|
+
slack_publication
|
|
2631
|
+
? {
|
|
2632
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
2633
|
+
actor: principal!.actor,
|
|
2634
|
+
ownerLabel: principal!.ownerLabel,
|
|
2635
|
+
}
|
|
2636
|
+
: null,
|
|
2134
2637
|
deps.getDocumentServices().embedder,
|
|
2135
2638
|
);
|
|
2136
2639
|
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
@@ -2173,7 +2676,22 @@ function registerMemoryTools(
|
|
|
2173
2676
|
: undefined,
|
|
2174
2677
|
timestamp: (result.replacement ?? result.memory).updatedAt,
|
|
2175
2678
|
idempotency: { status: "not_supported" },
|
|
2176
|
-
facts: {
|
|
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
|
+
},
|
|
2177
2695
|
}),
|
|
2178
2696
|
);
|
|
2179
2697
|
},
|
|
@@ -2309,7 +2827,7 @@ function registerConnectedMachineTools(
|
|
|
2309
2827
|
"connected_machine_remove",
|
|
2310
2828
|
{
|
|
2311
2829
|
description:
|
|
2312
|
-
"Remove one enrolled self-hosted machine while it is offline. Access is revoked, future heartbeat/reconnect credentials are rejected, session/route/lease/archive history is retained, and a fresh human-approved device-flow enrollment is required to reconnect. Pass the enrollmentId from the Machines surface, never a Modal sandbox id. Blocked outcomes include
|
|
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.",
|
|
2313
2831
|
inputSchema: {
|
|
2314
2832
|
enrollmentId: z4.string().uuid(),
|
|
2315
2833
|
expectedUpdatedAt: z4.string().datetime({ offset: true }).optional(),
|
|
@@ -2341,12 +2859,10 @@ async function beginMcpRigVerificationAttempt(
|
|
|
2341
2859
|
try {
|
|
2342
2860
|
return await beginRigChangeVerificationAttempt(deps.db, workspaceId, changeId, {
|
|
2343
2861
|
startedAt: new Date().toISOString(),
|
|
2862
|
+
allowAlreadyVerifying: true,
|
|
2344
2863
|
});
|
|
2345
2864
|
} catch (error) {
|
|
2346
|
-
if (
|
|
2347
|
-
error instanceof RigChangeAlreadyVerifyingError ||
|
|
2348
|
-
error instanceof RigChangeTransitionError
|
|
2349
|
-
) {
|
|
2865
|
+
if (error instanceof RigChangeTransitionError) {
|
|
2350
2866
|
throw new Error(error.message, { cause: error });
|
|
2351
2867
|
}
|
|
2352
2868
|
throw error;
|
|
@@ -2685,7 +3201,6 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2685
3201
|
grant: AccessGrant,
|
|
2686
3202
|
can: (permission: Permission) => boolean,
|
|
2687
3203
|
callerSessionId: string | null,
|
|
2688
|
-
toolspaceMode: boolean,
|
|
2689
3204
|
json: JsonResult,
|
|
2690
3205
|
): void {
|
|
2691
3206
|
if (can("sessions:read")) {
|
|
@@ -2763,7 +3278,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2763
3278
|
);
|
|
2764
3279
|
return json(
|
|
2765
3280
|
boundSessionDetailMcp(
|
|
2766
|
-
await withMcpEffectivePolicy(deps, grant.workspaceId, {
|
|
3281
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, grant.subjectId, {
|
|
2767
3282
|
...projected,
|
|
2768
3283
|
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
|
|
2769
3284
|
}),
|
|
@@ -2911,9 +3426,21 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2911
3426
|
// Bind the spawned session to a rig (freezes its active version);
|
|
2912
3427
|
// declared so MCP validation doesn't strip it before the domain reads it.
|
|
2913
3428
|
rigId: z4.string().uuid().optional(),
|
|
2914
|
-
model: z4
|
|
2915
|
-
|
|
2916
|
-
|
|
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."),
|
|
2917
3444
|
sandboxBackend: z4.string().optional(),
|
|
2918
3445
|
// Create-time machine targeting: an enrolled sandbox id (from
|
|
2919
3446
|
// sandboxes_list) to run the spawned session on. Seeds the active-sandbox
|
|
@@ -3007,7 +3534,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3007
3534
|
);
|
|
3008
3535
|
}
|
|
3009
3536
|
|
|
3010
|
-
if (can("sessions:control")
|
|
3537
|
+
if (can("sessions:control")) {
|
|
3011
3538
|
server.registerTool(
|
|
3012
3539
|
"session_send_message",
|
|
3013
3540
|
{
|
|
@@ -3123,16 +3650,25 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3123
3650
|
reason: reason ?? "agent_mcp_pause",
|
|
3124
3651
|
},
|
|
3125
3652
|
);
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
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
|
+
);
|
|
3136
3672
|
}
|
|
3137
3673
|
const controlled = await controlHumanSessionWorkstream(
|
|
3138
3674
|
deps,
|
|
@@ -3176,16 +3712,25 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3176
3712
|
reason: reason ?? "agent_mcp_resume",
|
|
3177
3713
|
},
|
|
3178
3714
|
);
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
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
|
+
);
|
|
3189
3734
|
}
|
|
3190
3735
|
const controlled = await controlHumanSessionWorkstream(
|
|
3191
3736
|
deps,
|
|
@@ -3283,7 +3828,6 @@ function registerVariableSetTools(
|
|
|
3283
3828
|
grant: AccessGrant,
|
|
3284
3829
|
can: (permission: Permission) => boolean,
|
|
3285
3830
|
sessionId: string | null,
|
|
3286
|
-
toolspaceMode: boolean,
|
|
3287
3831
|
json: JsonResult,
|
|
3288
3832
|
): void {
|
|
3289
3833
|
const registerListTool = (name: string, description: string): void => {
|
|
@@ -3435,7 +3979,6 @@ function registerVariableSetTools(
|
|
|
3435
3979
|
}
|
|
3436
3980
|
|
|
3437
3981
|
if (
|
|
3438
|
-
!toolspaceMode &&
|
|
3439
3982
|
sessionId !== null &&
|
|
3440
3983
|
can("variable-sets:read") &&
|
|
3441
3984
|
hasLiteralPermission(grant.permissions, "secrets:read")
|
|
@@ -3475,7 +4018,7 @@ function registerVariableSetTools(
|
|
|
3475
4018
|
throw new Error("variable set variable names must match ^[A-Z][A-Z0-9_]*$");
|
|
3476
4019
|
}
|
|
3477
4020
|
assertAllowedVariableSetVariableName(parsedName.data);
|
|
3478
|
-
const claims =
|
|
4021
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
3479
4022
|
if (!claims || claims.sessionId !== sessionId) {
|
|
3480
4023
|
throw new Error("Exact signed secret-read attempt authority is required.");
|
|
3481
4024
|
}
|
|
@@ -3515,6 +4058,240 @@ function registerVariableSetTools(
|
|
|
3515
4058
|
}
|
|
3516
4059
|
}
|
|
3517
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
|
+
|
|
3518
4295
|
function registerGitHubConnectTool(
|
|
3519
4296
|
server: McpServer,
|
|
3520
4297
|
deps: ApiRouteDeps,
|
|
@@ -4051,11 +4828,12 @@ function parseMcpDate(raw: string, label: string): Date {
|
|
|
4051
4828
|
async function withMcpEffectivePolicy(
|
|
4052
4829
|
deps: ApiRouteDeps,
|
|
4053
4830
|
workspaceId: string,
|
|
4831
|
+
subjectId: string,
|
|
4054
4832
|
session: Session,
|
|
4055
4833
|
): Promise<Session> {
|
|
4056
4834
|
const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
|
|
4057
|
-
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings),
|
|
4058
|
-
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings),
|
|
4835
|
+
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings, subjectId),
|
|
4836
|
+
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings, subjectId),
|
|
4059
4837
|
]);
|
|
4060
4838
|
return sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds);
|
|
4061
4839
|
}
|