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