@opengeni/api-router 0.23.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -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/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- 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/fiken.d.ts +233 -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 +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- 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 +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -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/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -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-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -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 +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- 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 +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -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-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
CreateScheduledTaskRequest,
|
|
4
|
-
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
5
4
|
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
6
5
|
defaultRepositoryMountPath,
|
|
7
6
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
@@ -12,25 +11,41 @@ import {
|
|
|
12
11
|
SessionEventResultMode,
|
|
13
12
|
SessionEventSemanticClass,
|
|
14
13
|
SessionEventType,
|
|
14
|
+
stableJson,
|
|
15
15
|
compactSessionEventResult,
|
|
16
16
|
sessionEventLatestClassToSemanticClass,
|
|
17
|
+
MemorySlackPublicationDistribution,
|
|
17
18
|
SessionMcpCredentialUpdateInput,
|
|
19
|
+
ToolAuthNeededPayload,
|
|
18
20
|
VariableSetVariableName,
|
|
21
|
+
capabilityCatalogItemIsTrustedForExposure,
|
|
19
22
|
type AccessGrant,
|
|
23
|
+
type CapabilityCatalogItem,
|
|
20
24
|
type GitHubRepository,
|
|
21
25
|
type FirstPartyMcpToolName,
|
|
22
26
|
type Permission,
|
|
23
27
|
type ResourceRef,
|
|
24
28
|
type SessionAuthorizationOperation,
|
|
29
|
+
type SessionAuthorizationActor,
|
|
25
30
|
type SessionAuthorizationSurface,
|
|
26
31
|
type Session,
|
|
32
|
+
type WorkspaceMemoryPromptMode,
|
|
27
33
|
type ScheduledTask,
|
|
28
34
|
UpdateScheduledTaskRequest,
|
|
29
35
|
normalizeWorkspaceArtifactSlug,
|
|
30
36
|
WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES,
|
|
37
|
+
SESSION_GOAL_PROGRESS_MAX_BYTES,
|
|
38
|
+
SESSION_GOAL_RATIONALE_MAX_BYTES,
|
|
39
|
+
SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
|
|
40
|
+
SESSION_GOAL_TEXT_MAX_BYTES,
|
|
41
|
+
sessionGoalUtf8Bytes,
|
|
42
|
+
TASK_NOTE_LIST_DEFAULT_LIMIT,
|
|
43
|
+
TASK_NOTE_LIST_MAX_LIMIT,
|
|
44
|
+
TASK_NOTE_MAX_LIFETIME_DAYS,
|
|
45
|
+
TASK_NOTE_REASON_MAX_BYTES,
|
|
46
|
+
TASK_NOTE_TEXT_MAX_BYTES,
|
|
31
47
|
} from "@opengeni/contracts";
|
|
32
48
|
import {
|
|
33
|
-
correctWorkspaceMemory,
|
|
34
49
|
countVariableSets,
|
|
35
50
|
beginRigChangeVerificationAttempt,
|
|
36
51
|
createVariableSet,
|
|
@@ -67,15 +82,14 @@ import {
|
|
|
67
82
|
MEMORY_SEARCH_TOOL_DESCRIPTION,
|
|
68
83
|
requireScheduledTask,
|
|
69
84
|
requireSession,
|
|
70
|
-
saveWorkspaceMemory,
|
|
71
85
|
searchWorkspaceMemories,
|
|
72
86
|
serializeEffectiveSessionControl,
|
|
73
87
|
setSessionGoalStatusWithEvent,
|
|
88
|
+
recordSessionGoalProgressWithEvent,
|
|
74
89
|
setVariableSetVariable,
|
|
75
90
|
updateScheduledTask,
|
|
76
91
|
updateSessionGoalWithEvent,
|
|
77
92
|
upsertSessionGoalWithEvent,
|
|
78
|
-
RigChangeAlreadyVerifyingError,
|
|
79
93
|
RigChangeTransitionError,
|
|
80
94
|
createWorkspaceArtifact,
|
|
81
95
|
getWorkspaceArtifact,
|
|
@@ -83,8 +97,16 @@ import {
|
|
|
83
97
|
listWorkspaceArtifacts,
|
|
84
98
|
publishWorkspaceArtifactVersion,
|
|
85
99
|
rollbackWorkspaceArtifact,
|
|
100
|
+
archiveTaskNote,
|
|
101
|
+
createTaskNote,
|
|
102
|
+
listTaskNotes,
|
|
86
103
|
} from "@opengeni/db";
|
|
87
|
-
import {
|
|
104
|
+
import {
|
|
105
|
+
appendAndPublishEvents,
|
|
106
|
+
appendAndPublishTurnEventsFenced,
|
|
107
|
+
publishDurableSessionEvents,
|
|
108
|
+
} from "@opengeni/events";
|
|
109
|
+
import { allowedFirstPartyMcpToolsForSession } from "@opengeni/config";
|
|
88
110
|
import {
|
|
89
111
|
createSignedState,
|
|
90
112
|
GitHubAppConfigurationError,
|
|
@@ -97,14 +119,23 @@ import {
|
|
|
97
119
|
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
98
120
|
import type { AnySchema, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
99
121
|
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
122
|
+
import { HTTPException } from "hono/http-exception";
|
|
100
123
|
import * as z4 from "zod/v4";
|
|
101
124
|
import {
|
|
102
125
|
hasLiteralPermission,
|
|
103
126
|
hasPermission,
|
|
104
127
|
authorizedSocialConnectionsForGrant,
|
|
128
|
+
authorizedAtlassianConnectionsForGrant,
|
|
129
|
+
buildCapabilityCatalog,
|
|
130
|
+
nativeConnectionCapabilityRecommendations,
|
|
131
|
+
correctWorkspaceMemoryWithSlackPublication,
|
|
105
132
|
requireLiveAgentAttemptAuthorization,
|
|
106
133
|
requireSessionAuthorization,
|
|
107
134
|
requireSessionAuthorizationListScope,
|
|
135
|
+
SessionAuthorizationDeniedError,
|
|
136
|
+
SessionAuthorizationUnavailableError,
|
|
137
|
+
saveWorkspaceMemoryWithSlackPublication,
|
|
138
|
+
searchCapabilityCatalogItems,
|
|
108
139
|
type ResolvedSessionAuthorization,
|
|
109
140
|
} from "@opengeni/core";
|
|
110
141
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
@@ -116,12 +147,14 @@ import {
|
|
|
116
147
|
} from "../github-access";
|
|
117
148
|
import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
|
|
118
149
|
import {
|
|
150
|
+
assertSocialConnectionProvider,
|
|
119
151
|
socialMentionsLive,
|
|
120
152
|
socialOwnPostsLive,
|
|
121
153
|
socialPostReply,
|
|
122
154
|
socialSearchLive,
|
|
123
155
|
socialThreadLive,
|
|
124
156
|
} from "../integrations/social-api";
|
|
157
|
+
import { revokeKnowledgeSourceScheduleAuthorization } from "../integrations/google-drive";
|
|
125
158
|
import {
|
|
126
159
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
127
160
|
proposeRigChangeForApi,
|
|
@@ -186,21 +219,140 @@ import {
|
|
|
186
219
|
boundScheduledTaskMcpPage,
|
|
187
220
|
scheduledTaskMcpSummary,
|
|
188
221
|
} from "./scheduled-task-view";
|
|
189
|
-
import type { ToolspaceMcpSurface } from "./toolspace";
|
|
190
222
|
import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
|
|
191
223
|
import {
|
|
192
224
|
createOpenGeniSlackBotClient,
|
|
193
225
|
resolveSlackBotConnectionForTool,
|
|
194
226
|
} from "../integrations/slack-bot";
|
|
227
|
+
import { createFikenClient, resolveFikenConnectionForTool } from "../integrations/fiken";
|
|
228
|
+
import {
|
|
229
|
+
browseAtlassianSources,
|
|
230
|
+
getAtlassianLiveItem,
|
|
231
|
+
revokeAtlassianScheduleAuthorization,
|
|
232
|
+
searchAtlassianLive,
|
|
233
|
+
} from "../integrations/atlassian";
|
|
234
|
+
import { AtlassianConnectionMetadata } from "@opengeni/contracts/atlassian";
|
|
235
|
+
import { registerEditableArtifactAgentTools } from "./editable-artifacts";
|
|
195
236
|
|
|
196
237
|
export type McpServerOptions = {
|
|
197
238
|
// Origin of the HTTP request that reached the MCP route. Browser-oriented
|
|
198
239
|
// tools use it only when no configured public base URL is available.
|
|
199
240
|
requestOrigin?: string | null;
|
|
200
|
-
toolspace?: ToolspaceMcpSurface | null;
|
|
201
241
|
workspaceMemoryEnabled?: boolean | undefined;
|
|
242
|
+
workspaceMemoryPromptMode?: WorkspaceMemoryPromptMode | undefined;
|
|
202
243
|
};
|
|
203
244
|
|
|
245
|
+
const ORCHESTRATION_FAILURE_CODE_MAX_LENGTH = 128;
|
|
246
|
+
const ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
|
|
247
|
+
|
|
248
|
+
type OrchestrationToolName = "session_create" | "session_send_message";
|
|
249
|
+
|
|
250
|
+
function boundedOrchestrationFailureMessage(value: string): string {
|
|
251
|
+
const normalized = value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
|
|
252
|
+
if (!normalized) return "OpenGeni could not complete the request.";
|
|
253
|
+
const encoded = new TextEncoder().encode(normalized);
|
|
254
|
+
if (encoded.byteLength <= ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES) return normalized;
|
|
255
|
+
let end = ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES;
|
|
256
|
+
while (end > 0 && (encoded[end]! & 0xc0) === 0x80) end -= 1;
|
|
257
|
+
return new TextDecoder().decode(encoded.slice(0, end)).trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function orchestrationFailureCode(tool: OrchestrationToolName, error: HTTPException): string {
|
|
261
|
+
const suffix =
|
|
262
|
+
error.status === 401
|
|
263
|
+
? "unauthenticated"
|
|
264
|
+
: error.status === 403
|
|
265
|
+
? "forbidden"
|
|
266
|
+
: error.status === 404
|
|
267
|
+
? "not_found"
|
|
268
|
+
: error.status === 409
|
|
269
|
+
? "conflict"
|
|
270
|
+
: error.status === 429
|
|
271
|
+
? "limit_exceeded"
|
|
272
|
+
: error.status >= 500
|
|
273
|
+
? "unavailable"
|
|
274
|
+
: "rejected";
|
|
275
|
+
return `${tool}_${suffix}`.slice(0, ORCHESTRATION_FAILURE_CODE_MAX_LENGTH);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function orchestrationFailureEnvelope(tool: OrchestrationToolName, error: unknown) {
|
|
279
|
+
if (error instanceof SessionSpawnDeniedError) {
|
|
280
|
+
const denial = sessionSpawnDenialEnvelope(error);
|
|
281
|
+
return {
|
|
282
|
+
error: {
|
|
283
|
+
...denial.error,
|
|
284
|
+
message: boundedOrchestrationFailureMessage(denial.error.message),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (error instanceof HTTPException) {
|
|
289
|
+
return {
|
|
290
|
+
error: {
|
|
291
|
+
code: orchestrationFailureCode(tool, error),
|
|
292
|
+
message:
|
|
293
|
+
error.status >= 500
|
|
294
|
+
? "OpenGeni is temporarily unavailable — retry."
|
|
295
|
+
: boundedOrchestrationFailureMessage(error.message),
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (error instanceof SessionAuthorizationDeniedError) {
|
|
300
|
+
return {
|
|
301
|
+
error: {
|
|
302
|
+
code: `${tool}_not_found_or_denied`,
|
|
303
|
+
message: "Session not found or access denied.",
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
308
|
+
return {
|
|
309
|
+
error: {
|
|
310
|
+
code: `${tool}_authorization_unavailable`,
|
|
311
|
+
message: "Session authorization is temporarily unavailable — retry.",
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const typedCode =
|
|
316
|
+
error && typeof error === "object" && "code" in error
|
|
317
|
+
? (error as { code?: unknown }).code
|
|
318
|
+
: null;
|
|
319
|
+
const knownFailure =
|
|
320
|
+
typedCode === "CALLER_STALE"
|
|
321
|
+
? ["caller_stale", "The calling session no longer owns this attempt."]
|
|
322
|
+
: typedCode === "CALLER_INTERRUPTED"
|
|
323
|
+
? ["caller_interrupted", "The calling session was interrupted before delivery."]
|
|
324
|
+
: typedCode === "TARGET_NOT_VERTICAL"
|
|
325
|
+
? ["target_not_vertical", "Agents may message only their parent or immediate children."]
|
|
326
|
+
: typedCode === "CONTROL_CHANGED"
|
|
327
|
+
? ["conflict", "The target session control state changed; refresh and retry."]
|
|
328
|
+
: typedCode === "IDEMPOTENCY_KEY_REUSED"
|
|
329
|
+
? ["idempotency_key_reused", "The idempotency key was reused with different input."]
|
|
330
|
+
: null;
|
|
331
|
+
if (knownFailure) {
|
|
332
|
+
return {
|
|
333
|
+
error: {
|
|
334
|
+
code: `${tool}_${knownFailure[0]}`,
|
|
335
|
+
message: knownFailure[1],
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
error: {
|
|
341
|
+
code: `${tool}_failed`,
|
|
342
|
+
message: "OpenGeni could not complete the request.",
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function orchestrationFailureResult(tool: OrchestrationToolName, error: unknown) {
|
|
348
|
+
const envelope = orchestrationFailureEnvelope(tool, error);
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: "text" as const, text: JSON.stringify(envelope, null, 2) }],
|
|
351
|
+
structuredContent: envelope,
|
|
352
|
+
isError: true as const,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
204
356
|
type FirstPartyToolAuthorization = {
|
|
205
357
|
sessionRequired?: true;
|
|
206
358
|
allOf?: readonly Permission[];
|
|
@@ -216,6 +368,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
216
368
|
set_session_title: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
217
369
|
goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
218
370
|
goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
371
|
+
goal_progress: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
219
372
|
goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
220
373
|
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
221
374
|
memory_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
@@ -226,6 +379,9 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
226
379
|
allOf: ["workspace:read"],
|
|
227
380
|
},
|
|
228
381
|
preference_registry_get: { sessionRequired: true, allOf: ["workspace:read"] },
|
|
382
|
+
task_notes_list: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
383
|
+
task_note_save: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
384
|
+
task_note_archive: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
229
385
|
sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
230
386
|
sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
231
387
|
sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
@@ -246,6 +402,27 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
246
402
|
session_resume: { allOf: ["sessions:control"] },
|
|
247
403
|
session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
248
404
|
set_other_session_title: { allOf: ["sessions:control"] },
|
|
405
|
+
interaction_discover: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
406
|
+
browser_open: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
407
|
+
browser_tabs: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
408
|
+
browser_observe: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
409
|
+
browser_act: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
410
|
+
browser_clipboard: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
411
|
+
browser_debug: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
412
|
+
browser_auth: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
413
|
+
interaction_request_human: {
|
|
414
|
+
sessionRequired: true,
|
|
415
|
+
allOf: ["sessions:control"],
|
|
416
|
+
},
|
|
417
|
+
browser_identity: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
418
|
+
browser_publish: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
419
|
+
browser_lifecycle: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
420
|
+
computer_open: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
421
|
+
computer_targets: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
422
|
+
computer_observe: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
423
|
+
computer_clipboard: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
424
|
+
computer_act: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
425
|
+
computer_lifecycle: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
249
426
|
variable_set_list: { allOf: ["variable-sets:list", "secrets:list"] },
|
|
250
427
|
environment_list: { allOf: ["variable-sets:list", "secrets:list"] },
|
|
251
428
|
variable_set_get_variable: {
|
|
@@ -256,6 +433,14 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
256
433
|
allOf: ["variable-sets:write", "secrets:write"],
|
|
257
434
|
},
|
|
258
435
|
environment_set_variable: { allOf: ["variable-sets:write", "secrets:write"] },
|
|
436
|
+
capability_catalog_search: {
|
|
437
|
+
sessionRequired: true,
|
|
438
|
+
allOf: ["workspace:read"],
|
|
439
|
+
},
|
|
440
|
+
capability_authorization_request: {
|
|
441
|
+
sessionRequired: true,
|
|
442
|
+
allOf: ["workspace:read"],
|
|
443
|
+
},
|
|
259
444
|
github_connect_link: { allOf: ["github:use"] },
|
|
260
445
|
github_repositories_list: { allOf: ["github:use"] },
|
|
261
446
|
social_connections_list: { allOf: ["connections:read"] },
|
|
@@ -270,6 +455,18 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
270
455
|
// Publishes under the user's identity: connections:write keeps it out of the
|
|
271
456
|
// default agent permission set, unlike the read-only social tools above.
|
|
272
457
|
social_post_reply: { allOf: ["connections:write"] },
|
|
458
|
+
x_accounts_list: { allOf: ["connections:read"] },
|
|
459
|
+
x_search_live: { allOf: ["connections:read"] },
|
|
460
|
+
x_mentions_live: { allOf: ["connections:read"] },
|
|
461
|
+
x_thread_fetch: { allOf: ["connections:read"] },
|
|
462
|
+
x_posts_sync: { allOf: ["connections:write"] },
|
|
463
|
+
x_post_reply: { allOf: ["connections:write"] },
|
|
464
|
+
reddit_accounts_list: { allOf: ["connections:read"] },
|
|
465
|
+
reddit_search_live: { allOf: ["connections:read"] },
|
|
466
|
+
reddit_mentions_live: { allOf: ["connections:read"] },
|
|
467
|
+
reddit_thread_fetch: { allOf: ["connections:read"] },
|
|
468
|
+
reddit_posts_sync: { allOf: ["connections:write"] },
|
|
469
|
+
reddit_post_reply: { allOf: ["connections:write"] },
|
|
273
470
|
scheduled_tasks_list: {
|
|
274
471
|
anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
|
|
275
472
|
},
|
|
@@ -294,11 +491,52 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
294
491
|
slack_bot_file_content: { allOf: ["connections:read"] },
|
|
295
492
|
slack_bot_post_message: { allOf: ["connections:read"] },
|
|
296
493
|
slack_bot_delete_message: { allOf: ["connections:read"] },
|
|
494
|
+
fiken_companies_list: { allOf: ["connections:read"] },
|
|
495
|
+
fiken_contacts_list: { allOf: ["connections:read"] },
|
|
496
|
+
fiken_products_list: { allOf: ["connections:read"] },
|
|
497
|
+
fiken_invoices_list: { allOf: ["connections:read"] },
|
|
498
|
+
fiken_invoice_get: { allOf: ["connections:read"] },
|
|
499
|
+
fiken_bank_accounts_list: { allOf: ["connections:read"] },
|
|
500
|
+
fiken_purchases_list: { allOf: ["connections:read"] },
|
|
501
|
+
fiken_sales_list: { allOf: ["connections:read"] },
|
|
502
|
+
// Writes into the workspace's real accounting ledger surface take the write
|
|
503
|
+
// scope, keeping them out of the default agent permission set.
|
|
504
|
+
fiken_contact_create: { allOf: ["connections:write"] },
|
|
505
|
+
fiken_invoice_draft_create: { allOf: ["connections:write"] },
|
|
506
|
+
atlassian_sources_list: { allOf: ["connections:read"] },
|
|
507
|
+
atlassian_search: { allOf: ["connections:read"] },
|
|
508
|
+
atlassian_get: { allOf: ["connections:read"] },
|
|
297
509
|
artifacts_list: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
298
510
|
artifacts_get_source: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
299
511
|
artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
300
512
|
artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
301
513
|
artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
514
|
+
editable_artifact_list: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
515
|
+
editable_artifact_create: {
|
|
516
|
+
sessionRequired: true,
|
|
517
|
+
allOf: ["artifacts:publish"],
|
|
518
|
+
},
|
|
519
|
+
editable_artifact_import: {
|
|
520
|
+
sessionRequired: true,
|
|
521
|
+
allOf: ["artifacts:publish", "files:read"],
|
|
522
|
+
},
|
|
523
|
+
editable_artifact_get: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
524
|
+
editable_artifact_inspect: {
|
|
525
|
+
sessionRequired: true,
|
|
526
|
+
allOf: ["artifacts:read"],
|
|
527
|
+
},
|
|
528
|
+
editable_artifact_apply: {
|
|
529
|
+
sessionRequired: true,
|
|
530
|
+
allOf: ["artifacts:publish"],
|
|
531
|
+
},
|
|
532
|
+
editable_artifact_export: {
|
|
533
|
+
sessionRequired: true,
|
|
534
|
+
allOf: ["artifacts:read"],
|
|
535
|
+
},
|
|
536
|
+
editable_artifact_export_status: {
|
|
537
|
+
sessionRequired: true,
|
|
538
|
+
allOf: ["artifacts:read", "files:upload"],
|
|
539
|
+
},
|
|
302
540
|
} satisfies Record<FirstPartyMcpToolName, FirstPartyToolAuthorization>;
|
|
303
541
|
|
|
304
542
|
const FIRST_PARTY_MCP_TOOL_NAME_SET = new Set<string>(FIRST_PARTY_MCP_TOOL_NAMES);
|
|
@@ -310,7 +548,6 @@ class PolicyMcpServer extends McpServer {
|
|
|
310
548
|
private readonly grant: AccessGrant,
|
|
311
549
|
private readonly sessionId: string | null,
|
|
312
550
|
private readonly selectedTools: ReadonlySet<FirstPartyMcpToolName> | null,
|
|
313
|
-
private readonly allowUncatalogued: boolean,
|
|
314
551
|
) {
|
|
315
552
|
super({ name: "opengeni", version: "1.0.0" });
|
|
316
553
|
}
|
|
@@ -331,7 +568,7 @@ class PolicyMcpServer extends McpServer {
|
|
|
331
568
|
cb: ToolCallback<InputArgs>,
|
|
332
569
|
): RegisteredTool {
|
|
333
570
|
const catalogued = FIRST_PARTY_MCP_TOOL_NAME_SET.has(name);
|
|
334
|
-
let admitted =
|
|
571
|
+
let admitted = false;
|
|
335
572
|
if (catalogued) {
|
|
336
573
|
const toolName = name as FirstPartyMcpToolName;
|
|
337
574
|
const policy: FirstPartyToolAuthorization = FIRST_PARTY_TOOL_AUTHORIZATION[toolName];
|
|
@@ -388,7 +625,6 @@ export function buildOpenGeniMcpServer(
|
|
|
388
625
|
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
389
626
|
});
|
|
390
627
|
const can = (permission: Permission) => hasPermission(grant.permissions, permission);
|
|
391
|
-
const toolspaceMode = options.toolspace != null;
|
|
392
628
|
let socialConnectionsPromise: ReturnType<typeof authorizedSocialConnectionsForGrant> | undefined;
|
|
393
629
|
const authorizedSocialConnections = () =>
|
|
394
630
|
(socialConnectionsPromise ??= authorizedSocialConnectionsForGrant({
|
|
@@ -403,6 +639,14 @@ export function buildOpenGeniMcpServer(
|
|
|
403
639
|
if (!authority) throw new Error(`Unknown or unavailable social connection: ${connectionId}`);
|
|
404
640
|
return authority;
|
|
405
641
|
};
|
|
642
|
+
const requireAuthorizedSocialConnectionForProvider = async (
|
|
643
|
+
provider: "x" | "reddit",
|
|
644
|
+
connectionId: string,
|
|
645
|
+
) => {
|
|
646
|
+
const authority = await requireAuthorizedSocialConnection(connectionId);
|
|
647
|
+
assertSocialConnectionProvider(authority.connection, provider);
|
|
648
|
+
return authority;
|
|
649
|
+
};
|
|
406
650
|
|
|
407
651
|
// Session-scoped tools key off the worker-asserted sessionId claim (signed
|
|
408
652
|
// into the delegated token by the worker, never agent-controlled).
|
|
@@ -411,17 +655,28 @@ export function buildOpenGeniMcpServer(
|
|
|
411
655
|
? (grant.metadata["sessionId"] as string)
|
|
412
656
|
: null;
|
|
413
657
|
const selectedTools =
|
|
414
|
-
sessionId !== null
|
|
658
|
+
sessionId !== null
|
|
415
659
|
? new Set(
|
|
416
|
-
(
|
|
417
|
-
|
|
660
|
+
allowedFirstPartyMcpToolsForSession(
|
|
661
|
+
deps.settings,
|
|
662
|
+
grant.metadata?.["firstPartyMcpTools"] as FirstPartyMcpToolName[] | undefined,
|
|
663
|
+
),
|
|
418
664
|
)
|
|
419
665
|
: null;
|
|
420
|
-
const
|
|
666
|
+
const nestedAgentDepth = grant.metadata?.["nestedAgentDepth"];
|
|
667
|
+
const effectiveMaxNestedAgentDepth = grant.metadata?.["effectiveMaxNestedAgentDepth"];
|
|
668
|
+
// Optional claims keep rolling deployments compatible. When both trusted
|
|
669
|
+
// facts are present, an exhausted session does not receive an unusable spawn
|
|
670
|
+
// tool; stale/legacy callers still meet the authoritative DB admission gate.
|
|
671
|
+
const sessionCreateVisible =
|
|
672
|
+
typeof nestedAgentDepth !== "number" ||
|
|
673
|
+
typeof effectiveMaxNestedAgentDepth !== "number" ||
|
|
674
|
+
nestedAgentDepth < effectiveMaxNestedAgentDepth;
|
|
675
|
+
const server = new PolicyMcpServer(grant, sessionId, selectedTools);
|
|
421
676
|
// set_session_title names the agent's OWN session — pure session metadata,
|
|
422
677
|
// not a goal operation — so it is available on every session, gated only on
|
|
423
678
|
// the signed sessionId (NOT goals:manage, and NOT on a goal existing).
|
|
424
|
-
if (sessionId !== null
|
|
679
|
+
if (sessionId !== null) {
|
|
425
680
|
server.registerTool(
|
|
426
681
|
"set_session_title",
|
|
427
682
|
{
|
|
@@ -444,18 +699,31 @@ export function buildOpenGeniMcpServer(
|
|
|
444
699
|
if (sessionId !== null && can("goals:manage")) {
|
|
445
700
|
registerGoalTools(server, deps, grant, sessionId, json);
|
|
446
701
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
702
|
+
if (sessionId !== null && options.workspaceMemoryEnabled === true) {
|
|
703
|
+
registerMemoryTools(
|
|
704
|
+
server,
|
|
705
|
+
deps,
|
|
706
|
+
grant,
|
|
707
|
+
sessionId,
|
|
708
|
+
json,
|
|
709
|
+
options.workspaceMemoryPromptMode ?? "legacy_standing",
|
|
710
|
+
);
|
|
453
711
|
}
|
|
454
|
-
if (
|
|
712
|
+
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
455
713
|
registerPreferenceRegistryTools(server, deps, grant, json);
|
|
714
|
+
registerTaskNoteTools(server, deps, grant, sessionId, json);
|
|
456
715
|
}
|
|
457
|
-
if (
|
|
716
|
+
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
458
717
|
registerWorkspaceArtifactTools(server, deps, grant, sessionId, json);
|
|
718
|
+
registerEditableArtifactAgentTools({
|
|
719
|
+
server,
|
|
720
|
+
deps,
|
|
721
|
+
grant,
|
|
722
|
+
sessionId,
|
|
723
|
+
authorize: async () => {
|
|
724
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
725
|
+
},
|
|
726
|
+
});
|
|
459
727
|
}
|
|
460
728
|
|
|
461
729
|
// Fleet tools (M7 bring-your-own-compute): list / attach / swap / run_on /
|
|
@@ -464,16 +732,16 @@ export function buildOpenGeniMcpServer(
|
|
|
464
732
|
// so they register only when the grant carries the worker-signed sessionId claim
|
|
465
733
|
// (never agent-controlled). Gated on the selfhosted feature flag: the active
|
|
466
734
|
// pointer + swap are only meaningful when bring-your-own-compute is enabled.
|
|
467
|
-
if (
|
|
735
|
+
if (sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
|
|
468
736
|
registerFleetTools(server, deps, grant, sessionId, json);
|
|
469
737
|
}
|
|
470
|
-
if (
|
|
738
|
+
if (can("enrollments:manage") && deps.settings.sandboxSelfhostedEnabled) {
|
|
471
739
|
registerConnectedMachineTools(server, deps, grant, json);
|
|
472
740
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
741
|
+
registerRigTools(server, deps, grant, can, sessionId, json);
|
|
742
|
+
registerSlackBotTools(server, deps, grant, sessionId, json);
|
|
743
|
+
registerFikenTools(server, deps, grant, sessionId, json);
|
|
744
|
+
registerAtlassianTools(server, deps, grant, json);
|
|
477
745
|
|
|
478
746
|
// Orchestration, variableSet, and GitHub status tools are permission-gated
|
|
479
747
|
// at registration: a grant without the permission does not see the tool.
|
|
@@ -486,13 +754,24 @@ export function buildOpenGeniMcpServer(
|
|
|
486
754
|
// never returned through a model-visible MCP tool. A user DEMOTES a specific
|
|
487
755
|
// session by setting a narrower session.firstPartyMcpPermissions (capped to
|
|
488
756
|
// the creator's own grant); operators still cap what any session can be given.
|
|
489
|
-
registerWorkspaceOrchestrationTools(
|
|
490
|
-
|
|
757
|
+
registerWorkspaceOrchestrationTools(
|
|
758
|
+
server,
|
|
759
|
+
deps,
|
|
760
|
+
grant,
|
|
761
|
+
can,
|
|
762
|
+
sessionId,
|
|
763
|
+
sessionCreateVisible,
|
|
764
|
+
json,
|
|
765
|
+
);
|
|
766
|
+
registerVariableSetTools(server, deps, grant, can, sessionId, json);
|
|
767
|
+
if (sessionId !== null && can("workspace:read")) {
|
|
768
|
+
registerCapabilityDiscoveryTools(server, deps, grant, sessionId, json);
|
|
769
|
+
}
|
|
491
770
|
if (can("github:use")) {
|
|
492
771
|
registerGitHubConnectTool(server, deps, grant, options, json);
|
|
493
772
|
}
|
|
494
773
|
|
|
495
|
-
if (
|
|
774
|
+
if (can("github:use")) {
|
|
496
775
|
server.registerTool(
|
|
497
776
|
"github_repositories_list",
|
|
498
777
|
{
|
|
@@ -521,7 +800,7 @@ export function buildOpenGeniMcpServer(
|
|
|
521
800
|
);
|
|
522
801
|
}
|
|
523
802
|
|
|
524
|
-
if (
|
|
803
|
+
if (can("connections:read")) {
|
|
525
804
|
server.registerTool(
|
|
526
805
|
"social_connections_list",
|
|
527
806
|
{
|
|
@@ -726,12 +1005,131 @@ export function buildOpenGeniMcpServer(
|
|
|
726
1005
|
});
|
|
727
1006
|
},
|
|
728
1007
|
);
|
|
1008
|
+
|
|
1009
|
+
// Provider-scoped aliases are the canonical tools advertised by the X and
|
|
1010
|
+
// Reddit Integration cards. The legacy social_* names remain available to
|
|
1011
|
+
// existing Packs/sessions, but these names bind the provider in the tool
|
|
1012
|
+
// identity and reject a near-identical Connection from the other adapter.
|
|
1013
|
+
for (const provider of ["x", "reddit"] as const) {
|
|
1014
|
+
const providerName = provider === "x" ? "X" : "Reddit";
|
|
1015
|
+
server.registerTool(
|
|
1016
|
+
`${provider}_accounts_list`,
|
|
1017
|
+
{
|
|
1018
|
+
description: `List the exact visible ${providerName} accounts available to this work.`,
|
|
1019
|
+
inputSchema: { limit: z4.number().int().positive().optional() },
|
|
1020
|
+
},
|
|
1021
|
+
async ({ limit }) =>
|
|
1022
|
+
json({
|
|
1023
|
+
connections: (await authorizedSocialConnections())
|
|
1024
|
+
.filter(
|
|
1025
|
+
({ connection }) =>
|
|
1026
|
+
connection.provider === provider && connection.status !== "disabled",
|
|
1027
|
+
)
|
|
1028
|
+
.slice(0, boundedMcpLimit(limit))
|
|
1029
|
+
.map(({ connection }) => connection),
|
|
1030
|
+
}),
|
|
1031
|
+
);
|
|
1032
|
+
server.registerTool(
|
|
1033
|
+
`${provider}_search_live`,
|
|
1034
|
+
{
|
|
1035
|
+
description:
|
|
1036
|
+
provider === "x"
|
|
1037
|
+
? "Search recent X conversations through one exact connected X account."
|
|
1038
|
+
: "Search Reddit through one exact connected Reddit account; optionally scope to a subreddit.",
|
|
1039
|
+
inputSchema: {
|
|
1040
|
+
connectionId: z4.string().uuid(),
|
|
1041
|
+
query: z4.string().min(1).max(512),
|
|
1042
|
+
subreddit: z4.string().min(1).max(100).optional(),
|
|
1043
|
+
limit: z4.number().int().positive().optional(),
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
async ({ connectionId, query, subreddit, limit }) => {
|
|
1047
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
1048
|
+
provider,
|
|
1049
|
+
connectionId,
|
|
1050
|
+
);
|
|
1051
|
+
const result = await socialSearchLive(
|
|
1052
|
+
deps,
|
|
1053
|
+
{
|
|
1054
|
+
workspaceId: grant.workspaceId,
|
|
1055
|
+
connectionId,
|
|
1056
|
+
subjectId: authority.subjectId,
|
|
1057
|
+
},
|
|
1058
|
+
{ query, subreddit, limit },
|
|
1059
|
+
);
|
|
1060
|
+
return json({
|
|
1061
|
+
provider: result.connection.provider,
|
|
1062
|
+
posts: result.posts,
|
|
1063
|
+
});
|
|
1064
|
+
},
|
|
1065
|
+
);
|
|
1066
|
+
server.registerTool(
|
|
1067
|
+
`${provider}_mentions_live`,
|
|
1068
|
+
{
|
|
1069
|
+
description: `Fetch live ${providerName} mentions and replies through one exact connected account.`,
|
|
1070
|
+
inputSchema: {
|
|
1071
|
+
connectionId: z4.string().uuid(),
|
|
1072
|
+
sinceId: z4.string().optional(),
|
|
1073
|
+
limit: z4.number().int().positive().optional(),
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
async ({ connectionId, sinceId, limit }) => {
|
|
1077
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
1078
|
+
provider,
|
|
1079
|
+
connectionId,
|
|
1080
|
+
);
|
|
1081
|
+
const result = await socialMentionsLive(
|
|
1082
|
+
deps,
|
|
1083
|
+
{
|
|
1084
|
+
workspaceId: grant.workspaceId,
|
|
1085
|
+
connectionId,
|
|
1086
|
+
subjectId: authority.subjectId,
|
|
1087
|
+
},
|
|
1088
|
+
{ sinceId, limit },
|
|
1089
|
+
);
|
|
1090
|
+
return json({
|
|
1091
|
+
provider: result.connection.provider,
|
|
1092
|
+
posts: result.posts,
|
|
1093
|
+
});
|
|
1094
|
+
},
|
|
1095
|
+
);
|
|
1096
|
+
server.registerTool(
|
|
1097
|
+
`${provider}_thread_fetch`,
|
|
1098
|
+
{
|
|
1099
|
+
description: `Fetch one live ${providerName} conversation thread through an exact connected account.`,
|
|
1100
|
+
inputSchema: {
|
|
1101
|
+
connectionId: z4.string().uuid(),
|
|
1102
|
+
id: z4.string().min(1).max(100),
|
|
1103
|
+
limit: z4.number().int().positive().optional(),
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
async ({ connectionId, id, limit }) => {
|
|
1107
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
1108
|
+
provider,
|
|
1109
|
+
connectionId,
|
|
1110
|
+
);
|
|
1111
|
+
const result = await socialThreadLive(
|
|
1112
|
+
deps,
|
|
1113
|
+
{
|
|
1114
|
+
workspaceId: grant.workspaceId,
|
|
1115
|
+
connectionId,
|
|
1116
|
+
subjectId: authority.subjectId,
|
|
1117
|
+
},
|
|
1118
|
+
{ id, limit },
|
|
1119
|
+
);
|
|
1120
|
+
return json({
|
|
1121
|
+
provider: result.connection.provider,
|
|
1122
|
+
posts: result.posts,
|
|
1123
|
+
});
|
|
1124
|
+
},
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
729
1127
|
}
|
|
730
1128
|
|
|
731
1129
|
// Writes are gated on connections:write (never in the default first-party
|
|
732
1130
|
// agent permission set) so scheduled tasks must opt in, and deployments can
|
|
733
1131
|
// additionally wrap posting in a requireApproval policy.
|
|
734
|
-
if (
|
|
1132
|
+
if (can("connections:write")) {
|
|
735
1133
|
server.registerTool(
|
|
736
1134
|
"social_posts_sync",
|
|
737
1135
|
{
|
|
@@ -827,9 +1225,106 @@ export function buildOpenGeniMcpServer(
|
|
|
827
1225
|
});
|
|
828
1226
|
},
|
|
829
1227
|
);
|
|
1228
|
+
|
|
1229
|
+
for (const provider of ["x", "reddit"] as const) {
|
|
1230
|
+
const providerName = provider === "x" ? "X" : "Reddit";
|
|
1231
|
+
server.registerTool(
|
|
1232
|
+
`${provider}_posts_sync`,
|
|
1233
|
+
{
|
|
1234
|
+
description: `Sync one exact connected ${providerName} account's recent posts into OpenGeni (idempotent).`,
|
|
1235
|
+
inputSchema: {
|
|
1236
|
+
connectionId: z4.string().uuid(),
|
|
1237
|
+
limit: z4.number().int().positive().optional(),
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
async ({ connectionId, limit }) => {
|
|
1241
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
1242
|
+
provider,
|
|
1243
|
+
connectionId,
|
|
1244
|
+
);
|
|
1245
|
+
const result = await socialOwnPostsLive(
|
|
1246
|
+
deps,
|
|
1247
|
+
{
|
|
1248
|
+
workspaceId: grant.workspaceId,
|
|
1249
|
+
connectionId,
|
|
1250
|
+
subjectId: authority.subjectId,
|
|
1251
|
+
},
|
|
1252
|
+
{ limit },
|
|
1253
|
+
);
|
|
1254
|
+
const datedPosts = result.posts.filter((post) => post.createdAt !== null);
|
|
1255
|
+
const synced = await recordSyncedSocialPosts(deps.db, {
|
|
1256
|
+
accountId: grant.accountId,
|
|
1257
|
+
workspaceId: grant.workspaceId,
|
|
1258
|
+
connectionId,
|
|
1259
|
+
subjectId: authority.subjectId,
|
|
1260
|
+
posts: datedPosts.map((post) => ({
|
|
1261
|
+
externalPostId: post.id,
|
|
1262
|
+
url: post.url,
|
|
1263
|
+
authorHandle: post.author,
|
|
1264
|
+
text: post.text,
|
|
1265
|
+
publishedAt: new Date(post.createdAt!),
|
|
1266
|
+
metrics: post.metrics,
|
|
1267
|
+
})),
|
|
1268
|
+
});
|
|
1269
|
+
return json({
|
|
1270
|
+
provider: result.connection.provider,
|
|
1271
|
+
fetched: result.posts.length,
|
|
1272
|
+
inserted: synced.inserted,
|
|
1273
|
+
skipped: synced.skipped,
|
|
1274
|
+
skippedMissingDate: result.posts.length - datedPosts.length,
|
|
1275
|
+
});
|
|
1276
|
+
},
|
|
1277
|
+
);
|
|
1278
|
+
server.registerTool(
|
|
1279
|
+
`${provider}_post_reply`,
|
|
1280
|
+
{
|
|
1281
|
+
description: `Publish a reply from one exact connected ${providerName} account. This is a public write and should require approval.`,
|
|
1282
|
+
inputSchema: {
|
|
1283
|
+
connectionId: z4.string().uuid(),
|
|
1284
|
+
inReplyToId: z4.string().min(1).max(100),
|
|
1285
|
+
text: z4.string().min(1).max(10000),
|
|
1286
|
+
},
|
|
1287
|
+
},
|
|
1288
|
+
async ({ connectionId, inReplyToId, text }) => {
|
|
1289
|
+
const authority = await requireAuthorizedSocialConnectionForProvider(
|
|
1290
|
+
provider,
|
|
1291
|
+
connectionId,
|
|
1292
|
+
);
|
|
1293
|
+
const result = await socialPostReply(
|
|
1294
|
+
deps,
|
|
1295
|
+
{
|
|
1296
|
+
workspaceId: grant.workspaceId,
|
|
1297
|
+
connectionId,
|
|
1298
|
+
subjectId: authority.subjectId,
|
|
1299
|
+
},
|
|
1300
|
+
{ inReplyToId, text },
|
|
1301
|
+
);
|
|
1302
|
+
await recordAuditEvent(deps.db, {
|
|
1303
|
+
accountId: grant.accountId,
|
|
1304
|
+
workspaceId: grant.workspaceId,
|
|
1305
|
+
subjectId: grant.subjectId,
|
|
1306
|
+
action: "social.post_reply",
|
|
1307
|
+
targetType: "social_connection",
|
|
1308
|
+
targetId: connectionId,
|
|
1309
|
+
metadata: {
|
|
1310
|
+
provider: result.connection.provider,
|
|
1311
|
+
adapterTool: `${provider}_post_reply`,
|
|
1312
|
+
inReplyToId,
|
|
1313
|
+
postedId: result.postedId,
|
|
1314
|
+
url: result.url,
|
|
1315
|
+
},
|
|
1316
|
+
});
|
|
1317
|
+
return json({
|
|
1318
|
+
provider: result.connection.provider,
|
|
1319
|
+
postedId: result.postedId,
|
|
1320
|
+
url: result.url,
|
|
1321
|
+
});
|
|
1322
|
+
},
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
830
1325
|
}
|
|
831
1326
|
|
|
832
|
-
if (
|
|
1327
|
+
if (can("scheduled_tasks:manage") || can("scheduled_tasks:run")) {
|
|
833
1328
|
server.registerTool(
|
|
834
1329
|
"scheduled_tasks_list",
|
|
835
1330
|
{
|
|
@@ -977,6 +1472,9 @@ export function buildOpenGeniMcpServer(
|
|
|
977
1472
|
sessionAuthorization: deps.sessionAuthorization,
|
|
978
1473
|
authorizationSurface: "first_party_mcp",
|
|
979
1474
|
});
|
|
1475
|
+
if (!scheduledTaskUpdateChangesState(existing, update)) {
|
|
1476
|
+
return json(scheduledTaskReceipt("scheduled_tasks_update", existing, "unchanged", false));
|
|
1477
|
+
}
|
|
980
1478
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, update);
|
|
981
1479
|
await syncUpdatedScheduledTask({
|
|
982
1480
|
db: deps.db,
|
|
@@ -984,7 +1482,7 @@ export function buildOpenGeniMcpServer(
|
|
|
984
1482
|
previous,
|
|
985
1483
|
task,
|
|
986
1484
|
});
|
|
987
|
-
return json(
|
|
1485
|
+
return json(scheduledTaskReceipt("scheduled_tasks_update", task, "updated", true));
|
|
988
1486
|
},
|
|
989
1487
|
);
|
|
990
1488
|
|
|
@@ -996,6 +1494,9 @@ export function buildOpenGeniMcpServer(
|
|
|
996
1494
|
},
|
|
997
1495
|
async ({ id }) => {
|
|
998
1496
|
const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1497
|
+
if (existing.status === "paused") {
|
|
1498
|
+
return json(scheduledTaskReceipt("scheduled_tasks_pause", existing, "unchanged", false));
|
|
1499
|
+
}
|
|
999
1500
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
1000
1501
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
|
|
1001
1502
|
status: "paused",
|
|
@@ -1006,7 +1507,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1006
1507
|
previous,
|
|
1007
1508
|
task,
|
|
1008
1509
|
});
|
|
1009
|
-
return json(
|
|
1510
|
+
return json(scheduledTaskReceipt("scheduled_tasks_pause", task, "updated", true));
|
|
1010
1511
|
},
|
|
1011
1512
|
);
|
|
1012
1513
|
|
|
@@ -1018,6 +1519,9 @@ export function buildOpenGeniMcpServer(
|
|
|
1018
1519
|
},
|
|
1019
1520
|
async ({ id }) => {
|
|
1020
1521
|
const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1522
|
+
if (existing.status === "active") {
|
|
1523
|
+
return json(scheduledTaskReceipt("scheduled_tasks_resume", existing, "unchanged", false));
|
|
1524
|
+
}
|
|
1021
1525
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
1022
1526
|
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
|
|
1023
1527
|
status: "active",
|
|
@@ -1028,7 +1532,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1028
1532
|
previous,
|
|
1029
1533
|
task,
|
|
1030
1534
|
});
|
|
1031
|
-
return json(
|
|
1535
|
+
return json(scheduledTaskReceipt("scheduled_tasks_resume", task, "updated", true));
|
|
1032
1536
|
},
|
|
1033
1537
|
);
|
|
1034
1538
|
|
|
@@ -1044,31 +1548,32 @@ export function buildOpenGeniMcpServer(
|
|
|
1044
1548
|
},
|
|
1045
1549
|
async ({ id, triggerId }) => {
|
|
1046
1550
|
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
|
-
|
|
1551
|
+
if (task.action.kind === "agent_turn") {
|
|
1552
|
+
await validateScheduledTaskTarget({
|
|
1553
|
+
db: deps.db,
|
|
1554
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
1555
|
+
authorizationSurface: "first_party_mcp",
|
|
1556
|
+
grant,
|
|
1557
|
+
targetSessionId: task.targetSessionId,
|
|
1558
|
+
runMode: task.runMode,
|
|
1559
|
+
variableSetId: task.variableSetId,
|
|
1560
|
+
rigId: task.rigId,
|
|
1561
|
+
agentConfig: task.agentConfig,
|
|
1562
|
+
missingTargetStatus: 404,
|
|
1563
|
+
});
|
|
1564
|
+
await requireLimit(deps, {
|
|
1565
|
+
accountId: grant.accountId,
|
|
1566
|
+
workspaceId: grant.workspaceId,
|
|
1567
|
+
action: "agent_run:create",
|
|
1568
|
+
quantity: 1,
|
|
1569
|
+
model: task.agentConfig.model ?? deps.settings.openaiModel,
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1066
1572
|
const triggerToken = scheduledTaskTriggerToken(triggerId);
|
|
1067
|
-
const agentRunUsageIdempotencyKey =
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
);
|
|
1573
|
+
const agentRunUsageIdempotencyKey =
|
|
1574
|
+
task.action.kind === "agent_turn"
|
|
1575
|
+
? manualScheduledTaskTriggerUsageKey(grant.workspaceId, task.id, triggerToken)
|
|
1576
|
+
: `knowledge-source-sync:manual:${grant.workspaceId}:${task.id}:${triggerToken}`;
|
|
1072
1577
|
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
1073
1578
|
await deps.workflowClient.triggerScheduledTask({
|
|
1074
1579
|
task,
|
|
@@ -1077,6 +1582,14 @@ export function buildOpenGeniMcpServer(
|
|
|
1077
1582
|
initiator: { kind: "subject", subjectId: grant.subjectId },
|
|
1078
1583
|
});
|
|
1079
1584
|
try {
|
|
1585
|
+
if (task.action.kind !== "agent_turn") {
|
|
1586
|
+
return json(
|
|
1587
|
+
scheduledTaskReceipt("scheduled_tasks_trigger", task, "triggered", true, {
|
|
1588
|
+
idempotencyStatus: triggerId ? "unknown" : "not_requested",
|
|
1589
|
+
facts: { triggerWorkflowId },
|
|
1590
|
+
}),
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1080
1593
|
await recordWorkspaceUsage(deps, {
|
|
1081
1594
|
accountId: grant.accountId,
|
|
1082
1595
|
workspaceId: grant.workspaceId,
|
|
@@ -1117,6 +1630,17 @@ export function buildOpenGeniMcpServer(
|
|
|
1117
1630
|
},
|
|
1118
1631
|
async ({ id }) => {
|
|
1119
1632
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1633
|
+
if (task.metadata.connectorKind === "atlassian") {
|
|
1634
|
+
await revokeAtlassianScheduleAuthorization(deps, {
|
|
1635
|
+
task,
|
|
1636
|
+
subjectId: grant.subjectId,
|
|
1637
|
+
});
|
|
1638
|
+
} else {
|
|
1639
|
+
await revokeKnowledgeSourceScheduleAuthorization(deps, {
|
|
1640
|
+
task,
|
|
1641
|
+
subjectId: grant.subjectId,
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1120
1644
|
await deps.workflowClient.deleteScheduledTaskSchedule({
|
|
1121
1645
|
temporalScheduleId: task.temporalScheduleId,
|
|
1122
1646
|
});
|
|
@@ -1163,7 +1687,6 @@ export function buildOpenGeniMcpServer(
|
|
|
1163
1687
|
);
|
|
1164
1688
|
}
|
|
1165
1689
|
|
|
1166
|
-
registerToolspaceProxyTools(server, options.toolspace ?? null);
|
|
1167
1690
|
server.ensureToolsListHandler();
|
|
1168
1691
|
|
|
1169
1692
|
return server;
|
|
@@ -1354,98 +1877,385 @@ function registerSlackBotTools(
|
|
|
1354
1877
|
);
|
|
1355
1878
|
|
|
1356
1879
|
server.registerTool(
|
|
1357
|
-
"
|
|
1880
|
+
"slack_bot_delete_message",
|
|
1358
1881
|
{
|
|
1359
1882
|
description:
|
|
1360
|
-
"
|
|
1883
|
+
"Delete a message authored by the workspace-shared OpenGeni bot. Pass the channel ID and exact message timestamp returned by a prior post or channel/thread read. Generate one operationId UUID per intended deletion and reuse it on every retry, including after an unknown outcome. Slack refuses deletion of messages not authored by this bot.",
|
|
1361
1884
|
inputSchema: {
|
|
1362
1885
|
connectionId: z4.string().uuid().optional(),
|
|
1363
1886
|
operationId: z4.string().uuid(),
|
|
1364
|
-
channelId: z4.string().min(1).max(64)
|
|
1365
|
-
|
|
1366
|
-
threadTimestamp: z4.string().min(1).max(64).optional(),
|
|
1367
|
-
text: z4.string().min(1).max(40_000),
|
|
1887
|
+
channelId: z4.string().min(1).max(64),
|
|
1888
|
+
timestamp: z4.string().min(1).max(64),
|
|
1368
1889
|
},
|
|
1369
1890
|
},
|
|
1370
|
-
async ({ connectionId, operationId, channelId,
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1891
|
+
async ({ connectionId, operationId, channelId, timestamp }) =>
|
|
1892
|
+
json(
|
|
1893
|
+
await (await clientFor(connectionId)).deleteMessage({ operationId, channelId, timestamp }),
|
|
1894
|
+
),
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function registerFikenTools(
|
|
1899
|
+
server: McpServer,
|
|
1900
|
+
deps: ApiRouteDeps,
|
|
1901
|
+
grant: AccessGrant,
|
|
1902
|
+
sessionId: string | null,
|
|
1903
|
+
json: JsonResult,
|
|
1904
|
+
): void {
|
|
1905
|
+
// One resolution per requested connection per MCP server instance: the
|
|
1906
|
+
// bound row cannot change mid-request, and re-resolving on every tool call
|
|
1907
|
+
// would pay an extra connections read each time.
|
|
1908
|
+
const clients = new Map<string, Promise<ReturnType<typeof createFikenClient>>>();
|
|
1909
|
+
const clientFor = (connectionId?: string) => {
|
|
1910
|
+
const cacheKey = connectionId ?? "";
|
|
1911
|
+
let client = clients.get(cacheKey);
|
|
1912
|
+
if (!client) {
|
|
1913
|
+
client = resolveFikenConnectionForTool({
|
|
1914
|
+
db: deps.db,
|
|
1915
|
+
grant,
|
|
1916
|
+
sessionId,
|
|
1917
|
+
...(connectionId ? { requestedConnectionId: connectionId } : {}),
|
|
1918
|
+
}).then((resolved) => createFikenClient(deps, resolved));
|
|
1919
|
+
// A failed resolution must not be cached as a poisoned entry.
|
|
1920
|
+
client.catch(() => clients.delete(cacheKey));
|
|
1921
|
+
clients.set(cacheKey, client);
|
|
1922
|
+
}
|
|
1923
|
+
return client;
|
|
1924
|
+
};
|
|
1925
|
+
const companySlugInput = z4
|
|
1926
|
+
.string()
|
|
1927
|
+
.min(1)
|
|
1928
|
+
.max(128)
|
|
1929
|
+
.optional()
|
|
1930
|
+
.describe(
|
|
1931
|
+
"Fiken company slug. Optional when the connection has a default company or access to exactly one company.",
|
|
1932
|
+
);
|
|
1933
|
+
const pageInputs = {
|
|
1934
|
+
page: z4.number().int().min(0).optional(),
|
|
1935
|
+
pageSize: z4.number().int().min(1).max(100).optional(),
|
|
1936
|
+
};
|
|
1937
|
+
const isoDate = z4
|
|
1938
|
+
.string()
|
|
1939
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
1940
|
+
.optional();
|
|
1941
|
+
|
|
1942
|
+
server.registerTool(
|
|
1943
|
+
"fiken_companies_list",
|
|
1944
|
+
{
|
|
1945
|
+
description:
|
|
1946
|
+
"List the Fiken companies this workspace's Fiken connection can act on. Use the returned slug as companySlug in other fiken tools.",
|
|
1947
|
+
inputSchema: {
|
|
1948
|
+
connectionId: z4.string().uuid().optional(),
|
|
1949
|
+
...pageInputs,
|
|
1950
|
+
},
|
|
1951
|
+
},
|
|
1952
|
+
async ({ connectionId, page, pageSize }) =>
|
|
1953
|
+
json(
|
|
1375
1954
|
await (
|
|
1376
1955
|
await clientFor(connectionId)
|
|
1377
|
-
).
|
|
1378
|
-
|
|
1379
|
-
...(
|
|
1380
|
-
...(userId ? { userId } : {}),
|
|
1381
|
-
...(threadTimestamp ? { threadTimestamp } : {}),
|
|
1382
|
-
text,
|
|
1956
|
+
).listCompanies({
|
|
1957
|
+
...(page !== undefined ? { page } : {}),
|
|
1958
|
+
...(pageSize !== undefined ? { pageSize } : {}),
|
|
1383
1959
|
}),
|
|
1384
|
-
)
|
|
1960
|
+
),
|
|
1961
|
+
);
|
|
1962
|
+
|
|
1963
|
+
server.registerTool(
|
|
1964
|
+
"fiken_contacts_list",
|
|
1965
|
+
{
|
|
1966
|
+
description:
|
|
1967
|
+
"List or search contacts (customers and suppliers) in a Fiken company. Filters combine with AND. customerId used elsewhere equals a contact's contactId where customer is true.",
|
|
1968
|
+
inputSchema: {
|
|
1969
|
+
connectionId: z4.string().uuid().optional(),
|
|
1970
|
+
companySlug: companySlugInput,
|
|
1971
|
+
...pageInputs,
|
|
1972
|
+
name: z4.string().min(1).max(256).optional(),
|
|
1973
|
+
email: z4.string().min(1).max(256).optional(),
|
|
1974
|
+
organizationNumber: z4.string().min(1).max(64).optional(),
|
|
1975
|
+
customer: z4.boolean().optional(),
|
|
1976
|
+
supplier: z4.boolean().optional(),
|
|
1977
|
+
inactive: z4.boolean().optional(),
|
|
1978
|
+
},
|
|
1385
1979
|
},
|
|
1980
|
+
async ({ connectionId, ...input }) =>
|
|
1981
|
+
json(await (await clientFor(connectionId)).listContacts(input)),
|
|
1386
1982
|
);
|
|
1387
1983
|
|
|
1388
1984
|
server.registerTool(
|
|
1389
|
-
"
|
|
1985
|
+
"fiken_contact_create",
|
|
1390
1986
|
{
|
|
1391
1987
|
description:
|
|
1392
|
-
"
|
|
1988
|
+
"Create a contact in a Fiken company. Retrying after an unknown outcome may create a duplicate; search fiken_contacts_list first when unsure.",
|
|
1989
|
+
inputSchema: {
|
|
1990
|
+
connectionId: z4.string().uuid().optional(),
|
|
1991
|
+
companySlug: companySlugInput,
|
|
1992
|
+
name: z4.string().min(1).max(256),
|
|
1993
|
+
email: z4.string().min(1).max(256).optional(),
|
|
1994
|
+
organizationNumber: z4.string().min(1).max(64).optional(),
|
|
1995
|
+
phoneNumber: z4.string().min(1).max(64).optional(),
|
|
1996
|
+
customer: z4.boolean().optional(),
|
|
1997
|
+
supplier: z4.boolean().optional(),
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
async ({ connectionId, ...input }) =>
|
|
2001
|
+
json(await (await clientFor(connectionId)).createContact(input)),
|
|
2002
|
+
);
|
|
2003
|
+
|
|
2004
|
+
server.registerTool(
|
|
2005
|
+
"fiken_products_list",
|
|
2006
|
+
{
|
|
2007
|
+
description: "List products in a Fiken company.",
|
|
2008
|
+
inputSchema: {
|
|
2009
|
+
connectionId: z4.string().uuid().optional(),
|
|
2010
|
+
companySlug: companySlugInput,
|
|
2011
|
+
...pageInputs,
|
|
2012
|
+
name: z4.string().min(1).max(256).optional(),
|
|
2013
|
+
active: z4.boolean().optional(),
|
|
2014
|
+
},
|
|
2015
|
+
},
|
|
2016
|
+
async ({ connectionId, ...input }) =>
|
|
2017
|
+
json(await (await clientFor(connectionId)).listProducts(input)),
|
|
2018
|
+
);
|
|
2019
|
+
|
|
2020
|
+
server.registerTool(
|
|
2021
|
+
"fiken_invoices_list",
|
|
2022
|
+
{
|
|
2023
|
+
description:
|
|
2024
|
+
"List invoices in a Fiken company. Dates are yyyy-mm-dd; monetary amounts in results are in cents/øre of the invoice currency.",
|
|
2025
|
+
inputSchema: {
|
|
2026
|
+
connectionId: z4.string().uuid().optional(),
|
|
2027
|
+
companySlug: companySlugInput,
|
|
2028
|
+
...pageInputs,
|
|
2029
|
+
issueDateGe: isoDate,
|
|
2030
|
+
issueDateLe: isoDate,
|
|
2031
|
+
customerId: z4.number().int().positive().optional(),
|
|
2032
|
+
settled: z4.boolean().optional(),
|
|
2033
|
+
invoiceNumber: z4.string().min(1).max(64).optional(),
|
|
2034
|
+
},
|
|
2035
|
+
},
|
|
2036
|
+
async ({ connectionId, ...input }) =>
|
|
2037
|
+
json(await (await clientFor(connectionId)).listInvoices(input)),
|
|
2038
|
+
);
|
|
2039
|
+
|
|
2040
|
+
server.registerTool(
|
|
2041
|
+
"fiken_invoice_get",
|
|
2042
|
+
{
|
|
2043
|
+
description: "Read a single Fiken invoice by its invoiceId.",
|
|
1393
2044
|
inputSchema: {
|
|
1394
2045
|
connectionId: z4.string().uuid().optional(),
|
|
2046
|
+
companySlug: companySlugInput,
|
|
2047
|
+
invoiceId: z4.number().int().positive(),
|
|
2048
|
+
},
|
|
2049
|
+
},
|
|
2050
|
+
async ({ connectionId, ...input }) =>
|
|
2051
|
+
json(await (await clientFor(connectionId)).getInvoice(input)),
|
|
2052
|
+
);
|
|
2053
|
+
|
|
2054
|
+
server.registerTool(
|
|
2055
|
+
"fiken_invoice_draft_create",
|
|
2056
|
+
{
|
|
2057
|
+
description:
|
|
2058
|
+
"Create an invoice DRAFT in Fiken. Drafts are not sent to the customer; a human finishes and sends them from Fiken. unitPriceCents is the net price per unit in cents/øre. Each line needs either productId or an incomeAccount + vatType. Generate one operationId UUID per intended draft and reuse that same UUID on every retry; a retry returns the existing draft instead of duplicating it.",
|
|
2059
|
+
inputSchema: {
|
|
2060
|
+
connectionId: z4.string().uuid().optional(),
|
|
2061
|
+
companySlug: companySlugInput,
|
|
1395
2062
|
operationId: z4.string().uuid(),
|
|
1396
|
-
|
|
1397
|
-
|
|
2063
|
+
customerId: z4.number().int().positive(),
|
|
2064
|
+
daysUntilDueDate: z4.number().int().min(0).max(365),
|
|
2065
|
+
invoiceText: z4.string().max(500).optional(),
|
|
2066
|
+
yourReference: z4.string().max(128).optional(),
|
|
2067
|
+
ourReference: z4.string().max(128).optional(),
|
|
2068
|
+
currency: z4
|
|
2069
|
+
.string()
|
|
2070
|
+
.regex(/^[A-Z]{3}$/)
|
|
2071
|
+
.optional(),
|
|
2072
|
+
bankAccountNumber: z4.string().min(1).max(64).optional(),
|
|
2073
|
+
lines: z4
|
|
2074
|
+
.array(
|
|
2075
|
+
z4.object({
|
|
2076
|
+
description: z4.string().max(200).optional(),
|
|
2077
|
+
productId: z4.number().int().positive().optional(),
|
|
2078
|
+
unitPriceCents: z4.number().int().optional(),
|
|
2079
|
+
vatType: z4.string().min(1).max(64).optional(),
|
|
2080
|
+
quantity: z4.number().positive(),
|
|
2081
|
+
discountPercent: z4.number().min(0).max(100).optional(),
|
|
2082
|
+
incomeAccount: z4.string().min(1).max(16).optional(),
|
|
2083
|
+
comment: z4.string().max(200).optional(),
|
|
2084
|
+
}),
|
|
2085
|
+
)
|
|
2086
|
+
.min(1)
|
|
2087
|
+
.max(100),
|
|
1398
2088
|
},
|
|
1399
2089
|
},
|
|
1400
|
-
async ({ connectionId,
|
|
1401
|
-
json(
|
|
1402
|
-
|
|
1403
|
-
|
|
2090
|
+
async ({ connectionId, ...input }) =>
|
|
2091
|
+
json(await (await clientFor(connectionId)).createInvoiceDraft(input)),
|
|
2092
|
+
);
|
|
2093
|
+
|
|
2094
|
+
server.registerTool(
|
|
2095
|
+
"fiken_bank_accounts_list",
|
|
2096
|
+
{
|
|
2097
|
+
description:
|
|
2098
|
+
"List bank accounts in a Fiken company, including reconciled balances in cents/øre.",
|
|
2099
|
+
inputSchema: {
|
|
2100
|
+
connectionId: z4.string().uuid().optional(),
|
|
2101
|
+
companySlug: companySlugInput,
|
|
2102
|
+
...pageInputs,
|
|
2103
|
+
inactive: z4.boolean().optional(),
|
|
2104
|
+
},
|
|
2105
|
+
},
|
|
2106
|
+
async ({ connectionId, ...input }) =>
|
|
2107
|
+
json(await (await clientFor(connectionId)).listBankAccounts(input)),
|
|
2108
|
+
);
|
|
2109
|
+
|
|
2110
|
+
server.registerTool(
|
|
2111
|
+
"fiken_purchases_list",
|
|
2112
|
+
{
|
|
2113
|
+
description: "List purchases in a Fiken company. Dates are yyyy-mm-dd.",
|
|
2114
|
+
inputSchema: {
|
|
2115
|
+
connectionId: z4.string().uuid().optional(),
|
|
2116
|
+
companySlug: companySlugInput,
|
|
2117
|
+
...pageInputs,
|
|
2118
|
+
dateGe: isoDate,
|
|
2119
|
+
dateLe: isoDate,
|
|
2120
|
+
paid: z4.boolean().optional(),
|
|
2121
|
+
},
|
|
2122
|
+
},
|
|
2123
|
+
async ({ connectionId, ...input }) =>
|
|
2124
|
+
json(await (await clientFor(connectionId)).listPurchases(input)),
|
|
2125
|
+
);
|
|
2126
|
+
|
|
2127
|
+
server.registerTool(
|
|
2128
|
+
"fiken_sales_list",
|
|
2129
|
+
{
|
|
2130
|
+
description: "List sales in a Fiken company. Dates are yyyy-mm-dd.",
|
|
2131
|
+
inputSchema: {
|
|
2132
|
+
connectionId: z4.string().uuid().optional(),
|
|
2133
|
+
companySlug: companySlugInput,
|
|
2134
|
+
...pageInputs,
|
|
2135
|
+
dateGe: isoDate,
|
|
2136
|
+
dateLe: isoDate,
|
|
2137
|
+
settled: z4.boolean().optional(),
|
|
2138
|
+
},
|
|
2139
|
+
},
|
|
2140
|
+
async ({ connectionId, ...input }) =>
|
|
2141
|
+
json(await (await clientFor(connectionId)).listSales(input)),
|
|
1404
2142
|
);
|
|
1405
2143
|
}
|
|
1406
2144
|
|
|
1407
|
-
function
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2145
|
+
function registerAtlassianTools(
|
|
2146
|
+
server: McpServer,
|
|
2147
|
+
deps: ApiRouteDeps,
|
|
2148
|
+
grant: AccessGrant,
|
|
2149
|
+
json: JsonResult,
|
|
2150
|
+
): void {
|
|
2151
|
+
const connectionFor = async (connectionId?: string) => {
|
|
2152
|
+
const authorized = await authorizedAtlassianConnectionsForGrant({
|
|
2153
|
+
db: deps.db,
|
|
2154
|
+
grant,
|
|
2155
|
+
});
|
|
2156
|
+
const candidates = authorized.filter(({ connection }) =>
|
|
2157
|
+
connectionId ? connection.id === connectionId : true,
|
|
2158
|
+
);
|
|
2159
|
+
if (candidates.length === 0) {
|
|
2160
|
+
throw new Error(
|
|
2161
|
+
connectionId
|
|
2162
|
+
? "the requested Atlassian connection is unavailable for this turn"
|
|
2163
|
+
: "no Atlassian connection is available for this turn",
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
if (!connectionId && candidates.length > 1) {
|
|
2167
|
+
throw new Error(
|
|
2168
|
+
"connectionId is required because multiple Atlassian connections are available",
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
const authority = candidates[0]!;
|
|
2172
|
+
const metadata = AtlassianConnectionMetadata.safeParse(authority.connection.metadata);
|
|
2173
|
+
if (!metadata.success) throw new Error("Atlassian connection metadata is invalid");
|
|
2174
|
+
return {
|
|
2175
|
+
connection: authority.connection,
|
|
2176
|
+
metadata: metadata.data,
|
|
2177
|
+
subjectId: authority.subjectId ?? grant.subjectId,
|
|
2178
|
+
};
|
|
2179
|
+
};
|
|
2180
|
+
|
|
2181
|
+
server.registerTool(
|
|
2182
|
+
"atlassian_sources_list",
|
|
2183
|
+
{
|
|
2184
|
+
description:
|
|
2185
|
+
"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.",
|
|
2186
|
+
inputSchema: { connectionId: z4.string().uuid().optional() },
|
|
2187
|
+
},
|
|
2188
|
+
async ({ connectionId }) => {
|
|
2189
|
+
const authority = await connectionFor(connectionId);
|
|
2190
|
+
const response = await browseAtlassianSources(deps, {
|
|
2191
|
+
workspaceId: grant.workspaceId,
|
|
2192
|
+
subjectId: authority.subjectId,
|
|
2193
|
+
connectionId: authority.connection.id,
|
|
2194
|
+
});
|
|
2195
|
+
const selected = new Set(authority.metadata.selectedSources.map((source) => source.id));
|
|
2196
|
+
return json({
|
|
2197
|
+
connectionId: authority.connection.id,
|
|
2198
|
+
account: authority.metadata.displayName,
|
|
2199
|
+
items: response.items.map((item) => ({
|
|
2200
|
+
...item,
|
|
2201
|
+
selected: selected.has(item.id),
|
|
2202
|
+
})),
|
|
2203
|
+
});
|
|
2204
|
+
},
|
|
2205
|
+
);
|
|
2206
|
+
|
|
2207
|
+
server.registerTool(
|
|
2208
|
+
"atlassian_search",
|
|
2209
|
+
{
|
|
2210
|
+
description:
|
|
2211
|
+
"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.",
|
|
2212
|
+
inputSchema: {
|
|
2213
|
+
connectionId: z4.string().uuid().optional(),
|
|
2214
|
+
query: z4.string().min(1).max(500),
|
|
2215
|
+
product: z4.enum(["jira", "confluence"]).optional(),
|
|
2216
|
+
limit: z4.number().int().min(1).max(50).optional(),
|
|
2217
|
+
},
|
|
2218
|
+
},
|
|
2219
|
+
async ({ connectionId, query, product, limit }) => {
|
|
2220
|
+
const authority = await connectionFor(connectionId);
|
|
2221
|
+
return json({
|
|
2222
|
+
connectionId: authority.connection.id,
|
|
2223
|
+
results: await searchAtlassianLive(deps, {
|
|
2224
|
+
workspaceId: grant.workspaceId,
|
|
2225
|
+
subjectId: authority.subjectId,
|
|
2226
|
+
connectionId: authority.connection.id,
|
|
2227
|
+
query,
|
|
2228
|
+
...(product ? { product } : {}),
|
|
2229
|
+
limit: limit ?? 20,
|
|
1426
2230
|
}),
|
|
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
|
-
},
|
|
2231
|
+
});
|
|
2232
|
+
},
|
|
2233
|
+
);
|
|
2234
|
+
|
|
2235
|
+
server.registerTool(
|
|
2236
|
+
"atlassian_get",
|
|
2237
|
+
{
|
|
2238
|
+
description:
|
|
2239
|
+
"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.",
|
|
2240
|
+
inputSchema: {
|
|
2241
|
+
connectionId: z4.string().uuid().optional(),
|
|
2242
|
+
kind: z4.enum(["jira_issue", "confluence_page"]),
|
|
2243
|
+
id: z4.string().min(1).max(256),
|
|
1445
2244
|
},
|
|
1446
|
-
|
|
1447
|
-
)
|
|
1448
|
-
|
|
2245
|
+
},
|
|
2246
|
+
async ({ connectionId, kind, id }) => {
|
|
2247
|
+
const authority = await connectionFor(connectionId);
|
|
2248
|
+
return json(
|
|
2249
|
+
await getAtlassianLiveItem(deps, {
|
|
2250
|
+
workspaceId: grant.workspaceId,
|
|
2251
|
+
subjectId: authority.subjectId,
|
|
2252
|
+
connectionId: authority.connection.id,
|
|
2253
|
+
kind,
|
|
2254
|
+
id,
|
|
2255
|
+
}),
|
|
2256
|
+
);
|
|
2257
|
+
},
|
|
2258
|
+
);
|
|
1449
2259
|
}
|
|
1450
2260
|
|
|
1451
2261
|
/** Only a prompt explicitly supplied through the human/API channel may redirect a user-paused goal. */
|
|
@@ -1500,26 +2310,49 @@ function registerGoalTools(
|
|
|
1500
2310
|
sessionId: string,
|
|
1501
2311
|
json: (value: unknown) => { content: Array<{ type: "text"; text: string }> },
|
|
1502
2312
|
): void {
|
|
2313
|
+
const boundedGoalToolString = (maxBytes: number, field: string) =>
|
|
2314
|
+
z4
|
|
2315
|
+
.string()
|
|
2316
|
+
.min(1)
|
|
2317
|
+
.refine((value) => sessionGoalUtf8Bytes(value) <= maxBytes, {
|
|
2318
|
+
message: `${field} exceeds ${maxBytes} UTF-8 bytes`,
|
|
2319
|
+
});
|
|
2320
|
+
const goalText = boundedGoalToolString(SESSION_GOAL_TEXT_MAX_BYTES, "goal text");
|
|
2321
|
+
const successCriteriaSchema = boundedGoalToolString(
|
|
2322
|
+
SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
|
|
2323
|
+
"goal success criteria",
|
|
2324
|
+
);
|
|
2325
|
+
const goalRationale = boundedGoalToolString(SESSION_GOAL_RATIONALE_MAX_BYTES, "goal rationale");
|
|
2326
|
+
const progressNoteSchema = boundedGoalToolString(
|
|
2327
|
+
SESSION_GOAL_PROGRESS_MAX_BYTES,
|
|
2328
|
+
"goal progress note",
|
|
2329
|
+
);
|
|
1503
2330
|
server.registerTool(
|
|
1504
2331
|
"goal_set",
|
|
1505
2332
|
{
|
|
1506
2333
|
description:
|
|
1507
|
-
"
|
|
2334
|
+
"Create a goal when this session has none. While active, idle moments synthesize continuation turns until goal_complete or goal_pause. To change an existing goal, use goal_update with its objective revision, a change kind, and rationale.",
|
|
1508
2335
|
inputSchema: {
|
|
1509
|
-
text:
|
|
1510
|
-
successCriteria:
|
|
2336
|
+
text: goalText,
|
|
2337
|
+
successCriteria: successCriteriaSchema.optional(),
|
|
1511
2338
|
maxAutoContinuations: z4.number().int().positive().optional(),
|
|
1512
2339
|
},
|
|
1513
2340
|
},
|
|
1514
2341
|
async ({ text, successCriteria, maxAutoContinuations }) => {
|
|
1515
2342
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
1516
2343
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
2344
|
+
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
2345
|
+
if (existing) {
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
`this session already has a goal at objective revision ${existing.objectiveRevision}; use goal_update to revise it`,
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
1517
2350
|
const callerTurnId =
|
|
1518
2351
|
typeof grant.metadata?.["turnId"] === "string"
|
|
1519
2352
|
? (grant.metadata["turnId"] as string)
|
|
1520
2353
|
: null;
|
|
1521
2354
|
await assertGoalReactivationAllowed(deps, grant.workspaceId, sessionId, callerTurnId);
|
|
1522
|
-
const { goal, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
2355
|
+
const { goal, replaced, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
1523
2356
|
accountId: grant.accountId,
|
|
1524
2357
|
workspaceId: grant.workspaceId,
|
|
1525
2358
|
sessionId,
|
|
@@ -1532,7 +2365,24 @@ function registerGoalTools(
|
|
|
1532
2365
|
if (events.length > 0) {
|
|
1533
2366
|
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
1534
2367
|
}
|
|
1535
|
-
return json(
|
|
2368
|
+
return json(
|
|
2369
|
+
mcpMutationReceipt({
|
|
2370
|
+
operation: "goal_set",
|
|
2371
|
+
committed: true,
|
|
2372
|
+
outcome: replaced ? "updated" : "created",
|
|
2373
|
+
changed: true,
|
|
2374
|
+
resource: {
|
|
2375
|
+
type: "session_goal",
|
|
2376
|
+
id: goal.id,
|
|
2377
|
+
version: goal.version,
|
|
2378
|
+
state: goal.status,
|
|
2379
|
+
},
|
|
2380
|
+
timestamp: goal.updatedAt,
|
|
2381
|
+
idempotency: { status: "not_supported" },
|
|
2382
|
+
facts: { replaced },
|
|
2383
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
2384
|
+
}),
|
|
2385
|
+
);
|
|
1536
2386
|
},
|
|
1537
2387
|
);
|
|
1538
2388
|
|
|
@@ -1540,26 +2390,100 @@ function registerGoalTools(
|
|
|
1540
2390
|
"goal_update",
|
|
1541
2391
|
{
|
|
1542
2392
|
description:
|
|
1543
|
-
"
|
|
2393
|
+
"Propose or apply a semantic goal revision under the session's mutation policy. Retain the standing goal unless explicit user direction or meaningful new evidence justifies the declared refinement, adaptation, or replacement. A rewrite never counts as execution progress; use goal_progress for that.",
|
|
1544
2394
|
inputSchema: {
|
|
1545
|
-
text:
|
|
1546
|
-
successCriteria:
|
|
1547
|
-
|
|
2395
|
+
text: goalText.optional(),
|
|
2396
|
+
successCriteria: successCriteriaSchema.nullable().optional(),
|
|
2397
|
+
// Optional for rolling compatibility with the former goal_update
|
|
2398
|
+
// surface. Omitted semantic metadata is classified as a refinement of
|
|
2399
|
+
// the currently fenced objective; new callers should always supply it.
|
|
2400
|
+
changeKind: z4.enum(["refinement", "adaptation", "replacement"]).optional(),
|
|
2401
|
+
rationale: goalRationale.optional(),
|
|
2402
|
+
expectedObjectiveRevision: z4.number().int().positive().optional(),
|
|
2403
|
+
// Deprecated compatibility input. It is committed through the new
|
|
2404
|
+
// progress operation and never makes a semantic rewrite count itself.
|
|
2405
|
+
progressNote: progressNoteSchema.optional(),
|
|
1548
2406
|
idempotencyKey: z4.string().uuid(),
|
|
1549
2407
|
},
|
|
1550
2408
|
},
|
|
1551
|
-
async ({
|
|
2409
|
+
async ({
|
|
2410
|
+
text,
|
|
2411
|
+
successCriteria,
|
|
2412
|
+
changeKind,
|
|
2413
|
+
rationale,
|
|
2414
|
+
expectedObjectiveRevision,
|
|
2415
|
+
progressNote,
|
|
2416
|
+
idempotencyKey,
|
|
2417
|
+
}) => {
|
|
1552
2418
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
2419
|
+
if (text === undefined && successCriteria === undefined && progressNote === undefined) {
|
|
2420
|
+
throw new Error("goal_update requires semantic content or a progressNote");
|
|
2421
|
+
}
|
|
1553
2422
|
const context = exactAgentCommandContext(grant, sessionId);
|
|
1554
|
-
const
|
|
2423
|
+
const command = {
|
|
2424
|
+
accountId: grant.accountId,
|
|
2425
|
+
actor: {
|
|
2426
|
+
type: "agent_attempt" as const,
|
|
2427
|
+
attemptId: context.callerAttemptId,
|
|
2428
|
+
sessionId: context.callerSessionId,
|
|
2429
|
+
turnId: context.callerTurnId,
|
|
2430
|
+
executionGeneration: context.callerExecutionGeneration,
|
|
2431
|
+
},
|
|
2432
|
+
operationKey: idempotencyKey,
|
|
2433
|
+
};
|
|
2434
|
+
const semantic =
|
|
2435
|
+
text !== undefined || successCriteria !== undefined
|
|
2436
|
+
? await updateSessionGoalWithEvent(deps.db, grant.workspaceId, sessionId, {
|
|
2437
|
+
...(text !== undefined ? { text } : {}),
|
|
2438
|
+
...(successCriteria !== undefined ? { successCriteria } : {}),
|
|
2439
|
+
changeKind: changeKind ?? "refinement",
|
|
2440
|
+
rationale: rationale ?? "Compatibility refinement from goal_update",
|
|
2441
|
+
...(expectedObjectiveRevision !== undefined ? { expectedObjectiveRevision } : {}),
|
|
2442
|
+
actor: "agent",
|
|
2443
|
+
command,
|
|
2444
|
+
})
|
|
2445
|
+
: null;
|
|
2446
|
+
const progress = progressNote
|
|
2447
|
+
? await recordSessionGoalProgressWithEvent(deps.db, grant.workspaceId, sessionId, {
|
|
2448
|
+
progressNote,
|
|
2449
|
+
command,
|
|
2450
|
+
})
|
|
2451
|
+
: null;
|
|
2452
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, [
|
|
2453
|
+
...(semantic?.events ?? []),
|
|
2454
|
+
...(progress?.events ?? []),
|
|
2455
|
+
]);
|
|
2456
|
+
const goal = progress?.goal ?? semantic?.goal;
|
|
2457
|
+
if (!goal) throw new Error("goal_update produced no mutation");
|
|
2458
|
+
return json({
|
|
2459
|
+
...goal,
|
|
2460
|
+
operationId: semantic?.operationId ?? progress?.operationId ?? null,
|
|
2461
|
+
replay: Boolean(semantic?.replay || progress?.replay),
|
|
2462
|
+
outcome: semantic?.outcome ?? "progress_recorded",
|
|
2463
|
+
proposalId: semantic?.proposalId ?? null,
|
|
2464
|
+
});
|
|
2465
|
+
},
|
|
2466
|
+
);
|
|
2467
|
+
|
|
2468
|
+
server.registerTool(
|
|
2469
|
+
"goal_progress",
|
|
2470
|
+
{
|
|
2471
|
+
description:
|
|
2472
|
+
"Record concrete progress toward the unchanged active goal. This does not change goal text, success criteria, mutation policy, or objective revision. Do not use it merely to keep the continuation loop alive.",
|
|
2473
|
+
inputSchema: {
|
|
2474
|
+
progressNote: progressNoteSchema,
|
|
2475
|
+
idempotencyKey: z4.string().uuid(),
|
|
2476
|
+
},
|
|
2477
|
+
},
|
|
2478
|
+
async ({ progressNote, idempotencyKey }) => {
|
|
2479
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
2480
|
+
const context = exactAgentCommandContext(grant, sessionId);
|
|
2481
|
+
const { goal, events, operationId, replay } = await recordSessionGoalProgressWithEvent(
|
|
1555
2482
|
deps.db,
|
|
1556
2483
|
grant.workspaceId,
|
|
1557
2484
|
sessionId,
|
|
1558
2485
|
{
|
|
1559
|
-
|
|
1560
|
-
...(successCriteria !== undefined ? { successCriteria } : {}),
|
|
1561
|
-
...(progressNote !== undefined ? { progressNote } : {}),
|
|
1562
|
-
actor: "agent",
|
|
2486
|
+
progressNote,
|
|
1563
2487
|
command: {
|
|
1564
2488
|
accountId: grant.accountId,
|
|
1565
2489
|
actor: {
|
|
@@ -1631,7 +2555,7 @@ function registerGoalTools(
|
|
|
1631
2555
|
{
|
|
1632
2556
|
description:
|
|
1633
2557
|
"Pause the session goal with a rationale (blocked, not productive, needs human input). No further continuation turns are synthesized until the goal is resumed or replaced.",
|
|
1634
|
-
inputSchema: { rationale:
|
|
2558
|
+
inputSchema: { rationale: goalRationale },
|
|
1635
2559
|
},
|
|
1636
2560
|
async ({ rationale }) => {
|
|
1637
2561
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
@@ -1706,7 +2630,7 @@ function registerWorkspaceArtifactTools(
|
|
|
1706
2630
|
json: JsonResult,
|
|
1707
2631
|
): void {
|
|
1708
2632
|
const attempt = () => {
|
|
1709
|
-
const claims =
|
|
2633
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
1710
2634
|
if (!claims) throw new Error("Exact signed artifact attempt authority is required.");
|
|
1711
2635
|
return claims;
|
|
1712
2636
|
};
|
|
@@ -1806,7 +2730,7 @@ function registerWorkspaceArtifactTools(
|
|
|
1806
2730
|
"artifacts_create",
|
|
1807
2731
|
{
|
|
1808
2732
|
description:
|
|
1809
|
-
"Create and publish a generic
|
|
2733
|
+
"Create and publish a generic workspace artifact from a complete HTML document. The exact HTML runs in an opaque-origin sandboxed iframe with JavaScript, external resources, forms, popups, and downloads enabled, but without parent-origin authority or top-level navigation.",
|
|
1810
2734
|
inputSchema: {
|
|
1811
2735
|
title: z4.string().min(1).max(120),
|
|
1812
2736
|
description: z4.string().max(2000).nullable().optional(),
|
|
@@ -1841,7 +2765,7 @@ function registerWorkspaceArtifactTools(
|
|
|
1841
2765
|
"artifacts_publish",
|
|
1842
2766
|
{
|
|
1843
2767
|
description:
|
|
1844
|
-
"Publish a new immutable
|
|
2768
|
+
"Publish a new immutable HTML version. The exact HTML runs in an opaque-origin sandboxed iframe. First read the current source and pass its version id for optimistic concurrency.",
|
|
1845
2769
|
inputSchema: {
|
|
1846
2770
|
artifactId: z4.string().uuid(),
|
|
1847
2771
|
expectedCurrentVersionId: z4.string().uuid(),
|
|
@@ -1894,12 +2818,13 @@ function registerWorkspaceArtifactTools(
|
|
|
1894
2818
|
);
|
|
1895
2819
|
}
|
|
1896
2820
|
|
|
1897
|
-
function
|
|
2821
|
+
function exactAgentAttemptClaims(grant: AccessGrant): {
|
|
1898
2822
|
sessionId: string;
|
|
1899
2823
|
turnId: string;
|
|
1900
2824
|
attemptId: string;
|
|
1901
2825
|
executionGeneration: number;
|
|
1902
2826
|
} | null {
|
|
2827
|
+
if (grant.principalKind !== "agent_attempt") return null;
|
|
1903
2828
|
const metadata = grant.metadata ?? {};
|
|
1904
2829
|
if (
|
|
1905
2830
|
typeof metadata["sessionId"] !== "string" ||
|
|
@@ -1920,6 +2845,109 @@ function preferenceAttemptClaims(grant: AccessGrant): {
|
|
|
1920
2845
|
};
|
|
1921
2846
|
}
|
|
1922
2847
|
|
|
2848
|
+
function registerTaskNoteTools(
|
|
2849
|
+
server: McpServer,
|
|
2850
|
+
deps: ApiRouteDeps,
|
|
2851
|
+
grant: AccessGrant,
|
|
2852
|
+
sessionId: string,
|
|
2853
|
+
json: JsonResult,
|
|
2854
|
+
): void {
|
|
2855
|
+
const attemptClaims = () => {
|
|
2856
|
+
const resolved = exactAgentAttemptClaims(grant);
|
|
2857
|
+
if (!resolved || resolved.sessionId !== sessionId) {
|
|
2858
|
+
throw new Error("Exact signed task-note attempt authority is required.");
|
|
2859
|
+
}
|
|
2860
|
+
return { accountId: grant.accountId, workspaceId: grant.workspaceId, ...resolved };
|
|
2861
|
+
};
|
|
2862
|
+
const authorize = async () => {
|
|
2863
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
2864
|
+
};
|
|
2865
|
+
const boundedUtf8 = (maxBytes: number, label: string) =>
|
|
2866
|
+
z4
|
|
2867
|
+
.string()
|
|
2868
|
+
.min(1)
|
|
2869
|
+
.refine((value) => value === value.trim(), {
|
|
2870
|
+
message: `${label} must not have leading or trailing whitespace`,
|
|
2871
|
+
})
|
|
2872
|
+
.refine((value) => new TextEncoder().encode(value).byteLength <= maxBytes, {
|
|
2873
|
+
message: `${label} must be at most ${maxBytes} UTF-8 bytes`,
|
|
2874
|
+
});
|
|
2875
|
+
|
|
2876
|
+
server.registerTool(
|
|
2877
|
+
"task_notes_list",
|
|
2878
|
+
{
|
|
2879
|
+
description:
|
|
2880
|
+
"Explicitly retrieve bounded, unexpired coordination notes shared by this session's root task tree. Notes are non-authoritative and are never injected into prompts automatically.",
|
|
2881
|
+
inputSchema: {
|
|
2882
|
+
includeArchived: z4.boolean().optional().default(false),
|
|
2883
|
+
limit: z4
|
|
2884
|
+
.number()
|
|
2885
|
+
.int()
|
|
2886
|
+
.min(1)
|
|
2887
|
+
.max(TASK_NOTE_LIST_MAX_LIMIT)
|
|
2888
|
+
.optional()
|
|
2889
|
+
.default(TASK_NOTE_LIST_DEFAULT_LIMIT),
|
|
2890
|
+
},
|
|
2891
|
+
},
|
|
2892
|
+
async ({ includeArchived, limit }) => {
|
|
2893
|
+
await authorize();
|
|
2894
|
+
return json(await listTaskNotes(deps.db, { ...attemptClaims(), includeArchived, limit }));
|
|
2895
|
+
},
|
|
2896
|
+
);
|
|
2897
|
+
|
|
2898
|
+
server.registerTool(
|
|
2899
|
+
"task_note_save",
|
|
2900
|
+
{
|
|
2901
|
+
description:
|
|
2902
|
+
"Save one bounded, expiring, non-authoritative coordination note for agents in this root task tree. Use a fresh operationId; exact retries of the same attempt and input replay safely.",
|
|
2903
|
+
inputSchema: {
|
|
2904
|
+
operationId: z4.string().uuid(),
|
|
2905
|
+
kind: z4.enum(["finding", "decision", "blocker", "ownership", "artifact", "handoff"]),
|
|
2906
|
+
text: boundedUtf8(TASK_NOTE_TEXT_MAX_BYTES, "Task note text"),
|
|
2907
|
+
expiresInDays: z4.number().int().min(1).max(TASK_NOTE_MAX_LIFETIME_DAYS),
|
|
2908
|
+
},
|
|
2909
|
+
},
|
|
2910
|
+
async ({ operationId, kind, text, expiresInDays }) => {
|
|
2911
|
+
await authorize();
|
|
2912
|
+
return json(
|
|
2913
|
+
await createTaskNote(deps.db, {
|
|
2914
|
+
...attemptClaims(),
|
|
2915
|
+
operationId,
|
|
2916
|
+
kind,
|
|
2917
|
+
text,
|
|
2918
|
+
expiresInDays,
|
|
2919
|
+
}),
|
|
2920
|
+
);
|
|
2921
|
+
},
|
|
2922
|
+
);
|
|
2923
|
+
|
|
2924
|
+
server.registerTool(
|
|
2925
|
+
"task_note_archive",
|
|
2926
|
+
{
|
|
2927
|
+
description:
|
|
2928
|
+
"Archive a coordination note from this root task tree with optimistic version fencing. This preserves its immutable create receipt and records a separate archive receipt.",
|
|
2929
|
+
inputSchema: {
|
|
2930
|
+
operationId: z4.string().uuid(),
|
|
2931
|
+
noteId: z4.string().uuid(),
|
|
2932
|
+
expectedVersion: z4.number().int().min(1).max(1),
|
|
2933
|
+
reason: boundedUtf8(TASK_NOTE_REASON_MAX_BYTES, "Task note archive reason"),
|
|
2934
|
+
},
|
|
2935
|
+
},
|
|
2936
|
+
async ({ operationId, noteId, expectedVersion, reason }) => {
|
|
2937
|
+
await authorize();
|
|
2938
|
+
return json(
|
|
2939
|
+
await archiveTaskNote(deps.db, {
|
|
2940
|
+
...attemptClaims(),
|
|
2941
|
+
operationId,
|
|
2942
|
+
noteId,
|
|
2943
|
+
expectedVersion,
|
|
2944
|
+
reason,
|
|
2945
|
+
}),
|
|
2946
|
+
);
|
|
2947
|
+
},
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
1923
2951
|
function registerPreferenceRegistryTools(
|
|
1924
2952
|
server: McpServer,
|
|
1925
2953
|
deps: ApiRouteDeps,
|
|
@@ -1927,7 +2955,7 @@ function registerPreferenceRegistryTools(
|
|
|
1927
2955
|
json: JsonResult,
|
|
1928
2956
|
): void {
|
|
1929
2957
|
const attemptClaims = () => {
|
|
1930
|
-
const resolved =
|
|
2958
|
+
const resolved = exactAgentAttemptClaims(grant);
|
|
1931
2959
|
if (!resolved) throw new Error("Exact signed preference attempt authority is required.");
|
|
1932
2960
|
return {
|
|
1933
2961
|
accountId: grant.accountId,
|
|
@@ -1992,22 +3020,95 @@ function scheduledTaskReceipt(
|
|
|
1992
3020
|
});
|
|
1993
3021
|
}
|
|
1994
3022
|
|
|
3023
|
+
function scheduledTaskUpdateChangesState(
|
|
3024
|
+
task: ScheduledTask,
|
|
3025
|
+
update: Awaited<ReturnType<typeof validatedScheduledTaskUpdate>>,
|
|
3026
|
+
): boolean {
|
|
3027
|
+
if (update.name !== undefined && update.name !== task.name) return true;
|
|
3028
|
+
if (update.status !== undefined && update.status !== task.status) return true;
|
|
3029
|
+
if (update.schedule !== undefined && stableJson(update.schedule) !== stableJson(task.schedule)) {
|
|
3030
|
+
return true;
|
|
3031
|
+
}
|
|
3032
|
+
if (update.runMode !== undefined && update.runMode !== task.runMode) return true;
|
|
3033
|
+
if (update.overlapPolicy !== undefined && update.overlapPolicy !== task.overlapPolicy)
|
|
3034
|
+
return true;
|
|
3035
|
+
if (
|
|
3036
|
+
update.agentConfig !== undefined &&
|
|
3037
|
+
stableJson(update.agentConfig) !== stableJson(task.agentConfig)
|
|
3038
|
+
) {
|
|
3039
|
+
return true;
|
|
3040
|
+
}
|
|
3041
|
+
if (update.targetSessionId !== undefined && update.targetSessionId !== task.targetSessionId) {
|
|
3042
|
+
return true;
|
|
3043
|
+
}
|
|
3044
|
+
if (
|
|
3045
|
+
update.reusableSessionId !== undefined &&
|
|
3046
|
+
update.reusableSessionId !== task.reusableSessionId
|
|
3047
|
+
) {
|
|
3048
|
+
return true;
|
|
3049
|
+
}
|
|
3050
|
+
if (update.variableSetId !== undefined && update.variableSetId !== task.variableSetId)
|
|
3051
|
+
return true;
|
|
3052
|
+
if (update.rigId !== undefined && update.rigId !== task.rigId) return true;
|
|
3053
|
+
if (update.metadata !== undefined && stableJson(update.metadata) !== stableJson(task.metadata)) {
|
|
3054
|
+
return true;
|
|
3055
|
+
}
|
|
3056
|
+
// Personal-connection delegations are recomputed with agentConfig and can
|
|
3057
|
+
// change even when the visible config is byte-identical (for example after a
|
|
3058
|
+
// connection rotation), so preserve that refresh as a real mutation.
|
|
3059
|
+
if (update.personalConnectionDelegations !== undefined) return true;
|
|
3060
|
+
return false;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
1995
3063
|
function memoryPreview(text: string): string {
|
|
1996
3064
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1997
3065
|
return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
|
|
1998
3066
|
}
|
|
1999
3067
|
|
|
3068
|
+
export function memorySlackPublicationActor(
|
|
3069
|
+
actor: Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>,
|
|
3070
|
+
sessionId: string,
|
|
3071
|
+
fallbackOwnerLabel: string | null,
|
|
3072
|
+
) {
|
|
3073
|
+
return {
|
|
3074
|
+
actor: {
|
|
3075
|
+
kind: actor.initiator.kind === "subject" ? ("human" as const) : ("service" as const),
|
|
3076
|
+
subjectId: actor.initiator.subjectId,
|
|
3077
|
+
initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
|
|
3078
|
+
sessionId,
|
|
3079
|
+
turnId: actor.turnId,
|
|
3080
|
+
attemptId: actor.attemptId,
|
|
3081
|
+
},
|
|
3082
|
+
ownerLabel: actor.initiator.label ?? fallbackOwnerLabel,
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
|
|
2000
3086
|
function registerMemoryTools(
|
|
2001
3087
|
server: McpServer,
|
|
2002
3088
|
deps: ApiRouteDeps,
|
|
2003
3089
|
grant: AccessGrant,
|
|
2004
3090
|
sessionId: string,
|
|
2005
3091
|
json: JsonResult,
|
|
3092
|
+
promptMode: WorkspaceMemoryPromptMode,
|
|
2006
3093
|
): void {
|
|
3094
|
+
const publicationActor = async () => {
|
|
3095
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
3096
|
+
return memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null);
|
|
3097
|
+
};
|
|
3098
|
+
const publicationInputSchema = z4.object({
|
|
3099
|
+
importance: z4.enum(["major", "normal", "minor"]),
|
|
3100
|
+
audience: z4.literal("workspace"),
|
|
3101
|
+
slackMode: z4.enum(["auto", "review", "never"]),
|
|
3102
|
+
shareSummary: z4.string().trim().min(1).max(4_096),
|
|
3103
|
+
});
|
|
3104
|
+
|
|
2007
3105
|
server.registerTool(
|
|
2008
3106
|
"memory_search",
|
|
2009
3107
|
{
|
|
2010
|
-
description:
|
|
3108
|
+
description:
|
|
3109
|
+
promptMode === "retrieval_only"
|
|
3110
|
+
? `${MEMORY_SEARCH_TOOL_DESCRIPTION} Legacy preference-kind records are excluded here because structured preferences are the only behavioral authority.`
|
|
3111
|
+
: MEMORY_SEARCH_TOOL_DESCRIPTION,
|
|
2011
3112
|
inputSchema: {
|
|
2012
3113
|
query: z4.string().min(1),
|
|
2013
3114
|
kind: MemoryKindSchema.optional(),
|
|
@@ -2023,6 +3124,7 @@ function registerMemoryTools(
|
|
|
2023
3124
|
query,
|
|
2024
3125
|
...(kind ? { kind } : {}),
|
|
2025
3126
|
...(limit ? { limit } : {}),
|
|
3127
|
+
agentPromptMode: promptMode,
|
|
2026
3128
|
},
|
|
2027
3129
|
deps.getDocumentServices().embedder,
|
|
2028
3130
|
),
|
|
@@ -2032,16 +3134,21 @@ function registerMemoryTools(
|
|
|
2032
3134
|
server.registerTool(
|
|
2033
3135
|
"memory_save",
|
|
2034
3136
|
{
|
|
2035
|
-
description:
|
|
3137
|
+
description:
|
|
3138
|
+
promptMode === "retrieval_only"
|
|
3139
|
+
? `${MEMORY_SAVE_TOOL_DESCRIPTION} A preference-kind save is retained only as a legacy observation and cannot become behavioral authority; use the structured preference proposal surface for behavioral guidance.`
|
|
3140
|
+
: MEMORY_SAVE_TOOL_DESCRIPTION,
|
|
2036
3141
|
inputSchema: {
|
|
2037
3142
|
text: z4.string().min(1),
|
|
2038
3143
|
kind: MemoryKindSchema,
|
|
2039
3144
|
confidence: z4.number().min(0).max(1).optional(),
|
|
2040
3145
|
replaces_id: z4.string().min(1).optional(),
|
|
3146
|
+
slack_publication: publicationInputSchema.optional(),
|
|
2041
3147
|
},
|
|
2042
3148
|
},
|
|
2043
|
-
async ({ text, kind, confidence, replaces_id }) => {
|
|
2044
|
-
const
|
|
3149
|
+
async ({ text, kind, confidence, replaces_id, slack_publication }) => {
|
|
3150
|
+
const principal = slack_publication ? await publicationActor() : null;
|
|
3151
|
+
const result = await saveWorkspaceMemoryWithSlackPublication(
|
|
2045
3152
|
deps.db,
|
|
2046
3153
|
{
|
|
2047
3154
|
accountId: grant.accountId,
|
|
@@ -2053,6 +3160,13 @@ function registerMemoryTools(
|
|
|
2053
3160
|
...(replaces_id ? { replacesId: replaces_id } : {}),
|
|
2054
3161
|
origin: "agent",
|
|
2055
3162
|
},
|
|
3163
|
+
slack_publication
|
|
3164
|
+
? {
|
|
3165
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3166
|
+
actor: principal!.actor,
|
|
3167
|
+
ownerLabel: principal!.ownerLabel,
|
|
3168
|
+
}
|
|
3169
|
+
: null,
|
|
2056
3170
|
deps.getDocumentServices().embedder,
|
|
2057
3171
|
);
|
|
2058
3172
|
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
@@ -2104,6 +3218,19 @@ function registerMemoryTools(
|
|
|
2104
3218
|
dedupeReason: result.dedupeReason,
|
|
2105
3219
|
updatedInPlace: result.updated,
|
|
2106
3220
|
embedded: result.embedded,
|
|
3221
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
3222
|
+
? "eligible"
|
|
3223
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
3224
|
+
slackPublicationId:
|
|
3225
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3226
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3227
|
+
? result.slackPublication.enqueue.publication.id
|
|
3228
|
+
: null,
|
|
3229
|
+
slackPublicationState:
|
|
3230
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3231
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3232
|
+
? result.slackPublication.enqueue.publication.state
|
|
3233
|
+
: null,
|
|
2107
3234
|
},
|
|
2108
3235
|
}),
|
|
2109
3236
|
);
|
|
@@ -2118,10 +3245,12 @@ function registerMemoryTools(
|
|
|
2118
3245
|
id: z4.string().min(1),
|
|
2119
3246
|
reason: z4.string().min(1).optional(),
|
|
2120
3247
|
replacement_text: z4.string().min(1).optional(),
|
|
3248
|
+
slack_publication: publicationInputSchema.optional(),
|
|
2121
3249
|
},
|
|
2122
3250
|
},
|
|
2123
|
-
async ({ id, reason, replacement_text }) => {
|
|
2124
|
-
const
|
|
3251
|
+
async ({ id, reason, replacement_text, slack_publication }) => {
|
|
3252
|
+
const principal = slack_publication ? await publicationActor() : null;
|
|
3253
|
+
const result = await correctWorkspaceMemoryWithSlackPublication(
|
|
2125
3254
|
deps.db,
|
|
2126
3255
|
{
|
|
2127
3256
|
accountId: grant.accountId,
|
|
@@ -2131,6 +3260,13 @@ function registerMemoryTools(
|
|
|
2131
3260
|
...(reason ? { reason } : {}),
|
|
2132
3261
|
...(replacement_text ? { replacementText: replacement_text } : {}),
|
|
2133
3262
|
},
|
|
3263
|
+
slack_publication
|
|
3264
|
+
? {
|
|
3265
|
+
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3266
|
+
actor: principal!.actor,
|
|
3267
|
+
ownerLabel: principal!.ownerLabel,
|
|
3268
|
+
}
|
|
3269
|
+
: null,
|
|
2134
3270
|
deps.getDocumentServices().embedder,
|
|
2135
3271
|
);
|
|
2136
3272
|
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
@@ -2173,7 +3309,22 @@ function registerMemoryTools(
|
|
|
2173
3309
|
: undefined,
|
|
2174
3310
|
timestamp: (result.replacement ?? result.memory).updatedAt,
|
|
2175
3311
|
idempotency: { status: "not_supported" },
|
|
2176
|
-
facts: {
|
|
3312
|
+
facts: {
|
|
3313
|
+
correctionAction: result.action,
|
|
3314
|
+
slackPublicationDecision: result.slackPublication.decision?.eligible
|
|
3315
|
+
? "eligible"
|
|
3316
|
+
: (result.slackPublication.decision?.reason ?? "not_requested"),
|
|
3317
|
+
slackPublicationId:
|
|
3318
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3319
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3320
|
+
? result.slackPublication.enqueue.publication.id
|
|
3321
|
+
: null,
|
|
3322
|
+
slackPublicationState:
|
|
3323
|
+
result.slackPublication.enqueue?.kind === "enqueued" ||
|
|
3324
|
+
result.slackPublication.enqueue?.kind === "replayed"
|
|
3325
|
+
? result.slackPublication.enqueue.publication.state
|
|
3326
|
+
: null,
|
|
3327
|
+
},
|
|
2177
3328
|
}),
|
|
2178
3329
|
);
|
|
2179
3330
|
},
|
|
@@ -2309,7 +3460,7 @@ function registerConnectedMachineTools(
|
|
|
2309
3460
|
"connected_machine_remove",
|
|
2310
3461
|
{
|
|
2311
3462
|
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
|
|
3463
|
+
"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
3464
|
inputSchema: {
|
|
2314
3465
|
enrollmentId: z4.string().uuid(),
|
|
2315
3466
|
expectedUpdatedAt: z4.string().datetime({ offset: true }).optional(),
|
|
@@ -2341,12 +3492,10 @@ async function beginMcpRigVerificationAttempt(
|
|
|
2341
3492
|
try {
|
|
2342
3493
|
return await beginRigChangeVerificationAttempt(deps.db, workspaceId, changeId, {
|
|
2343
3494
|
startedAt: new Date().toISOString(),
|
|
3495
|
+
allowAlreadyVerifying: true,
|
|
2344
3496
|
});
|
|
2345
3497
|
} catch (error) {
|
|
2346
|
-
if (
|
|
2347
|
-
error instanceof RigChangeAlreadyVerifyingError ||
|
|
2348
|
-
error instanceof RigChangeTransitionError
|
|
2349
|
-
) {
|
|
3498
|
+
if (error instanceof RigChangeTransitionError) {
|
|
2350
3499
|
throw new Error(error.message, { cause: error });
|
|
2351
3500
|
}
|
|
2352
3501
|
throw error;
|
|
@@ -2685,7 +3834,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2685
3834
|
grant: AccessGrant,
|
|
2686
3835
|
can: (permission: Permission) => boolean,
|
|
2687
3836
|
callerSessionId: string | null,
|
|
2688
|
-
|
|
3837
|
+
sessionCreateVisible: boolean,
|
|
2689
3838
|
json: JsonResult,
|
|
2690
3839
|
): void {
|
|
2691
3840
|
if (can("sessions:read")) {
|
|
@@ -2763,7 +3912,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2763
3912
|
);
|
|
2764
3913
|
return json(
|
|
2765
3914
|
boundSessionDetailMcp(
|
|
2766
|
-
await withMcpEffectivePolicy(deps, grant.workspaceId, {
|
|
3915
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, grant.subjectId, {
|
|
2767
3916
|
...projected,
|
|
2768
3917
|
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
|
|
2769
3918
|
}),
|
|
@@ -2885,129 +4034,100 @@ function registerWorkspaceOrchestrationTools(
|
|
|
2885
4034
|
);
|
|
2886
4035
|
}
|
|
2887
4036
|
|
|
2888
|
-
if (can("sessions:create")) {
|
|
4037
|
+
if (can("sessions:create") && sessionCreateVisible) {
|
|
4038
|
+
const sessionCreateInput = z4
|
|
4039
|
+
.object({
|
|
4040
|
+
initialMessage: z4.string().min(1),
|
|
4041
|
+
instructions: z4.string().min(1).max(32768).optional(),
|
|
4042
|
+
goal: z4.unknown().optional(),
|
|
4043
|
+
resources: z4.array(z4.unknown()).optional(),
|
|
4044
|
+
tools: z4.array(z4.unknown()).optional(),
|
|
4045
|
+
mcpServers: z4.array(z4.unknown()).optional(),
|
|
4046
|
+
variableSetId: z4.string().uuid().optional(),
|
|
4047
|
+
environmentId: z4.string().uuid().optional(),
|
|
4048
|
+
rigId: z4.string().uuid().optional(),
|
|
4049
|
+
model: z4
|
|
4050
|
+
.string()
|
|
4051
|
+
.min(1)
|
|
4052
|
+
.optional()
|
|
4053
|
+
.describe(
|
|
4054
|
+
"Model for the worker. Omit to inherit the exact calling turn's model, including its Codex subscription billing path.",
|
|
4055
|
+
),
|
|
4056
|
+
reasoningEffort: z4
|
|
4057
|
+
.string()
|
|
4058
|
+
.optional()
|
|
4059
|
+
.describe("Omit to inherit the exact calling turn's reasoning effort."),
|
|
4060
|
+
latencyMode: z4
|
|
4061
|
+
.enum(["standard", "priority", "fast"])
|
|
4062
|
+
.optional()
|
|
4063
|
+
.describe("Omit to inherit the exact calling turn's latency mode."),
|
|
4064
|
+
sandboxBackend: z4.string().optional(),
|
|
4065
|
+
// Model-only structural coupling: workingDir cannot exist without a
|
|
4066
|
+
// targetSandboxId because both live inside one optional object. The
|
|
4067
|
+
// handler maps this back to the stable public REST/SDK request fields.
|
|
4068
|
+
machineTarget: z4
|
|
4069
|
+
.object({
|
|
4070
|
+
targetSandboxId: z4.string().uuid(),
|
|
4071
|
+
workingDir: z4.string().optional(),
|
|
4072
|
+
})
|
|
4073
|
+
.strict()
|
|
4074
|
+
.optional(),
|
|
4075
|
+
metadata: z4.record(z4.string(), z4.unknown()).optional(),
|
|
4076
|
+
idempotencyKey: z4.string().min(1).max(200).optional(),
|
|
4077
|
+
firstPartyMcpPermissions: z4
|
|
4078
|
+
.array(z4.string())
|
|
4079
|
+
.optional()
|
|
4080
|
+
.describe(
|
|
4081
|
+
"Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly.",
|
|
4082
|
+
),
|
|
4083
|
+
firstPartyMcpTools: z4
|
|
4084
|
+
.array(z4.enum(FIRST_PARTY_MCP_TOOL_NAMES))
|
|
4085
|
+
.optional()
|
|
4086
|
+
.describe(
|
|
4087
|
+
"Exact model-visible first-party tool selection for the child. Omit to inherit this session's effective selection. To create a non-delegating leaf, provide a selection that omits session_create. This does not grant permissions.",
|
|
4088
|
+
),
|
|
4089
|
+
// Omission is the ordinary safe sharing path. Literal "shared" remains
|
|
4090
|
+
// available to advanced REST/SDK callers but is intentionally absent
|
|
4091
|
+
// from the model surface because it turns compatibility drift into a
|
|
4092
|
+
// deterministic failure instead of the omission path's safe own-box fallback.
|
|
4093
|
+
sandbox: z4
|
|
4094
|
+
.union([z4.literal("new"), z4.object({ groupId: z4.string().uuid() })])
|
|
4095
|
+
.optional(),
|
|
4096
|
+
})
|
|
4097
|
+
.strict();
|
|
2889
4098
|
server.registerTool(
|
|
2890
4099
|
"session_create",
|
|
2891
4100
|
{
|
|
2892
4101
|
description:
|
|
2893
|
-
"Spawn a new agent session (a worker)
|
|
2894
|
-
inputSchema:
|
|
2895
|
-
initialMessage: z4.string().min(1),
|
|
2896
|
-
// Per-session agent persona/system instructions for the spawned worker
|
|
2897
|
-
// (a per-agent-type prompt). Delivered system-level, composed AFTER the
|
|
2898
|
-
// workspace persona; never shown in the worker's timeline. Trimmed,
|
|
2899
|
-
// non-empty, max 32768 chars (re-validated by the contracts schema).
|
|
2900
|
-
instructions: z4.string().min(1).max(32768).optional(),
|
|
2901
|
-
goal: z4.unknown().optional(),
|
|
2902
|
-
resources: z4.array(z4.unknown()).optional(),
|
|
2903
|
-
tools: z4.array(z4.unknown()).optional(),
|
|
2904
|
-
// Per-session third-party MCP servers. Credential header values are
|
|
2905
|
-
// accepted only at create and never appear in responses/events.
|
|
2906
|
-
mcpServers: z4.array(z4.unknown()).optional(),
|
|
2907
|
-
variableSetId: z4.string().uuid().optional(),
|
|
2908
|
-
// Deprecated alias of variableSetId (rename back-compat); declared so MCP
|
|
2909
|
-
// validation doesn't strip it before createSessionForRequest maps it.
|
|
2910
|
-
environmentId: z4.string().uuid().optional(),
|
|
2911
|
-
// Bind the spawned session to a rig (freezes its active version);
|
|
2912
|
-
// declared so MCP validation doesn't strip it before the domain reads it.
|
|
2913
|
-
rigId: z4.string().uuid().optional(),
|
|
2914
|
-
model: z4.string().min(1).optional(),
|
|
2915
|
-
reasoningEffort: z4.string().optional(),
|
|
2916
|
-
latencyMode: z4.enum(["standard", "priority", "fast"]).optional(),
|
|
2917
|
-
sandboxBackend: z4.string().optional(),
|
|
2918
|
-
// Create-time machine targeting: an enrolled sandbox id (from
|
|
2919
|
-
// sandboxes_list) to run the spawned session on. Seeds the active-sandbox
|
|
2920
|
-
// pointer at creation so the FIRST turn lands on the chosen machine
|
|
2921
|
-
// (race-free). Ownership + liveness are validated in the domain via the
|
|
2922
|
-
// same path as sandbox_swap; an unowned/offline/unknown target 422s.
|
|
2923
|
-
targetSandboxId: z4.string().uuid().optional(),
|
|
2924
|
-
// The working directory (cwd) for a machine target: the path/cwd base the
|
|
2925
|
-
// spawned session's agent exec, terminal, and file dock run under. A
|
|
2926
|
-
// workspace_root-relative subdir or an absolute machine path. Only valid
|
|
2927
|
-
// WITH targetSandboxId (workingDir alone 422s); omitted ⇒ workspace_root.
|
|
2928
|
-
workingDir: z4.string().optional(),
|
|
2929
|
-
metadata: z4.record(z4.string(), z4.unknown()).optional(),
|
|
2930
|
-
// Workspace-scoped CREATE idempotency key: a retried session_create with
|
|
2931
|
-
// the same key returns the already-spawned worker instead of a duplicate.
|
|
2932
|
-
idempotencyKey: z4.string().min(1).max(200).optional(),
|
|
2933
|
-
// Per-session/agent descendant policy. Reductions need only create;
|
|
2934
|
-
// increases are authorized server-side with workspace:admin.
|
|
2935
|
-
maxNestedAgentDepth: z4.number().int().nonnegative().optional(),
|
|
2936
|
-
// First-party MCP token permissions for the spawned session; every
|
|
2937
|
-
// permission must be held by this grant (validated in the domain).
|
|
2938
|
-
// A goal requires goals:manage in the resulting set; it is never
|
|
2939
|
-
// silently added beyond the inherited or explicit authority.
|
|
2940
|
-
firstPartyMcpPermissions: z4
|
|
2941
|
-
.array(z4.string())
|
|
2942
|
-
.optional()
|
|
2943
|
-
.describe(
|
|
2944
|
-
"Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly.",
|
|
2945
|
-
),
|
|
2946
|
-
firstPartyMcpTools: z4
|
|
2947
|
-
.array(z4.enum(FIRST_PARTY_MCP_TOOL_NAMES))
|
|
2948
|
-
.optional()
|
|
2949
|
-
.describe(
|
|
2950
|
-
"Exact model-visible first-party tool selection for the child. Omit to inherit this session's effective selection. This does not grant permissions.",
|
|
2951
|
-
),
|
|
2952
|
-
// Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
|
|
2953
|
-
// creator's box — one filesystem/repo/desktop, N independent conversations;
|
|
2954
|
-
// this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
|
|
2955
|
-
// repo set or a genuinely separate filesystem), or {groupId} (a sibling
|
|
2956
|
-
// session's `sandboxGroupId` from a prior session_create response) to join
|
|
2957
|
-
// that specific sibling's box.
|
|
2958
|
-
// Shared state must be compatible: a shared box requires the SAME image
|
|
2959
|
-
// (rejected at the lease layer, B3) and — because the box's variable set is
|
|
2960
|
-
// fixed at creation under the current mechanics — the SAME workspace
|
|
2961
|
-
// VariableSet. The domain layer is env-aware: an inherited default with a
|
|
2962
|
-
// different variableSetId silently gets its OWN box (the spawn still works),
|
|
2963
|
-
// while an explicit shared/{groupId} with a mismatched variableSet 422s at
|
|
2964
|
-
// create. When the VariableSet is eventually evicted from the box manifest
|
|
2965
|
-
// (per-exec, like the git token), the env check dissolves on its own.
|
|
2966
|
-
// The description below is what the AGENT sees (this comment is invisible to
|
|
2967
|
-
// it); keep the two in sync.
|
|
2968
|
-
sandbox: z4
|
|
2969
|
-
.union([
|
|
2970
|
-
z4.literal("shared"),
|
|
2971
|
-
z4.literal("new"),
|
|
2972
|
-
z4.object({ groupId: z4.string().uuid() }),
|
|
2973
|
-
])
|
|
2974
|
-
.describe(
|
|
2975
|
-
"Sandbox placement. OMIT (default) to SHARE the creator's box — one filesystem/repo/desktop, N independent conversations; this is the safe default. If the new session attaches a DIFFERENT variableSet than the creator's box, the platform automatically gives it its own box (the box variable set is fixed at creation), so omitting stays safe. Pass 'new' for a fresh isolated box (different repo set or a genuinely separate filesystem). Pass {groupId} to join a specific sibling's box — requires the same variableSet (a mismatch is rejected at create) and the same image (a conflicting image is rejected when the box warms).",
|
|
2976
|
-
)
|
|
2977
|
-
.optional(),
|
|
2978
|
-
// The parent (manager) session is auto-inferred from the caller's
|
|
2979
|
-
// worker-signed sessionId claim, so a spawned worker's completion wakes
|
|
2980
|
-
// its manager automatically. There is deliberately no caller-supplied
|
|
2981
|
-
// parent parameter: it would let a sessions:create grant target an
|
|
2982
|
-
// arbitrary session's wake channel without sessions:control on it.
|
|
2983
|
-
},
|
|
4102
|
+
"Spawn a new agent session (a worker). Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set or Rig gets its own compatible box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
|
|
4103
|
+
inputSchema: sessionCreateInput,
|
|
2984
4104
|
},
|
|
2985
4105
|
async (args) => {
|
|
2986
4106
|
try {
|
|
2987
4107
|
if (callerSessionId !== null) {
|
|
2988
4108
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
2989
4109
|
}
|
|
2990
|
-
const
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
4110
|
+
const { machineTarget, ...request } = args;
|
|
4111
|
+
const result = await createSessionForRequestWithOutcome(deps, grant, grant.workspaceId, {
|
|
4112
|
+
...request,
|
|
4113
|
+
...(machineTarget
|
|
4114
|
+
? {
|
|
4115
|
+
targetSandboxId: machineTarget.targetSandboxId,
|
|
4116
|
+
...(machineTarget.workingDir !== undefined
|
|
4117
|
+
? { workingDir: machineTarget.workingDir }
|
|
4118
|
+
: {}),
|
|
4119
|
+
}
|
|
4120
|
+
: {}),
|
|
4121
|
+
});
|
|
4122
|
+
return json(sessionCreateMutationReceipt(result, Boolean(request.idempotencyKey)));
|
|
2997
4123
|
} catch (error) {
|
|
2998
|
-
|
|
2999
|
-
return {
|
|
3000
|
-
...json(sessionSpawnDenialEnvelope(error)),
|
|
3001
|
-
isError: true,
|
|
3002
|
-
};
|
|
3003
|
-
}
|
|
3004
|
-
throw error;
|
|
4124
|
+
return orchestrationFailureResult("session_create", error);
|
|
3005
4125
|
}
|
|
3006
4126
|
},
|
|
3007
4127
|
);
|
|
3008
4128
|
}
|
|
3009
4129
|
|
|
3010
|
-
if (can("sessions:control")
|
|
4130
|
+
if (can("sessions:control")) {
|
|
3011
4131
|
server.registerTool(
|
|
3012
4132
|
"session_send_message",
|
|
3013
4133
|
{
|
|
@@ -3023,81 +4143,87 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3023
4143
|
},
|
|
3024
4144
|
},
|
|
3025
4145
|
async ({ sessionId: targetSessionId, text, idempotencyKey, mcpCredentialUpdates }) => {
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
if (
|
|
3029
|
-
|
|
4146
|
+
try {
|
|
4147
|
+
await authorizeFirstPartySession(deps, grant, targetSessionId, "session.append");
|
|
4148
|
+
if (callerSessionId !== null) {
|
|
4149
|
+
if ((mcpCredentialUpdates?.length ?? 0) > 0) {
|
|
4150
|
+
throw new HTTPException(422, {
|
|
4151
|
+
message: "internal session updates cannot change MCP credentials",
|
|
4152
|
+
});
|
|
4153
|
+
}
|
|
4154
|
+
const result = await sendAgentSessionMessage(
|
|
4155
|
+
deps,
|
|
4156
|
+
exactAgentCommandContext(grant, callerSessionId),
|
|
4157
|
+
{ targetSessionId, text, idempotencyKey },
|
|
4158
|
+
);
|
|
4159
|
+
return json(
|
|
4160
|
+
mcpMutationReceipt({
|
|
4161
|
+
operation: "session_send_message",
|
|
4162
|
+
committed: true,
|
|
4163
|
+
outcome: result.replay ? "replayed" : "accepted",
|
|
4164
|
+
changed: !result.replay,
|
|
4165
|
+
resource: {
|
|
4166
|
+
type: "session_system_update",
|
|
4167
|
+
id: result.updateId,
|
|
4168
|
+
state: result.effectiveState,
|
|
4169
|
+
},
|
|
4170
|
+
relatedResources: [{ type: "session", id: targetSessionId }],
|
|
4171
|
+
timestamp: result.receipt.createdAt.toISOString(),
|
|
4172
|
+
idempotency: { status: result.replay ? "replayed" : "applied" },
|
|
4173
|
+
facts: {
|
|
4174
|
+
delivery: "coalesced_internal_update",
|
|
4175
|
+
wakeRequested: result.wakeRevision !== null,
|
|
4176
|
+
resumeRequired: result.effectiveState === "paused",
|
|
4177
|
+
},
|
|
4178
|
+
nextAction: {
|
|
4179
|
+
tool: "session_get",
|
|
4180
|
+
arguments: { sessionId: targetSessionId },
|
|
4181
|
+
},
|
|
4182
|
+
}),
|
|
4183
|
+
);
|
|
3030
4184
|
}
|
|
3031
|
-
const
|
|
4185
|
+
const { accepted, turn, replay } = await acceptSessionUserMessageWithOutcome(
|
|
3032
4186
|
deps,
|
|
3033
|
-
|
|
3034
|
-
|
|
4187
|
+
grant,
|
|
4188
|
+
grant.workspaceId,
|
|
4189
|
+
targetSessionId,
|
|
4190
|
+
{
|
|
4191
|
+
text,
|
|
4192
|
+
delivery: "send",
|
|
4193
|
+
origin: "operator",
|
|
4194
|
+
clientEventId: idempotencyKey,
|
|
4195
|
+
mcpCredentialUpdates: (mcpCredentialUpdates ?? []).map((update) =>
|
|
4196
|
+
SessionMcpCredentialUpdateInput.parse(update),
|
|
4197
|
+
),
|
|
4198
|
+
},
|
|
3035
4199
|
);
|
|
3036
4200
|
return json(
|
|
3037
4201
|
mcpMutationReceipt({
|
|
3038
4202
|
operation: "session_send_message",
|
|
3039
4203
|
committed: true,
|
|
3040
|
-
outcome:
|
|
3041
|
-
changed: !
|
|
4204
|
+
outcome: replay ? "replayed" : "accepted",
|
|
4205
|
+
changed: !replay,
|
|
3042
4206
|
resource: {
|
|
3043
|
-
type: "
|
|
3044
|
-
id:
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
relatedResources: [{ type: "session", id: targetSessionId }],
|
|
3048
|
-
timestamp: result.receipt.createdAt.toISOString(),
|
|
3049
|
-
idempotency: { status: result.replay ? "replayed" : "applied" },
|
|
3050
|
-
facts: {
|
|
3051
|
-
delivery: "coalesced_internal_update",
|
|
3052
|
-
wakeRequested: result.wakeRevision !== null,
|
|
3053
|
-
resumeRequired: result.effectiveState === "paused",
|
|
4207
|
+
type: "session_turn",
|
|
4208
|
+
id: turn.id,
|
|
4209
|
+
version: turn.version,
|
|
4210
|
+
state: turn.status,
|
|
3054
4211
|
},
|
|
4212
|
+
relatedResources: [
|
|
4213
|
+
{ type: "session", id: targetSessionId },
|
|
4214
|
+
{ type: "session_event", id: accepted.id, state: accepted.type },
|
|
4215
|
+
],
|
|
4216
|
+
timestamp: accepted.occurredAt,
|
|
4217
|
+
idempotency: { status: replay ? "replayed" : "applied" },
|
|
3055
4218
|
nextAction: {
|
|
3056
4219
|
tool: "session_get",
|
|
3057
4220
|
arguments: { sessionId: targetSessionId },
|
|
3058
4221
|
},
|
|
3059
4222
|
}),
|
|
3060
4223
|
);
|
|
4224
|
+
} catch (error) {
|
|
4225
|
+
return orchestrationFailureResult("session_send_message", error);
|
|
3061
4226
|
}
|
|
3062
|
-
const { accepted, turn, replay } = await acceptSessionUserMessageWithOutcome(
|
|
3063
|
-
deps,
|
|
3064
|
-
grant,
|
|
3065
|
-
grant.workspaceId,
|
|
3066
|
-
targetSessionId,
|
|
3067
|
-
{
|
|
3068
|
-
text,
|
|
3069
|
-
delivery: "send",
|
|
3070
|
-
origin: "operator",
|
|
3071
|
-
clientEventId: idempotencyKey,
|
|
3072
|
-
mcpCredentialUpdates: (mcpCredentialUpdates ?? []).map((update) =>
|
|
3073
|
-
SessionMcpCredentialUpdateInput.parse(update),
|
|
3074
|
-
),
|
|
3075
|
-
},
|
|
3076
|
-
);
|
|
3077
|
-
return json(
|
|
3078
|
-
mcpMutationReceipt({
|
|
3079
|
-
operation: "session_send_message",
|
|
3080
|
-
committed: true,
|
|
3081
|
-
outcome: replay ? "replayed" : "accepted",
|
|
3082
|
-
changed: !replay,
|
|
3083
|
-
resource: {
|
|
3084
|
-
type: "session_turn",
|
|
3085
|
-
id: turn.id,
|
|
3086
|
-
version: turn.version,
|
|
3087
|
-
state: turn.status,
|
|
3088
|
-
},
|
|
3089
|
-
relatedResources: [
|
|
3090
|
-
{ type: "session", id: targetSessionId },
|
|
3091
|
-
{ type: "session_event", id: accepted.id, state: accepted.type },
|
|
3092
|
-
],
|
|
3093
|
-
timestamp: accepted.occurredAt,
|
|
3094
|
-
idempotency: { status: replay ? "replayed" : "applied" },
|
|
3095
|
-
nextAction: {
|
|
3096
|
-
tool: "session_get",
|
|
3097
|
-
arguments: { sessionId: targetSessionId },
|
|
3098
|
-
},
|
|
3099
|
-
}),
|
|
3100
|
-
);
|
|
3101
4227
|
},
|
|
3102
4228
|
);
|
|
3103
4229
|
|
|
@@ -3123,16 +4249,31 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3123
4249
|
reason: reason ?? "agent_mcp_pause",
|
|
3124
4250
|
},
|
|
3125
4251
|
);
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
4252
|
+
const effectiveControl = projectEffectiveControlForRelatedAccess(
|
|
4253
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
4254
|
+
sessionId,
|
|
4255
|
+
controlled.authorization?.relatedSessionAccess ?? "root",
|
|
4256
|
+
);
|
|
4257
|
+
return json(
|
|
4258
|
+
mcpMutationReceipt({
|
|
4259
|
+
operation: "session_pause",
|
|
4260
|
+
committed: true,
|
|
4261
|
+
outcome: controlled.replay ? "replayed" : "updated",
|
|
4262
|
+
changed: !controlled.replay,
|
|
4263
|
+
resource: {
|
|
4264
|
+
type: "session",
|
|
4265
|
+
id: sessionId,
|
|
4266
|
+
state: effectiveControl.state,
|
|
4267
|
+
},
|
|
4268
|
+
relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
|
|
4269
|
+
timestamp: controlled.receipt.createdAt.toISOString(),
|
|
4270
|
+
idempotency: {
|
|
4271
|
+
status: controlled.replay ? "replayed" : "applied",
|
|
4272
|
+
},
|
|
4273
|
+
facts: { interruptionCount: controlled.interruptionCount },
|
|
4274
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
4275
|
+
}),
|
|
4276
|
+
);
|
|
3136
4277
|
}
|
|
3137
4278
|
const controlled = await controlHumanSessionWorkstream(
|
|
3138
4279
|
deps,
|
|
@@ -3176,16 +4317,31 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3176
4317
|
reason: reason ?? "agent_mcp_resume",
|
|
3177
4318
|
},
|
|
3178
4319
|
);
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
4320
|
+
const effectiveControl = projectEffectiveControlForRelatedAccess(
|
|
4321
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
4322
|
+
sessionId,
|
|
4323
|
+
controlled.authorization?.relatedSessionAccess ?? "root",
|
|
4324
|
+
);
|
|
4325
|
+
return json(
|
|
4326
|
+
mcpMutationReceipt({
|
|
4327
|
+
operation: "session_resume",
|
|
4328
|
+
committed: true,
|
|
4329
|
+
outcome: controlled.replay ? "replayed" : "updated",
|
|
4330
|
+
changed: !controlled.replay,
|
|
4331
|
+
resource: {
|
|
4332
|
+
type: "session",
|
|
4333
|
+
id: sessionId,
|
|
4334
|
+
state: effectiveControl.state,
|
|
4335
|
+
},
|
|
4336
|
+
relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
|
|
4337
|
+
timestamp: controlled.receipt.createdAt.toISOString(),
|
|
4338
|
+
idempotency: {
|
|
4339
|
+
status: controlled.replay ? "replayed" : "applied",
|
|
4340
|
+
},
|
|
4341
|
+
facts: { interruptionCount: controlled.interruptionCount },
|
|
4342
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
4343
|
+
}),
|
|
4344
|
+
);
|
|
3189
4345
|
}
|
|
3190
4346
|
const controlled = await controlHumanSessionWorkstream(
|
|
3191
4347
|
deps,
|
|
@@ -3283,7 +4439,6 @@ function registerVariableSetTools(
|
|
|
3283
4439
|
grant: AccessGrant,
|
|
3284
4440
|
can: (permission: Permission) => boolean,
|
|
3285
4441
|
sessionId: string | null,
|
|
3286
|
-
toolspaceMode: boolean,
|
|
3287
4442
|
json: JsonResult,
|
|
3288
4443
|
): void {
|
|
3289
4444
|
const registerListTool = (name: string, description: string): void => {
|
|
@@ -3435,7 +4590,6 @@ function registerVariableSetTools(
|
|
|
3435
4590
|
}
|
|
3436
4591
|
|
|
3437
4592
|
if (
|
|
3438
|
-
!toolspaceMode &&
|
|
3439
4593
|
sessionId !== null &&
|
|
3440
4594
|
can("variable-sets:read") &&
|
|
3441
4595
|
hasLiteralPermission(grant.permissions, "secrets:read")
|
|
@@ -3475,7 +4629,7 @@ function registerVariableSetTools(
|
|
|
3475
4629
|
throw new Error("variable set variable names must match ^[A-Z][A-Z0-9_]*$");
|
|
3476
4630
|
}
|
|
3477
4631
|
assertAllowedVariableSetVariableName(parsedName.data);
|
|
3478
|
-
const claims =
|
|
4632
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
3479
4633
|
if (!claims || claims.sessionId !== sessionId) {
|
|
3480
4634
|
throw new Error("Exact signed secret-read attempt authority is required.");
|
|
3481
4635
|
}
|
|
@@ -3515,6 +4669,252 @@ function registerVariableSetTools(
|
|
|
3515
4669
|
}
|
|
3516
4670
|
}
|
|
3517
4671
|
|
|
4672
|
+
type CapabilitySetupProjection =
|
|
4673
|
+
| { status: "ready"; action: null; detail: string }
|
|
4674
|
+
| {
|
|
4675
|
+
status: "authorization_required";
|
|
4676
|
+
action: "connect" | "add_credentials" | "enable";
|
|
4677
|
+
detail: string;
|
|
4678
|
+
}
|
|
4679
|
+
| { status: "unavailable"; action: null; detail: string };
|
|
4680
|
+
|
|
4681
|
+
function registerCapabilityDiscoveryTools(
|
|
4682
|
+
server: McpServer,
|
|
4683
|
+
deps: ApiRouteDeps,
|
|
4684
|
+
grant: AccessGrant,
|
|
4685
|
+
sessionId: string,
|
|
4686
|
+
json: JsonResult,
|
|
4687
|
+
): void {
|
|
4688
|
+
const catalog = () =>
|
|
4689
|
+
buildCapabilityCatalog({
|
|
4690
|
+
db: deps.db,
|
|
4691
|
+
workspaceId: grant.workspaceId,
|
|
4692
|
+
settings: deps.settings,
|
|
4693
|
+
subjectId: grant.subjectId,
|
|
4694
|
+
});
|
|
4695
|
+
const authorize = async () => {
|
|
4696
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
4697
|
+
};
|
|
4698
|
+
|
|
4699
|
+
server.registerTool(
|
|
4700
|
+
"capability_catalog_search",
|
|
4701
|
+
{
|
|
4702
|
+
description:
|
|
4703
|
+
"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.",
|
|
4704
|
+
inputSchema: {
|
|
4705
|
+
query: z4.string().min(1).max(500),
|
|
4706
|
+
limit: z4.number().int().min(1).max(20).optional(),
|
|
4707
|
+
},
|
|
4708
|
+
},
|
|
4709
|
+
async ({ query, limit }) => {
|
|
4710
|
+
await authorize();
|
|
4711
|
+
const current = await catalog();
|
|
4712
|
+
const ranked = searchCapabilityCatalogItems(
|
|
4713
|
+
[...current.items, ...nativeConnectionCapabilityRecommendations()],
|
|
4714
|
+
query,
|
|
4715
|
+
limit ?? 8,
|
|
4716
|
+
);
|
|
4717
|
+
const matches = await Promise.all(
|
|
4718
|
+
ranked.map(async ({ item, matchedOn }) => ({
|
|
4719
|
+
capabilityId: item.id,
|
|
4720
|
+
name: item.name,
|
|
4721
|
+
description: item.description,
|
|
4722
|
+
kind: item.kind,
|
|
4723
|
+
source: item.source,
|
|
4724
|
+
category: item.category,
|
|
4725
|
+
tags: item.tags.slice(0, 16),
|
|
4726
|
+
providerDomain: item.providerDomain,
|
|
4727
|
+
authKind: item.authKind,
|
|
4728
|
+
tier: item.tier,
|
|
4729
|
+
matchedOn,
|
|
4730
|
+
setup: {
|
|
4731
|
+
...(await capabilitySetupProjection(deps, grant.workspaceId, item)),
|
|
4732
|
+
requiredVariables: capabilityRequiredVariables(item),
|
|
4733
|
+
},
|
|
4734
|
+
})),
|
|
4735
|
+
);
|
|
4736
|
+
return json({ query, matches });
|
|
4737
|
+
},
|
|
4738
|
+
);
|
|
4739
|
+
|
|
4740
|
+
server.registerTool(
|
|
4741
|
+
"capability_authorization_request",
|
|
4742
|
+
{
|
|
4743
|
+
description:
|
|
4744
|
+
"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.",
|
|
4745
|
+
inputSchema: {
|
|
4746
|
+
capabilityId: z4.string().min(1).max(512),
|
|
4747
|
+
rationale: z4.string().min(1).max(2000),
|
|
4748
|
+
},
|
|
4749
|
+
},
|
|
4750
|
+
async ({ capabilityId, rationale }) => {
|
|
4751
|
+
await authorize();
|
|
4752
|
+
const current = await catalog();
|
|
4753
|
+
const item = [...current.items, ...nativeConnectionCapabilityRecommendations()].find(
|
|
4754
|
+
(candidate) => candidate.id === capabilityId,
|
|
4755
|
+
);
|
|
4756
|
+
if (!item || !capabilityCatalogItemIsTrustedForExposure(item)) {
|
|
4757
|
+
throw new Error("Unknown or untrusted capability; search the catalog again.");
|
|
4758
|
+
}
|
|
4759
|
+
const setup = await capabilitySetupProjection(deps, grant.workspaceId, item);
|
|
4760
|
+
if (setup.status === "ready") {
|
|
4761
|
+
return json({
|
|
4762
|
+
capabilityId: item.id,
|
|
4763
|
+
status: "ready",
|
|
4764
|
+
message: setup.detail,
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4767
|
+
if (setup.status === "unavailable") {
|
|
4768
|
+
return json({
|
|
4769
|
+
capabilityId: item.id,
|
|
4770
|
+
status: "unavailable",
|
|
4771
|
+
message: setup.detail,
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
const claims = exactAgentCommandContext(grant, sessionId);
|
|
4775
|
+
const payload = ToolAuthNeededPayload.parse({
|
|
4776
|
+
serverId: item.runtime.mcpServerId ?? "opengeni",
|
|
4777
|
+
toolName: "capability_authorization_request",
|
|
4778
|
+
providerDomain: capabilityProviderDomain(item),
|
|
4779
|
+
reason: "missing_connection",
|
|
4780
|
+
capability: {
|
|
4781
|
+
id: item.id,
|
|
4782
|
+
name: item.name,
|
|
4783
|
+
kind: item.kind,
|
|
4784
|
+
source: item.source,
|
|
4785
|
+
action: setup.action,
|
|
4786
|
+
rationale,
|
|
4787
|
+
requiredVariables: capabilityRequiredVariables(item),
|
|
4788
|
+
},
|
|
4789
|
+
});
|
|
4790
|
+
const appended = await appendAndPublishTurnEventsFenced(
|
|
4791
|
+
deps.db,
|
|
4792
|
+
deps.bus,
|
|
4793
|
+
grant.workspaceId,
|
|
4794
|
+
sessionId,
|
|
4795
|
+
claims.callerTurnId,
|
|
4796
|
+
claims.callerExecutionGeneration,
|
|
4797
|
+
claims.callerAttemptId,
|
|
4798
|
+
[{ type: "tool.auth_needed", payload }],
|
|
4799
|
+
);
|
|
4800
|
+
if (!appended.accepted) {
|
|
4801
|
+
throw new Error(
|
|
4802
|
+
"The calling turn was replaced before the authorization request committed.",
|
|
4803
|
+
);
|
|
4804
|
+
}
|
|
4805
|
+
return json({
|
|
4806
|
+
capabilityId: item.id,
|
|
4807
|
+
status: "authorization_requested",
|
|
4808
|
+
action: setup.action,
|
|
4809
|
+
eventId: appended.events[0]?.id ?? null,
|
|
4810
|
+
message:
|
|
4811
|
+
"The recommendation was posted for human confirmation. No access has been granted yet.",
|
|
4812
|
+
});
|
|
4813
|
+
},
|
|
4814
|
+
);
|
|
4815
|
+
}
|
|
4816
|
+
|
|
4817
|
+
async function capabilitySetupProjection(
|
|
4818
|
+
deps: ApiRouteDeps,
|
|
4819
|
+
workspaceId: string,
|
|
4820
|
+
item: CapabilityCatalogItem,
|
|
4821
|
+
): Promise<CapabilitySetupProjection> {
|
|
4822
|
+
if (item.id === "api:github-app" || item.surfaceType === "first_party_github") {
|
|
4823
|
+
const missing = githubAppMissingSettings(deps.settings);
|
|
4824
|
+
if (missing.length > 0) {
|
|
4825
|
+
return {
|
|
4826
|
+
status: "unavailable",
|
|
4827
|
+
action: null,
|
|
4828
|
+
detail:
|
|
4829
|
+
deps.settings.productAccessMode === "managed"
|
|
4830
|
+
? "GitHub is not available on this deployment."
|
|
4831
|
+
: `The operator must configure the GitHub App first (${missing.join(", ")}).`,
|
|
4832
|
+
};
|
|
4833
|
+
}
|
|
4834
|
+
const installations = await listWorkspaceGitHubInstallationBindings(deps, workspaceId);
|
|
4835
|
+
if (githubBindingStatus(true, installations) === "bound") {
|
|
4836
|
+
return {
|
|
4837
|
+
status: "ready",
|
|
4838
|
+
action: null,
|
|
4839
|
+
detail: "GitHub is connected and ready.",
|
|
4840
|
+
};
|
|
4841
|
+
}
|
|
4842
|
+
return {
|
|
4843
|
+
status: "authorization_required",
|
|
4844
|
+
action: "connect",
|
|
4845
|
+
detail: "A GitHub owner must approve an installation and repository allowlist.",
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
4848
|
+
if (item.surfaceType === "codex_apps") {
|
|
4849
|
+
return item.enabled
|
|
4850
|
+
? {
|
|
4851
|
+
status: "ready",
|
|
4852
|
+
action: null,
|
|
4853
|
+
detail: "Codex Apps is connected and ready.",
|
|
4854
|
+
}
|
|
4855
|
+
: {
|
|
4856
|
+
status: "authorization_required",
|
|
4857
|
+
action: "connect",
|
|
4858
|
+
detail: "A workspace admin must designate an authorized Codex Apps subscription.",
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
if (item.enabled) {
|
|
4862
|
+
return {
|
|
4863
|
+
status: "ready",
|
|
4864
|
+
action: null,
|
|
4865
|
+
detail: "This capability is already enabled.",
|
|
4866
|
+
};
|
|
4867
|
+
}
|
|
4868
|
+
if (!item.runtime.available) {
|
|
4869
|
+
return {
|
|
4870
|
+
status: "unavailable",
|
|
4871
|
+
action: null,
|
|
4872
|
+
detail: item.runtime.notes ?? "This catalog entry has no executable runtime adapter.",
|
|
4873
|
+
};
|
|
4874
|
+
}
|
|
4875
|
+
if (item.authKind === "oauth2" || item.surfaceType === "first_party_social") {
|
|
4876
|
+
return {
|
|
4877
|
+
status: "authorization_required",
|
|
4878
|
+
action: "connect",
|
|
4879
|
+
detail: "The user must confirm the provider domain and complete sign-in.",
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
if (item.authKind === "api_key" || item.authModel?.toLowerCase().includes("key")) {
|
|
4883
|
+
return {
|
|
4884
|
+
status: "authorization_required",
|
|
4885
|
+
action: "add_credentials",
|
|
4886
|
+
detail: "The user must provide the required credential through the protected setup form.",
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
return {
|
|
4890
|
+
status: "authorization_required",
|
|
4891
|
+
action: "enable",
|
|
4892
|
+
detail: "A workspace admin must review and enable this capability.",
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4896
|
+
function capabilityRequiredVariables(item: CapabilityCatalogItem): string[] {
|
|
4897
|
+
const variableSet = item.metadata.variableSet;
|
|
4898
|
+
if (!variableSet || typeof variableSet !== "object" || Array.isArray(variableSet)) return [];
|
|
4899
|
+
const required = (variableSet as Record<string, unknown>).requiredVariables;
|
|
4900
|
+
return Array.isArray(required)
|
|
4901
|
+
? required.filter((name): name is string => typeof name === "string").slice(0, 64)
|
|
4902
|
+
: [];
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
function capabilityProviderDomain(item: CapabilityCatalogItem): string {
|
|
4906
|
+
if (item.providerDomain?.trim()) return item.providerDomain.trim();
|
|
4907
|
+
for (const candidate of [item.homepageUrl, item.endpointUrl, item.mcpUrl]) {
|
|
4908
|
+
if (!candidate) continue;
|
|
4909
|
+
try {
|
|
4910
|
+
return new URL(candidate).hostname;
|
|
4911
|
+
} catch {
|
|
4912
|
+
// Continue to the local, non-provider fallback below.
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
return "opengeni.local";
|
|
4916
|
+
}
|
|
4917
|
+
|
|
3518
4918
|
function registerGitHubConnectTool(
|
|
3519
4919
|
server: McpServer,
|
|
3520
4920
|
deps: ApiRouteDeps,
|
|
@@ -4051,11 +5451,12 @@ function parseMcpDate(raw: string, label: string): Date {
|
|
|
4051
5451
|
async function withMcpEffectivePolicy(
|
|
4052
5452
|
deps: ApiRouteDeps,
|
|
4053
5453
|
workspaceId: string,
|
|
5454
|
+
subjectId: string,
|
|
4054
5455
|
session: Session,
|
|
4055
5456
|
): Promise<Session> {
|
|
4056
5457
|
const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
|
|
4057
|
-
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings),
|
|
4058
|
-
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings),
|
|
5458
|
+
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings, subjectId),
|
|
5459
|
+
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings, subjectId),
|
|
4059
5460
|
]);
|
|
4060
5461
|
return sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds);
|
|
4061
5462
|
}
|