@opengeni/api-router 0.30.3 → 2.2.0-canary.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/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
stableJson,
|
|
15
15
|
compactSessionEventResult,
|
|
16
16
|
sessionEventLatestClassToSemanticClass,
|
|
17
|
-
MemorySlackPublicationDistribution,
|
|
18
17
|
SessionMcpCredentialUpdateInput,
|
|
19
18
|
ToolAuthNeededPayload,
|
|
20
19
|
VariableSetVariableName,
|
|
@@ -38,19 +37,20 @@ import {
|
|
|
38
37
|
SESSION_GOAL_RATIONALE_MAX_BYTES,
|
|
39
38
|
SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
|
|
40
39
|
SESSION_GOAL_TEXT_MAX_BYTES,
|
|
40
|
+
SESSION_INSTRUCTIONS_MAX_CHARACTERS,
|
|
41
41
|
sessionGoalUtf8Bytes,
|
|
42
42
|
TASK_NOTE_LIST_DEFAULT_LIMIT,
|
|
43
43
|
TASK_NOTE_LIST_MAX_LIMIT,
|
|
44
44
|
TASK_NOTE_MAX_LIFETIME_DAYS,
|
|
45
45
|
TASK_NOTE_REASON_MAX_BYTES,
|
|
46
46
|
TASK_NOTE_TEXT_MAX_BYTES,
|
|
47
|
+
SubmitHumanInputResponseRequest,
|
|
47
48
|
} from "@opengeni/contracts";
|
|
48
49
|
import {
|
|
49
50
|
countVariableSets,
|
|
50
51
|
beginRigChangeVerificationAttempt,
|
|
51
52
|
createVariableSet,
|
|
52
53
|
decryptVariableSetValue,
|
|
53
|
-
deleteScheduledTask,
|
|
54
54
|
encryptVariableSetValue,
|
|
55
55
|
getSession,
|
|
56
56
|
getSessionGoal,
|
|
@@ -63,7 +63,9 @@ import {
|
|
|
63
63
|
listScheduledTaskRuns,
|
|
64
64
|
listScheduledTasks,
|
|
65
65
|
listSessionEventPage,
|
|
66
|
+
listOutstandingSessionSystemUpdates,
|
|
66
67
|
listSessionDiscoverySummaries,
|
|
68
|
+
listEnrollments,
|
|
67
69
|
projectEffectiveControlForRelatedAccess,
|
|
68
70
|
projectSessionForRelatedAccess,
|
|
69
71
|
type SessionDiscoveryCursor,
|
|
@@ -77,8 +79,6 @@ import {
|
|
|
77
79
|
readVariableSetSecretAtomically,
|
|
78
80
|
recordSyncedSocialPosts,
|
|
79
81
|
listVariableSets,
|
|
80
|
-
MEMORY_CORRECT_TOOL_DESCRIPTION,
|
|
81
|
-
MEMORY_SAVE_TOOL_DESCRIPTION,
|
|
82
82
|
MEMORY_SEARCH_TOOL_DESCRIPTION,
|
|
83
83
|
requireScheduledTask,
|
|
84
84
|
requireSession,
|
|
@@ -86,8 +86,10 @@ import {
|
|
|
86
86
|
serializeEffectiveSessionControl,
|
|
87
87
|
setSessionGoalStatusWithEvent,
|
|
88
88
|
recordSessionGoalProgressWithEvent,
|
|
89
|
+
holdSessionGoalContinuationWithEvent,
|
|
90
|
+
SESSION_GOAL_HOLD_MAX_SECONDS,
|
|
91
|
+
SESSION_GOAL_HOLD_MIN_SECONDS,
|
|
89
92
|
setVariableSetVariable,
|
|
90
|
-
updateScheduledTask,
|
|
91
93
|
updateSessionGoalWithEvent,
|
|
92
94
|
upsertSessionGoalWithEvent,
|
|
93
95
|
RigChangeTransitionError,
|
|
@@ -100,13 +102,12 @@ import {
|
|
|
100
102
|
archiveTaskNote,
|
|
101
103
|
createTaskNote,
|
|
102
104
|
listTaskNotes,
|
|
105
|
+
replaceTaskNote,
|
|
106
|
+
acceptSessionHumanInputResponse,
|
|
107
|
+
HumanInputResponseValidationError,
|
|
103
108
|
} from "@opengeni/db";
|
|
104
|
-
import {
|
|
105
|
-
|
|
106
|
-
appendAndPublishTurnEventsFenced,
|
|
107
|
-
publishDurableSessionEvents,
|
|
108
|
-
} from "@opengeni/events";
|
|
109
|
-
import { allowedFirstPartyMcpToolsForSession } from "@opengeni/config";
|
|
109
|
+
import { appendAndPublishTurnEventsFenced, publishDurableSessionEvents } from "@opengeni/events";
|
|
110
|
+
import { allowedFirstPartyMcpToolsForSession, codemodeWorkspaceUrl } from "@opengeni/config";
|
|
110
111
|
import {
|
|
111
112
|
createSignedState,
|
|
112
113
|
GitHubAppConfigurationError,
|
|
@@ -128,24 +129,24 @@ import {
|
|
|
128
129
|
authorizedAtlassianConnectionsForGrant,
|
|
129
130
|
buildCapabilityCatalog,
|
|
130
131
|
nativeConnectionCapabilityRecommendations,
|
|
131
|
-
correctWorkspaceMemoryWithSlackPublication,
|
|
132
132
|
requireLiveAgentAttemptAuthorization,
|
|
133
133
|
requireSessionAuthorization,
|
|
134
134
|
requireSessionAuthorizationListScope,
|
|
135
135
|
SessionAuthorizationDeniedError,
|
|
136
136
|
SessionAuthorizationUnavailableError,
|
|
137
|
-
saveWorkspaceMemoryWithSlackPublication,
|
|
138
137
|
searchCapabilityCatalogItems,
|
|
139
138
|
type ResolvedSessionAuthorization,
|
|
140
139
|
} from "@opengeni/core";
|
|
141
140
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
142
141
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
142
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
143
143
|
import {
|
|
144
144
|
githubBindingStatus,
|
|
145
145
|
listWorkspaceGitHubInstallationBindings,
|
|
146
146
|
listWorkspaceGitHubRepositories,
|
|
147
147
|
} from "../github-access";
|
|
148
148
|
import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
|
|
149
|
+
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
149
150
|
import {
|
|
150
151
|
assertSocialConnectionProvider,
|
|
151
152
|
socialMentionsLive,
|
|
@@ -154,7 +155,6 @@ import {
|
|
|
154
155
|
socialSearchLive,
|
|
155
156
|
socialThreadLive,
|
|
156
157
|
} from "../integrations/social-api";
|
|
157
|
-
import { revokeKnowledgeSourceScheduleAuthorization } from "../integrations/google-drive";
|
|
158
158
|
import {
|
|
159
159
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
160
160
|
proposeRigChangeForApi,
|
|
@@ -179,6 +179,7 @@ import {
|
|
|
179
179
|
syncCreatedScheduledTask,
|
|
180
180
|
syncUpdatedScheduledTask,
|
|
181
181
|
validateScheduledTaskTarget,
|
|
182
|
+
updateScheduledTaskForApi,
|
|
182
183
|
validatedScheduledTaskUpdate,
|
|
183
184
|
} from "@opengeni/core";
|
|
184
185
|
import {
|
|
@@ -213,6 +214,14 @@ import {
|
|
|
213
214
|
boundRigDetailMcp,
|
|
214
215
|
SESSION_EVENT_MCP_MAX_BYTES,
|
|
215
216
|
} from "./session-view";
|
|
217
|
+
import {
|
|
218
|
+
SESSION_WAIT_DEFAULT_SECONDS,
|
|
219
|
+
SESSION_WAIT_EVENT_TYPES,
|
|
220
|
+
SESSION_WAIT_EVENTS_PER_TARGET,
|
|
221
|
+
SESSION_WAIT_MAX_SECONDS,
|
|
222
|
+
SESSION_WAIT_MAX_TARGETS,
|
|
223
|
+
waitForSessionChanges,
|
|
224
|
+
} from "./session-wait";
|
|
216
225
|
import { mcpMutationReceipt, sessionCreateMutationReceipt } from "./receipts";
|
|
217
226
|
import {
|
|
218
227
|
boundScheduledTaskDetailMcp,
|
|
@@ -228,11 +237,15 @@ import { createFikenClient, resolveFikenConnectionForTool } from "../integration
|
|
|
228
237
|
import {
|
|
229
238
|
browseAtlassianSources,
|
|
230
239
|
getAtlassianLiveItem,
|
|
231
|
-
revokeAtlassianScheduleAuthorization,
|
|
232
240
|
searchAtlassianLive,
|
|
233
241
|
} from "../integrations/atlassian";
|
|
234
242
|
import { AtlassianConnectionMetadata } from "@opengeni/contracts/atlassian";
|
|
235
243
|
import { registerEditableArtifactAgentTools } from "./editable-artifacts";
|
|
244
|
+
import { registerCompanyBrainGovernedWriteTools } from "./company-brain-governed-writes";
|
|
245
|
+
import { registerCompanyProfileAgentAdminTools } from "./company-profile-agent-admin";
|
|
246
|
+
import { registerRememberTools } from "./remember";
|
|
247
|
+
import { mintSandboxCodemodeToken } from "@opengeni/runtime/sandbox";
|
|
248
|
+
import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
|
|
236
249
|
|
|
237
250
|
export type McpServerOptions = {
|
|
238
251
|
// Origin of the HTTP request that reached the MCP route. Browser-oriented
|
|
@@ -322,12 +335,17 @@ function orchestrationFailureEnvelope(tool: OrchestrationToolName, error: unknow
|
|
|
322
335
|
: typedCode === "CALLER_INTERRUPTED"
|
|
323
336
|
? ["caller_interrupted", "The calling session was interrupted before delivery."]
|
|
324
337
|
: typedCode === "TARGET_NOT_VERTICAL"
|
|
325
|
-
? ["target_not_vertical", "
|
|
338
|
+
? ["target_not_vertical", "The calling agent cannot act on that session."]
|
|
326
339
|
: typedCode === "CONTROL_CHANGED"
|
|
327
340
|
? ["conflict", "The target session control state changed; refresh and retry."]
|
|
328
341
|
: typedCode === "IDEMPOTENCY_KEY_REUSED"
|
|
329
342
|
? ["idempotency_key_reused", "The idempotency key was reused with different input."]
|
|
330
|
-
:
|
|
343
|
+
: typedCode === "WORKSPACE_CONTROL_BUSY"
|
|
344
|
+
? [
|
|
345
|
+
"workspace_busy",
|
|
346
|
+
"The workspace is busy applying other session commands; nothing was applied. Retry the same call shortly.",
|
|
347
|
+
]
|
|
348
|
+
: null;
|
|
331
349
|
if (knownFailure) {
|
|
332
350
|
return {
|
|
333
351
|
error: {
|
|
@@ -369,9 +387,13 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
369
387
|
goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
370
388
|
goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
371
389
|
goal_progress: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
390
|
+
goal_wait: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
372
391
|
goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
373
392
|
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
374
393
|
memory_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
394
|
+
// Retired: never registered, so these are never consulted. The map must stay
|
|
395
|
+
// total over the tool-name union, which still carries both names so that
|
|
396
|
+
// previously written scheduled-task snapshots keep parsing.
|
|
375
397
|
memory_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
376
398
|
memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
|
|
377
399
|
preference_registry_summary: {
|
|
@@ -382,6 +404,47 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
382
404
|
task_notes_list: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
383
405
|
task_note_save: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
384
406
|
task_note_archive: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
407
|
+
task_note_replace: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
408
|
+
knowledge_propose: { sessionRequired: true, allOf: ["documents:search"] },
|
|
409
|
+
knowledge_correct: { sessionRequired: true, allOf: ["documents:search"] },
|
|
410
|
+
task_note_promote_knowledge: {
|
|
411
|
+
sessionRequired: true,
|
|
412
|
+
allOf: ["documents:search", "sessions:control"],
|
|
413
|
+
},
|
|
414
|
+
task_note_promote_instruction_policy: {
|
|
415
|
+
sessionRequired: true,
|
|
416
|
+
allOf: ["documents:search", "sessions:control", "workspace:read"],
|
|
417
|
+
},
|
|
418
|
+
task_note_promote_preference: {
|
|
419
|
+
sessionRequired: true,
|
|
420
|
+
allOf: ["documents:search", "sessions:control", "workspace:read"],
|
|
421
|
+
},
|
|
422
|
+
instruction_policy_propose: {
|
|
423
|
+
sessionRequired: true,
|
|
424
|
+
allOf: ["documents:search", "workspace:read"],
|
|
425
|
+
},
|
|
426
|
+
preference_propose: {
|
|
427
|
+
sessionRequired: true,
|
|
428
|
+
allOf: ["documents:search", "workspace:read"],
|
|
429
|
+
},
|
|
430
|
+
// Explicit user-directed remember composes task-note evidence with the
|
|
431
|
+
// governed promotion path, so it needs the union of both permission sets.
|
|
432
|
+
remember: {
|
|
433
|
+
sessionRequired: true,
|
|
434
|
+
allOf: ["documents:search", "sessions:control", "workspace:read"],
|
|
435
|
+
},
|
|
436
|
+
remember_confirm: {
|
|
437
|
+
sessionRequired: true,
|
|
438
|
+
allOf: ["documents:search", "sessions:control", "workspace:read"],
|
|
439
|
+
},
|
|
440
|
+
company_profile_propose: {
|
|
441
|
+
sessionRequired: true,
|
|
442
|
+
allOf: ["sessions:control", "workspace:read"],
|
|
443
|
+
},
|
|
444
|
+
company_profile_confirm: {
|
|
445
|
+
sessionRequired: true,
|
|
446
|
+
allOf: ["sessions:control", "workspace:read"],
|
|
447
|
+
},
|
|
385
448
|
sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
386
449
|
sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
387
450
|
sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
@@ -396,11 +459,17 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
396
459
|
sessions_list: { allOf: ["sessions:read"] },
|
|
397
460
|
session_get: { allOf: ["sessions:read"] },
|
|
398
461
|
session_events: { allOf: ["sessions:read"] },
|
|
462
|
+
// Blocking wait inside a running turn: the live attempt's own session is the
|
|
463
|
+
// self target, so the tool exists only for session-scoped grants.
|
|
464
|
+
session_wait: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
399
465
|
session_create: { allOf: ["sessions:create"] },
|
|
400
466
|
session_send_message: { allOf: ["sessions:control"] },
|
|
401
467
|
session_pause: { allOf: ["sessions:control"] },
|
|
402
468
|
session_resume: { allOf: ["sessions:control"] },
|
|
403
469
|
session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
470
|
+
// A live attempt may answer another session's structured human-input
|
|
471
|
+
// request (never a tool approval, which stays human-only).
|
|
472
|
+
session_human_input_respond: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
404
473
|
set_other_session_title: { allOf: ["sessions:control"] },
|
|
405
474
|
interaction_discover: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
406
475
|
browser_open: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
@@ -483,6 +552,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
483
552
|
anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"],
|
|
484
553
|
},
|
|
485
554
|
slack_bot_list_channels: { allOf: ["connections:read"] },
|
|
555
|
+
slack_bot_search: { allOf: ["connections:read"] },
|
|
486
556
|
slack_bot_channel_history: { allOf: ["connections:read"] },
|
|
487
557
|
slack_bot_thread_replies: { allOf: ["connections:read"] },
|
|
488
558
|
slack_bot_list_users: { allOf: ["connections:read"] },
|
|
@@ -511,6 +581,10 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
511
581
|
artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
512
582
|
artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
513
583
|
artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
584
|
+
sandbox_file_publish: {
|
|
585
|
+
sessionRequired: true,
|
|
586
|
+
allOf: ["files:read", "files:upload"],
|
|
587
|
+
},
|
|
514
588
|
editable_artifact_list: { sessionRequired: true, allOf: ["artifacts:read"] },
|
|
515
589
|
editable_artifact_create: {
|
|
516
590
|
sessionRequired: true,
|
|
@@ -706,15 +780,57 @@ export function buildOpenGeniMcpServer(
|
|
|
706
780
|
grant,
|
|
707
781
|
sessionId,
|
|
708
782
|
json,
|
|
709
|
-
options.workspaceMemoryPromptMode ?? "
|
|
783
|
+
options.workspaceMemoryPromptMode ?? "retrieval_only",
|
|
710
784
|
);
|
|
711
785
|
}
|
|
712
786
|
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
713
787
|
registerPreferenceRegistryTools(server, deps, grant, json);
|
|
714
788
|
registerTaskNoteTools(server, deps, grant, sessionId, json);
|
|
789
|
+
const attempt = exactAgentAttemptClaims(grant)!;
|
|
790
|
+
registerCompanyBrainGovernedWriteTools({
|
|
791
|
+
server,
|
|
792
|
+
db: deps.db,
|
|
793
|
+
attempt: {
|
|
794
|
+
accountId: grant.accountId,
|
|
795
|
+
workspaceId: grant.workspaceId,
|
|
796
|
+
...attempt,
|
|
797
|
+
},
|
|
798
|
+
authorize: async () => {
|
|
799
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
800
|
+
},
|
|
801
|
+
json,
|
|
802
|
+
});
|
|
803
|
+
registerCompanyProfileAgentAdminTools({
|
|
804
|
+
server,
|
|
805
|
+
db: deps.db,
|
|
806
|
+
attempt: {
|
|
807
|
+
accountId: grant.accountId,
|
|
808
|
+
workspaceId: grant.workspaceId,
|
|
809
|
+
...attempt,
|
|
810
|
+
agentSubjectId: grant.subjectId,
|
|
811
|
+
},
|
|
812
|
+
authorize: async () => {
|
|
813
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
814
|
+
},
|
|
815
|
+
json,
|
|
816
|
+
});
|
|
817
|
+
registerRememberTools({
|
|
818
|
+
server,
|
|
819
|
+
db: deps.db,
|
|
820
|
+
attempt: {
|
|
821
|
+
accountId: grant.accountId,
|
|
822
|
+
workspaceId: grant.workspaceId,
|
|
823
|
+
...attempt,
|
|
824
|
+
},
|
|
825
|
+
authorize: async () => {
|
|
826
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
827
|
+
},
|
|
828
|
+
json,
|
|
829
|
+
});
|
|
715
830
|
}
|
|
716
831
|
if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
|
|
717
832
|
registerWorkspaceArtifactTools(server, deps, grant, sessionId, json);
|
|
833
|
+
registerSandboxFileArtifactTool(server, deps, grant, sessionId, json);
|
|
718
834
|
registerEditableArtifactAgentTools({
|
|
719
835
|
server,
|
|
720
836
|
deps,
|
|
@@ -736,7 +852,7 @@ export function buildOpenGeniMcpServer(
|
|
|
736
852
|
registerFleetTools(server, deps, grant, sessionId, json);
|
|
737
853
|
}
|
|
738
854
|
if (can("enrollments:manage") && deps.settings.sandboxSelfhostedEnabled) {
|
|
739
|
-
registerConnectedMachineTools(server, deps, grant, json);
|
|
855
|
+
registerConnectedMachineTools(server, deps, grant, sessionId, json);
|
|
740
856
|
}
|
|
741
857
|
registerRigTools(server, deps, grant, can, sessionId, json);
|
|
742
858
|
registerSlackBotTools(server, deps, grant, sessionId, json);
|
|
@@ -1383,6 +1499,9 @@ export function buildOpenGeniMcpServer(
|
|
|
1383
1499
|
overlapPolicy: z4.string().optional(),
|
|
1384
1500
|
agentConfig: z4.unknown(),
|
|
1385
1501
|
status: z4.string().optional(),
|
|
1502
|
+
// Explicit credential-free connection authority selections; declared
|
|
1503
|
+
// so MCP validation doesn't strip them before the contract parse.
|
|
1504
|
+
connectionAuthorities: z4.array(z4.unknown()).optional(),
|
|
1386
1505
|
variableSetId: z4.string().uuid().optional(),
|
|
1387
1506
|
// Deprecated alias of variableSetId; declared so MCP validation doesn't
|
|
1388
1507
|
// strip it before the contract parse maps it (rename back-compat).
|
|
@@ -1447,6 +1566,9 @@ export function buildOpenGeniMcpServer(
|
|
|
1447
1566
|
overlapPolicy: z4.string().optional(),
|
|
1448
1567
|
agentConfig: z4.unknown().optional(),
|
|
1449
1568
|
status: z4.string().optional(),
|
|
1569
|
+
// Omitted preserves the frozen selections, [] clears them, and an
|
|
1570
|
+
// array replaces them; declared so MCP validation doesn't strip it.
|
|
1571
|
+
connectionAuthorities: z4.array(z4.unknown()).optional(),
|
|
1450
1572
|
variableSetId: z4.string().uuid().nullable().optional(),
|
|
1451
1573
|
// Deprecated alias of variableSetId (rename back-compat); declared so MCP
|
|
1452
1574
|
// validation doesn't strip it before the contract parse maps it.
|
|
@@ -1475,7 +1597,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1475
1597
|
if (!scheduledTaskUpdateChangesState(existing, update)) {
|
|
1476
1598
|
return json(scheduledTaskReceipt("scheduled_tasks_update", existing, "unchanged", false));
|
|
1477
1599
|
}
|
|
1478
|
-
const task = await
|
|
1600
|
+
const task = await updateScheduledTaskForApi(deps.db, grant.workspaceId, id, update);
|
|
1479
1601
|
await syncUpdatedScheduledTask({
|
|
1480
1602
|
db: deps.db,
|
|
1481
1603
|
workflowClient: deps.workflowClient,
|
|
@@ -1498,7 +1620,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1498
1620
|
return json(scheduledTaskReceipt("scheduled_tasks_pause", existing, "unchanged", false));
|
|
1499
1621
|
}
|
|
1500
1622
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
1501
|
-
const task = await
|
|
1623
|
+
const task = await updateScheduledTaskForApi(deps.db, grant.workspaceId, id, {
|
|
1502
1624
|
status: "paused",
|
|
1503
1625
|
});
|
|
1504
1626
|
await syncUpdatedScheduledTask({
|
|
@@ -1523,9 +1645,17 @@ export function buildOpenGeniMcpServer(
|
|
|
1523
1645
|
return json(scheduledTaskReceipt("scheduled_tasks_resume", existing, "unchanged", false));
|
|
1524
1646
|
}
|
|
1525
1647
|
const previous = await captureScheduledTaskRestoreState(deps.db, existing);
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1648
|
+
const update = await validatedScheduledTaskUpdate({
|
|
1649
|
+
settings: deps.settings,
|
|
1650
|
+
db: deps.db,
|
|
1651
|
+
objectStorage: deps.objectStorage,
|
|
1652
|
+
grant,
|
|
1653
|
+
existing,
|
|
1654
|
+
payload: { status: "active" },
|
|
1655
|
+
sessionAuthorization: deps.sessionAuthorization,
|
|
1656
|
+
authorizationSurface: "first_party_mcp",
|
|
1528
1657
|
});
|
|
1658
|
+
const task = await updateScheduledTaskForApi(deps.db, grant.workspaceId, id, update);
|
|
1529
1659
|
await syncUpdatedScheduledTask({
|
|
1530
1660
|
db: deps.db,
|
|
1531
1661
|
workflowClient: deps.workflowClient,
|
|
@@ -1629,41 +1759,19 @@ export function buildOpenGeniMcpServer(
|
|
|
1629
1759
|
inputSchema: { id: z4.string().uuid() },
|
|
1630
1760
|
},
|
|
1631
1761
|
async ({ id }) => {
|
|
1632
|
-
const task = await
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
subjectId: grant.subjectId,
|
|
1637
|
-
});
|
|
1638
|
-
} else {
|
|
1639
|
-
await revokeKnowledgeSourceScheduleAuthorization(deps, {
|
|
1640
|
-
task,
|
|
1641
|
-
subjectId: grant.subjectId,
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
await deps.workflowClient.deleteScheduledTaskSchedule({
|
|
1645
|
-
temporalScheduleId: task.temporalScheduleId,
|
|
1762
|
+
const { task, changed } = await deleteScheduledTaskWithDurableCleanup(deps, {
|
|
1763
|
+
workspaceId: grant.workspaceId,
|
|
1764
|
+
taskId: id,
|
|
1765
|
+
subjectId: grant.subjectId,
|
|
1646
1766
|
});
|
|
1647
|
-
try {
|
|
1648
|
-
await deleteScheduledTask(deps.db, grant.workspaceId, id);
|
|
1649
|
-
} catch {
|
|
1650
|
-
return json(
|
|
1651
|
-
scheduledTaskReceipt("scheduled_tasks_delete", task, "partial_failure", true, {
|
|
1652
|
-
partialFailure: { stage: "database_delete", retryable: true },
|
|
1653
|
-
warnings: [
|
|
1654
|
-
"The Temporal schedule was deleted, but the task database record remains.",
|
|
1655
|
-
],
|
|
1656
|
-
}),
|
|
1657
|
-
);
|
|
1658
|
-
}
|
|
1659
1767
|
return json(
|
|
1660
1768
|
mcpMutationReceipt({
|
|
1661
1769
|
operation: "scheduled_tasks_delete",
|
|
1662
1770
|
committed: true,
|
|
1663
1771
|
outcome: "deleted",
|
|
1664
|
-
changed
|
|
1772
|
+
changed,
|
|
1665
1773
|
resource: { type: "scheduled_task", id: task.id, state: "deleted" },
|
|
1666
|
-
idempotency: { status: "
|
|
1774
|
+
idempotency: { status: changed ? "applied" : "replayed" },
|
|
1667
1775
|
}),
|
|
1668
1776
|
);
|
|
1669
1777
|
},
|
|
@@ -1692,6 +1800,36 @@ export function buildOpenGeniMcpServer(
|
|
|
1692
1800
|
return server;
|
|
1693
1801
|
}
|
|
1694
1802
|
|
|
1803
|
+
function registerSandboxFileArtifactTool(
|
|
1804
|
+
server: McpServer,
|
|
1805
|
+
deps: ApiRouteDeps,
|
|
1806
|
+
grant: AccessGrant,
|
|
1807
|
+
sessionId: string,
|
|
1808
|
+
json: JsonResult,
|
|
1809
|
+
): void {
|
|
1810
|
+
server.registerTool(
|
|
1811
|
+
"sandbox_file_publish",
|
|
1812
|
+
{
|
|
1813
|
+
description:
|
|
1814
|
+
"Publish one exact file from this session's /workspace into durable workspace storage. Use this before presenting a ZIP, CSV, JSON, Markdown, HTML, PDF, Office file, or other sandbox output as downloadable. Returns a permanent, authenticated artifact receipt; never invent or expose a sandbox: URL as the durable result.",
|
|
1815
|
+
inputSchema: {
|
|
1816
|
+
path: z4.string().min(1).max(4_096),
|
|
1817
|
+
},
|
|
1818
|
+
},
|
|
1819
|
+
async ({ path }) => {
|
|
1820
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
1821
|
+
const session = await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1822
|
+
return json(
|
|
1823
|
+
await publishSandboxFileArtifact(deps, {
|
|
1824
|
+
grant,
|
|
1825
|
+
session,
|
|
1826
|
+
path,
|
|
1827
|
+
}),
|
|
1828
|
+
);
|
|
1829
|
+
},
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1695
1833
|
function registerSlackBotTools(
|
|
1696
1834
|
server: McpServer,
|
|
1697
1835
|
deps: ApiRouteDeps,
|
|
@@ -1731,6 +1869,57 @@ function registerSlackBotTools(
|
|
|
1731
1869
|
),
|
|
1732
1870
|
);
|
|
1733
1871
|
|
|
1872
|
+
server.registerTool(
|
|
1873
|
+
"slack_bot_search",
|
|
1874
|
+
{
|
|
1875
|
+
description:
|
|
1876
|
+
"Search public Slack channels workspace-wide as the workspace-shared OpenGeni bot: messages by default, plus files and channels via contentTypes. Public content only; private channels, DMs, and group DMs are searchable only through a member's personal Slack connection. Requires a bot install with the search scopes; older installs must be reinstalled by an admin. Continue with cursor for more results.",
|
|
1877
|
+
inputSchema: {
|
|
1878
|
+
connectionId: z4.string().uuid().optional(),
|
|
1879
|
+
query: z4.string().min(1).max(500),
|
|
1880
|
+
contentTypes: z4
|
|
1881
|
+
.array(z4.enum(["messages", "files", "channels"]))
|
|
1882
|
+
.min(1)
|
|
1883
|
+
.max(3)
|
|
1884
|
+
.optional(),
|
|
1885
|
+
includeBots: z4.boolean().optional(),
|
|
1886
|
+
before: z4.number().int().min(0).optional(),
|
|
1887
|
+
after: z4.number().int().min(0).optional(),
|
|
1888
|
+
sort: z4.enum(["score", "timestamp"]).optional(),
|
|
1889
|
+
sortDir: z4.enum(["asc", "desc"]).optional(),
|
|
1890
|
+
cursor: z4.string().max(1024).optional(),
|
|
1891
|
+
limit: z4.number().int().min(1).max(20).optional(),
|
|
1892
|
+
},
|
|
1893
|
+
},
|
|
1894
|
+
async ({
|
|
1895
|
+
connectionId,
|
|
1896
|
+
query,
|
|
1897
|
+
contentTypes,
|
|
1898
|
+
includeBots,
|
|
1899
|
+
before,
|
|
1900
|
+
after,
|
|
1901
|
+
sort,
|
|
1902
|
+
sortDir,
|
|
1903
|
+
cursor,
|
|
1904
|
+
limit,
|
|
1905
|
+
}) =>
|
|
1906
|
+
json(
|
|
1907
|
+
await (
|
|
1908
|
+
await clientFor(connectionId)
|
|
1909
|
+
).searchContext({
|
|
1910
|
+
query,
|
|
1911
|
+
...(contentTypes ? { contentTypes } : {}),
|
|
1912
|
+
...(includeBots !== undefined ? { includeBots } : {}),
|
|
1913
|
+
...(before !== undefined ? { before } : {}),
|
|
1914
|
+
...(after !== undefined ? { after } : {}),
|
|
1915
|
+
...(sort ? { sort } : {}),
|
|
1916
|
+
...(sortDir ? { sortDir } : {}),
|
|
1917
|
+
...(cursor ? { cursor } : {}),
|
|
1918
|
+
...(limit !== undefined ? { limit } : {}),
|
|
1919
|
+
}),
|
|
1920
|
+
),
|
|
1921
|
+
);
|
|
1922
|
+
|
|
1734
1923
|
server.registerTool(
|
|
1735
1924
|
"slack_bot_channel_history",
|
|
1736
1925
|
{
|
|
@@ -2331,20 +2520,23 @@ function registerGoalTools(
|
|
|
2331
2520
|
"goal_set",
|
|
2332
2521
|
{
|
|
2333
2522
|
description:
|
|
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
|
|
2523
|
+
"Create a goal when this session has none, or replace a completed goal with a new one. While active, idle moments synthesize continuation turns until goal_complete or goal_pause. To change an active or paused goal, use goal_update with its objective revision, a change kind, and rationale.",
|
|
2524
|
+
// `maxAutoContinuations` is deliberately not agent-facing: the ceiling is
|
|
2525
|
+
// API/scheduled-task pacing configuration, and an agent that set its own
|
|
2526
|
+
// cap used to silence its orchestration for hours. Continuation pacing is
|
|
2527
|
+
// the deployment's input-aware idle backoff instead.
|
|
2335
2528
|
inputSchema: {
|
|
2336
2529
|
text: goalText,
|
|
2337
2530
|
successCriteria: successCriteriaSchema.optional(),
|
|
2338
|
-
maxAutoContinuations: z4.number().int().positive().optional(),
|
|
2339
2531
|
},
|
|
2340
2532
|
},
|
|
2341
|
-
async ({ text, successCriteria
|
|
2533
|
+
async ({ text, successCriteria }) => {
|
|
2342
2534
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
2343
2535
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
2344
2536
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
2345
|
-
if (existing) {
|
|
2537
|
+
if (existing && existing.status !== "completed") {
|
|
2346
2538
|
throw new Error(
|
|
2347
|
-
`this session
|
|
2539
|
+
`this session's goal is ${existing.status} at objective revision ${existing.objectiveRevision}; use goal_update to revise it`,
|
|
2348
2540
|
);
|
|
2349
2541
|
}
|
|
2350
2542
|
const callerTurnId =
|
|
@@ -2358,7 +2550,7 @@ function registerGoalTools(
|
|
|
2358
2550
|
sessionId,
|
|
2359
2551
|
text,
|
|
2360
2552
|
successCriteria: successCriteria ?? null,
|
|
2361
|
-
maxAutoContinuations:
|
|
2553
|
+
maxAutoContinuations: null,
|
|
2362
2554
|
createdBy: "agent",
|
|
2363
2555
|
actor: "agent",
|
|
2364
2556
|
});
|
|
@@ -2390,19 +2582,13 @@ function registerGoalTools(
|
|
|
2390
2582
|
"goal_update",
|
|
2391
2583
|
{
|
|
2392
2584
|
description:
|
|
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
|
|
2585
|
+
"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. Every rewrite must use the exact expected objective revision and a concise rationale. Root constraints cannot be changed by an agent. A rewrite is not an execution-progress audit fact; use the optional goal_progress tool when such a fact should be recorded.",
|
|
2394
2586
|
inputSchema: {
|
|
2395
2587
|
text: goalText.optional(),
|
|
2396
2588
|
successCriteria: successCriteriaSchema.nullable().optional(),
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
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(),
|
|
2589
|
+
changeKind: z4.enum(["refinement", "adaptation", "replacement"]),
|
|
2590
|
+
rationale: goalRationale,
|
|
2591
|
+
expectedObjectiveRevision: z4.number().int().positive(),
|
|
2406
2592
|
idempotencyKey: z4.string().uuid(),
|
|
2407
2593
|
},
|
|
2408
2594
|
},
|
|
@@ -2412,12 +2598,11 @@ function registerGoalTools(
|
|
|
2412
2598
|
changeKind,
|
|
2413
2599
|
rationale,
|
|
2414
2600
|
expectedObjectiveRevision,
|
|
2415
|
-
progressNote,
|
|
2416
2601
|
idempotencyKey,
|
|
2417
2602
|
}) => {
|
|
2418
2603
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
2419
|
-
if (text === undefined && successCriteria === undefined
|
|
2420
|
-
throw new Error("goal_update requires
|
|
2604
|
+
if (text === undefined && successCriteria === undefined) {
|
|
2605
|
+
throw new Error("goal_update requires text or successCriteria");
|
|
2421
2606
|
}
|
|
2422
2607
|
const context = exactAgentCommandContext(grant, sessionId);
|
|
2423
2608
|
const command = {
|
|
@@ -2431,36 +2616,22 @@ function registerGoalTools(
|
|
|
2431
2616
|
},
|
|
2432
2617
|
operationKey: idempotencyKey,
|
|
2433
2618
|
};
|
|
2434
|
-
const semantic =
|
|
2435
|
-
text !== undefined
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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");
|
|
2619
|
+
const semantic = await updateSessionGoalWithEvent(deps.db, grant.workspaceId, sessionId, {
|
|
2620
|
+
...(text !== undefined ? { text } : {}),
|
|
2621
|
+
...(successCriteria !== undefined ? { successCriteria } : {}),
|
|
2622
|
+
changeKind,
|
|
2623
|
+
rationale,
|
|
2624
|
+
expectedObjectiveRevision,
|
|
2625
|
+
actor: "agent",
|
|
2626
|
+
command,
|
|
2627
|
+
});
|
|
2628
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, semantic.events);
|
|
2458
2629
|
return json({
|
|
2459
|
-
...goal,
|
|
2460
|
-
operationId: semantic
|
|
2461
|
-
replay:
|
|
2462
|
-
outcome: semantic
|
|
2463
|
-
proposalId: semantic
|
|
2630
|
+
...semantic.goal,
|
|
2631
|
+
operationId: semantic.operationId,
|
|
2632
|
+
replay: semantic.replay,
|
|
2633
|
+
outcome: semantic.outcome,
|
|
2634
|
+
proposalId: semantic.proposalId,
|
|
2464
2635
|
});
|
|
2465
2636
|
},
|
|
2466
2637
|
);
|
|
@@ -2502,6 +2673,64 @@ function registerGoalTools(
|
|
|
2502
2673
|
},
|
|
2503
2674
|
);
|
|
2504
2675
|
|
|
2676
|
+
server.registerTool(
|
|
2677
|
+
"goal_wait",
|
|
2678
|
+
{
|
|
2679
|
+
description:
|
|
2680
|
+
"Hold the active goal's automatic continuation while progress depends on child sessions or an external event. Use it instead of sleeping or polling sessions_list/session_get/session_events, and only after re-checking the child or external state once. Always end your turn right after calling it: you will be woken by a child result, an agent message, a human prompt, or at the deadline, and the goal stays active. The deadline (untilSeconds) is mandatory and capped at 7 days. Never use goal_wait instead of goal_pause when you are blocked on a human decision.",
|
|
2681
|
+
inputSchema: {
|
|
2682
|
+
reason: goalRationale,
|
|
2683
|
+
untilSeconds: z4
|
|
2684
|
+
.number()
|
|
2685
|
+
.int()
|
|
2686
|
+
.min(SESSION_GOAL_HOLD_MIN_SECONDS)
|
|
2687
|
+
.max(SESSION_GOAL_HOLD_MAX_SECONDS),
|
|
2688
|
+
idempotencyKey: z4.string().uuid().optional(),
|
|
2689
|
+
},
|
|
2690
|
+
},
|
|
2691
|
+
async ({ reason, untilSeconds, idempotencyKey }) => {
|
|
2692
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
2693
|
+
const context = exactAgentCommandContext(grant, sessionId);
|
|
2694
|
+
// Without a caller-supplied key the operation is idempotent per (goal
|
|
2695
|
+
// target, turn, exact arguments): a replacement attempt replaying the
|
|
2696
|
+
// same call receives the stored result instead of re-stamping the hold,
|
|
2697
|
+
// while a genuinely new same-turn wait (different reason/deadline) is a
|
|
2698
|
+
// new operation whose later hold wins.
|
|
2699
|
+
const operationKey =
|
|
2700
|
+
idempotencyKey ??
|
|
2701
|
+
`goal-wait:${context.callerTurnId}:${createHash("sha256")
|
|
2702
|
+
.update(JSON.stringify({ reason, untilSeconds }))
|
|
2703
|
+
.digest("hex")
|
|
2704
|
+
.slice(0, 32)}`;
|
|
2705
|
+
const { goal, events, operationId, replay, untilAt } =
|
|
2706
|
+
await holdSessionGoalContinuationWithEvent(deps.db, grant.workspaceId, sessionId, {
|
|
2707
|
+
reason,
|
|
2708
|
+
untilSeconds,
|
|
2709
|
+
command: {
|
|
2710
|
+
accountId: grant.accountId,
|
|
2711
|
+
actor: {
|
|
2712
|
+
type: "agent_attempt",
|
|
2713
|
+
attemptId: context.callerAttemptId,
|
|
2714
|
+
sessionId: context.callerSessionId,
|
|
2715
|
+
turnId: context.callerTurnId,
|
|
2716
|
+
executionGeneration: context.callerExecutionGeneration,
|
|
2717
|
+
},
|
|
2718
|
+
operationKey,
|
|
2719
|
+
},
|
|
2720
|
+
});
|
|
2721
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
2722
|
+
return json({
|
|
2723
|
+
status: "held",
|
|
2724
|
+
goalId: goal.id,
|
|
2725
|
+
untilAt,
|
|
2726
|
+
operationId,
|
|
2727
|
+
replay,
|
|
2728
|
+
nextAction:
|
|
2729
|
+
"End your turn now; you will be woken by a child result, a message, a human prompt, or at untilAt.",
|
|
2730
|
+
});
|
|
2731
|
+
},
|
|
2732
|
+
);
|
|
2733
|
+
|
|
2505
2734
|
server.registerTool(
|
|
2506
2735
|
"goal_complete",
|
|
2507
2736
|
{
|
|
@@ -2713,7 +2942,9 @@ function registerWorkspaceArtifactTools(
|
|
|
2713
2942
|
getWorkspaceArtifact(deps.db, grant.workspaceId, artifactId),
|
|
2714
2943
|
getWorkspaceArtifactContentRef(deps.db, grant.workspaceId, artifactId, versionId),
|
|
2715
2944
|
]);
|
|
2716
|
-
const object = await
|
|
2945
|
+
const object = await retryWhileMissing(async () =>
|
|
2946
|
+
deps.objectStorage!.getObjectBytes(ref.contentKey),
|
|
2947
|
+
);
|
|
2717
2948
|
if (!object) throw new Error("Artifact content is unavailable");
|
|
2718
2949
|
const actualHash = createHash("sha256").update(object.bytes).digest("hex");
|
|
2719
2950
|
if (actualHash !== ref.version.contentSha256)
|
|
@@ -2857,7 +3088,11 @@ function registerTaskNoteTools(
|
|
|
2857
3088
|
if (!resolved || resolved.sessionId !== sessionId) {
|
|
2858
3089
|
throw new Error("Exact signed task-note attempt authority is required.");
|
|
2859
3090
|
}
|
|
2860
|
-
return {
|
|
3091
|
+
return {
|
|
3092
|
+
accountId: grant.accountId,
|
|
3093
|
+
workspaceId: grant.workspaceId,
|
|
3094
|
+
...resolved,
|
|
3095
|
+
};
|
|
2861
3096
|
};
|
|
2862
3097
|
const authorize = async () => {
|
|
2863
3098
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
@@ -2891,7 +3126,13 @@ function registerTaskNoteTools(
|
|
|
2891
3126
|
},
|
|
2892
3127
|
async ({ includeArchived, limit }) => {
|
|
2893
3128
|
await authorize();
|
|
2894
|
-
return json(
|
|
3129
|
+
return json(
|
|
3130
|
+
await listTaskNotes(deps.db, {
|
|
3131
|
+
...attemptClaims(),
|
|
3132
|
+
includeArchived,
|
|
3133
|
+
limit,
|
|
3134
|
+
}),
|
|
3135
|
+
);
|
|
2895
3136
|
},
|
|
2896
3137
|
);
|
|
2897
3138
|
|
|
@@ -2946,6 +3187,53 @@ function registerTaskNoteTools(
|
|
|
2946
3187
|
);
|
|
2947
3188
|
},
|
|
2948
3189
|
);
|
|
3190
|
+
|
|
3191
|
+
server.registerTool(
|
|
3192
|
+
"task_note_replace",
|
|
3193
|
+
{
|
|
3194
|
+
description:
|
|
3195
|
+
"Atomically correct one exact active coordination note: archive the old immutable note and create a fresh linked replacement. Exact retries replay safely; stale versions or changed input fail closed.",
|
|
3196
|
+
inputSchema: {
|
|
3197
|
+
operationId: z4.string().uuid(),
|
|
3198
|
+
replacedNoteId: z4.string().uuid(),
|
|
3199
|
+
expectedReplacedVersion: z4.number().int().min(1).max(1),
|
|
3200
|
+
replacementKind: z4.enum([
|
|
3201
|
+
"finding",
|
|
3202
|
+
"decision",
|
|
3203
|
+
"blocker",
|
|
3204
|
+
"ownership",
|
|
3205
|
+
"artifact",
|
|
3206
|
+
"handoff",
|
|
3207
|
+
]),
|
|
3208
|
+
replacementText: boundedUtf8(TASK_NOTE_TEXT_MAX_BYTES, "Task note replacement text"),
|
|
3209
|
+
replacementExpiresInDays: z4.number().int().min(1).max(TASK_NOTE_MAX_LIFETIME_DAYS),
|
|
3210
|
+
reason: boundedUtf8(TASK_NOTE_REASON_MAX_BYTES, "Task note replacement reason"),
|
|
3211
|
+
},
|
|
3212
|
+
},
|
|
3213
|
+
async ({
|
|
3214
|
+
operationId,
|
|
3215
|
+
replacedNoteId,
|
|
3216
|
+
expectedReplacedVersion,
|
|
3217
|
+
replacementKind,
|
|
3218
|
+
replacementText,
|
|
3219
|
+
replacementExpiresInDays,
|
|
3220
|
+
reason,
|
|
3221
|
+
}) => {
|
|
3222
|
+
await authorize();
|
|
3223
|
+
return json(
|
|
3224
|
+
await replaceTaskNote(deps.db, {
|
|
3225
|
+
...attemptClaims(),
|
|
3226
|
+
operationId,
|
|
3227
|
+
replacedNoteId,
|
|
3228
|
+
expectedReplacedVersion,
|
|
3229
|
+
replacementKind,
|
|
3230
|
+
replacementText,
|
|
3231
|
+
replacementExpiresInDays,
|
|
3232
|
+
reason,
|
|
3233
|
+
}),
|
|
3234
|
+
);
|
|
3235
|
+
},
|
|
3236
|
+
);
|
|
2949
3237
|
}
|
|
2950
3238
|
|
|
2951
3239
|
function registerPreferenceRegistryTools(
|
|
@@ -3060,11 +3348,6 @@ function scheduledTaskUpdateChangesState(
|
|
|
3060
3348
|
return false;
|
|
3061
3349
|
}
|
|
3062
3350
|
|
|
3063
|
-
function memoryPreview(text: string): string {
|
|
3064
|
-
const normalized = text.replace(/\s+/g, " ").trim();
|
|
3065
|
-
return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3068
3351
|
export function memorySlackPublicationActor(
|
|
3069
3352
|
actor: Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>,
|
|
3070
3353
|
sessionId: string,
|
|
@@ -3091,24 +3374,10 @@ function registerMemoryTools(
|
|
|
3091
3374
|
json: JsonResult,
|
|
3092
3375
|
promptMode: WorkspaceMemoryPromptMode,
|
|
3093
3376
|
): 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
|
-
|
|
3105
3377
|
server.registerTool(
|
|
3106
3378
|
"memory_search",
|
|
3107
3379
|
{
|
|
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,
|
|
3380
|
+
description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} Legacy preference-kind records are excluded from this tool because structured preferences are the only behavioral authority. To save something the user explicitly asked to keep, use \`remember\`; for your own findings use task notes and their promotion tools.`,
|
|
3112
3381
|
inputSchema: {
|
|
3113
3382
|
query: z4.string().min(1),
|
|
3114
3383
|
kind: MemoryKindSchema.optional(),
|
|
@@ -3131,204 +3400,10 @@ function registerMemoryTools(
|
|
|
3131
3400
|
}),
|
|
3132
3401
|
);
|
|
3133
3402
|
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
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,
|
|
3141
|
-
inputSchema: {
|
|
3142
|
-
text: z4.string().min(1),
|
|
3143
|
-
kind: MemoryKindSchema,
|
|
3144
|
-
confidence: z4.number().min(0).max(1).optional(),
|
|
3145
|
-
replaces_id: z4.string().min(1).optional(),
|
|
3146
|
-
slack_publication: publicationInputSchema.optional(),
|
|
3147
|
-
},
|
|
3148
|
-
},
|
|
3149
|
-
async ({ text, kind, confidence, replaces_id, slack_publication }) => {
|
|
3150
|
-
const principal = slack_publication ? await publicationActor() : null;
|
|
3151
|
-
const result = await saveWorkspaceMemoryWithSlackPublication(
|
|
3152
|
-
deps.db,
|
|
3153
|
-
{
|
|
3154
|
-
accountId: grant.accountId,
|
|
3155
|
-
workspaceId: grant.workspaceId,
|
|
3156
|
-
sessionId,
|
|
3157
|
-
text,
|
|
3158
|
-
kind,
|
|
3159
|
-
...(confidence !== undefined ? { confidence } : {}),
|
|
3160
|
-
...(replaces_id ? { replacesId: replaces_id } : {}),
|
|
3161
|
-
origin: "agent",
|
|
3162
|
-
},
|
|
3163
|
-
slack_publication
|
|
3164
|
-
? {
|
|
3165
|
-
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3166
|
-
actor: principal!.actor,
|
|
3167
|
-
ownerLabel: principal!.ownerLabel,
|
|
3168
|
-
}
|
|
3169
|
-
: null,
|
|
3170
|
-
deps.getDocumentServices().embedder,
|
|
3171
|
-
);
|
|
3172
|
-
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
3173
|
-
{
|
|
3174
|
-
type: "memory.saved",
|
|
3175
|
-
payload: {
|
|
3176
|
-
memoryId: result.memory.id,
|
|
3177
|
-
kind: result.memory.kind,
|
|
3178
|
-
preview: memoryPreview(result.memory.text),
|
|
3179
|
-
deduped: result.deduped,
|
|
3180
|
-
...(result.superseded ? { supersededMemoryId: result.superseded.id } : {}),
|
|
3181
|
-
},
|
|
3182
|
-
},
|
|
3183
|
-
]);
|
|
3184
|
-
const changed = !result.deduped || result.updated || result.superseded !== null;
|
|
3185
|
-
const outcome =
|
|
3186
|
-
result.updated || result.superseded !== null
|
|
3187
|
-
? "updated"
|
|
3188
|
-
: result.deduped
|
|
3189
|
-
? "unchanged"
|
|
3190
|
-
: "created";
|
|
3191
|
-
return json(
|
|
3192
|
-
mcpMutationReceipt({
|
|
3193
|
-
operation: "memory_save",
|
|
3194
|
-
committed: true,
|
|
3195
|
-
outcome,
|
|
3196
|
-
changed,
|
|
3197
|
-
resource: {
|
|
3198
|
-
type: "knowledge_memory",
|
|
3199
|
-
id: result.memory.id,
|
|
3200
|
-
state: result.memory.status,
|
|
3201
|
-
},
|
|
3202
|
-
relatedResources: result.superseded
|
|
3203
|
-
? [
|
|
3204
|
-
{
|
|
3205
|
-
type: "knowledge_memory",
|
|
3206
|
-
id: result.superseded.id,
|
|
3207
|
-
state: result.superseded.status,
|
|
3208
|
-
},
|
|
3209
|
-
]
|
|
3210
|
-
: undefined,
|
|
3211
|
-
timestamp: result.memory.updatedAt,
|
|
3212
|
-
idempotency: { status: "not_supported" },
|
|
3213
|
-
warnings: !result.embedded
|
|
3214
|
-
? ["Memory committed without a vector embedding; keyword search remains available."]
|
|
3215
|
-
: [],
|
|
3216
|
-
facts: {
|
|
3217
|
-
deduped: result.deduped,
|
|
3218
|
-
dedupeReason: result.dedupeReason,
|
|
3219
|
-
updatedInPlace: result.updated,
|
|
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,
|
|
3234
|
-
},
|
|
3235
|
-
}),
|
|
3236
|
-
);
|
|
3237
|
-
},
|
|
3238
|
-
);
|
|
3239
|
-
|
|
3240
|
-
server.registerTool(
|
|
3241
|
-
"memory_correct",
|
|
3242
|
-
{
|
|
3243
|
-
description: MEMORY_CORRECT_TOOL_DESCRIPTION,
|
|
3244
|
-
inputSchema: {
|
|
3245
|
-
id: z4.string().min(1),
|
|
3246
|
-
reason: z4.string().min(1).optional(),
|
|
3247
|
-
replacement_text: z4.string().min(1).optional(),
|
|
3248
|
-
slack_publication: publicationInputSchema.optional(),
|
|
3249
|
-
},
|
|
3250
|
-
},
|
|
3251
|
-
async ({ id, reason, replacement_text, slack_publication }) => {
|
|
3252
|
-
const principal = slack_publication ? await publicationActor() : null;
|
|
3253
|
-
const result = await correctWorkspaceMemoryWithSlackPublication(
|
|
3254
|
-
deps.db,
|
|
3255
|
-
{
|
|
3256
|
-
accountId: grant.accountId,
|
|
3257
|
-
workspaceId: grant.workspaceId,
|
|
3258
|
-
sessionId,
|
|
3259
|
-
id,
|
|
3260
|
-
...(reason ? { reason } : {}),
|
|
3261
|
-
...(replacement_text ? { replacementText: replacement_text } : {}),
|
|
3262
|
-
},
|
|
3263
|
-
slack_publication
|
|
3264
|
-
? {
|
|
3265
|
-
distribution: MemorySlackPublicationDistribution.parse(slack_publication),
|
|
3266
|
-
actor: principal!.actor,
|
|
3267
|
-
ownerLabel: principal!.ownerLabel,
|
|
3268
|
-
}
|
|
3269
|
-
: null,
|
|
3270
|
-
deps.getDocumentServices().embedder,
|
|
3271
|
-
);
|
|
3272
|
-
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
3273
|
-
{
|
|
3274
|
-
type: "memory.corrected",
|
|
3275
|
-
payload: {
|
|
3276
|
-
memoryId: result.memory.id,
|
|
3277
|
-
kind: result.memory.kind,
|
|
3278
|
-
preview: memoryPreview(result.memory.text),
|
|
3279
|
-
action: result.action,
|
|
3280
|
-
...(reason ? { reason: memoryPreview(reason) } : {}),
|
|
3281
|
-
...(result.replacement
|
|
3282
|
-
? {
|
|
3283
|
-
replacementMemoryId: result.replacement.id,
|
|
3284
|
-
replacementPreview: memoryPreview(result.replacement.text),
|
|
3285
|
-
}
|
|
3286
|
-
: {}),
|
|
3287
|
-
},
|
|
3288
|
-
},
|
|
3289
|
-
]);
|
|
3290
|
-
return json(
|
|
3291
|
-
mcpMutationReceipt({
|
|
3292
|
-
operation: "memory_correct",
|
|
3293
|
-
committed: true,
|
|
3294
|
-
outcome: "updated",
|
|
3295
|
-
changed: true,
|
|
3296
|
-
resource: {
|
|
3297
|
-
type: "knowledge_memory",
|
|
3298
|
-
id: result.memory.id,
|
|
3299
|
-
state: result.memory.status,
|
|
3300
|
-
},
|
|
3301
|
-
relatedResources: result.replacement
|
|
3302
|
-
? [
|
|
3303
|
-
{
|
|
3304
|
-
type: "knowledge_memory",
|
|
3305
|
-
id: result.replacement.id,
|
|
3306
|
-
state: result.replacement.status,
|
|
3307
|
-
},
|
|
3308
|
-
]
|
|
3309
|
-
: undefined,
|
|
3310
|
-
timestamp: (result.replacement ?? result.memory).updatedAt,
|
|
3311
|
-
idempotency: { status: "not_supported" },
|
|
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
|
-
},
|
|
3328
|
-
}),
|
|
3329
|
-
);
|
|
3330
|
-
},
|
|
3331
|
-
);
|
|
3403
|
+
// Memory V1 writes are retired. Explicit user-directed knowledge goes
|
|
3404
|
+
// through `remember`; an agent's own findings go through task notes and
|
|
3405
|
+
// governed promotion. `memory_search` stays: reading the existing record
|
|
3406
|
+
// set is still how an agent recalls what a workspace already knows.
|
|
3332
3407
|
}
|
|
3333
3408
|
|
|
3334
3409
|
// Fleet tools (M7 bring-your-own-compute). Session-scoped (they steer THIS
|
|
@@ -3353,7 +3428,12 @@ function registerFleetTools(
|
|
|
3353
3428
|
const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
|
|
3354
3429
|
return await ensureViewerSessionGroupReady(
|
|
3355
3430
|
{ db: deps.db, settings: deps.settings, bus: deps.bus },
|
|
3356
|
-
{
|
|
3431
|
+
{
|
|
3432
|
+
accountId: ctx.accountId,
|
|
3433
|
+
workspaceId: ctx.workspaceId,
|
|
3434
|
+
session,
|
|
3435
|
+
subjectId: ctx.subjectId ?? null,
|
|
3436
|
+
},
|
|
3357
3437
|
);
|
|
3358
3438
|
},
|
|
3359
3439
|
};
|
|
@@ -3362,12 +3442,49 @@ function registerFleetTools(
|
|
|
3362
3442
|
// call-time via the shared helper (same context the user-authenticated swap
|
|
3363
3443
|
// REST route builds). Throws when the session has no box (backend:none) — the
|
|
3364
3444
|
// fleet is only meaningful for a session that runs in a sandbox.
|
|
3365
|
-
const fleetContext = async (): Promise<FleetContext> =>
|
|
3366
|
-
|
|
3445
|
+
const fleetContext = async (): Promise<FleetContext> => {
|
|
3446
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
3447
|
+
const actor = claims
|
|
3448
|
+
? await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId)
|
|
3449
|
+
: null;
|
|
3450
|
+
return await buildFleetContextForSession(deps, {
|
|
3367
3451
|
accountId: grant.accountId,
|
|
3368
3452
|
workspaceId: grant.workspaceId,
|
|
3369
3453
|
sessionId,
|
|
3454
|
+
...(actor?.initiatingHumanSubjectId ? { subjectId: actor.initiatingHumanSubjectId } : {}),
|
|
3455
|
+
...(actor?.initiatingHumanSubjectId && claims
|
|
3456
|
+
? {
|
|
3457
|
+
attemptAuthority: {
|
|
3458
|
+
turnId: claims.turnId,
|
|
3459
|
+
attemptId: claims.attemptId,
|
|
3460
|
+
executionGeneration: claims.executionGeneration,
|
|
3461
|
+
initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
|
|
3462
|
+
},
|
|
3463
|
+
}
|
|
3464
|
+
: {}),
|
|
3370
3465
|
});
|
|
3466
|
+
};
|
|
3467
|
+
|
|
3468
|
+
const oneOffCodemodeEnvironment = async (
|
|
3469
|
+
op: RunOnOp,
|
|
3470
|
+
): Promise<Readonly<Record<string, string>> | undefined> => {
|
|
3471
|
+
if (op.kind !== "exec") return undefined;
|
|
3472
|
+
const claims = exactAgentAttemptClaims(grant);
|
|
3473
|
+
if (!claims || claims.sessionId !== sessionId) return undefined;
|
|
3474
|
+
const material = await mintSandboxCodemodeToken(
|
|
3475
|
+
deps.settings,
|
|
3476
|
+
{ accountId: grant.accountId, workspaceId: grant.workspaceId },
|
|
3477
|
+
claims,
|
|
3478
|
+
);
|
|
3479
|
+
if (!material) return undefined;
|
|
3480
|
+
return {
|
|
3481
|
+
OPENGENI_CODEMODE_URL: codemodeWorkspaceUrl(deps.settings, grant.workspaceId),
|
|
3482
|
+
OPENGENI_CODEMODE_TOKEN: material.token,
|
|
3483
|
+
...(deps.settings.ogtoolPackageSpec
|
|
3484
|
+
? { OPENGENI_OGTOOL_PACKAGE_SPEC: deps.settings.ogtoolPackageSpec }
|
|
3485
|
+
: {}),
|
|
3486
|
+
};
|
|
3487
|
+
};
|
|
3371
3488
|
|
|
3372
3489
|
server.registerTool(
|
|
3373
3490
|
"sandboxes_list",
|
|
@@ -3383,7 +3500,7 @@ function registerFleetTools(
|
|
|
3383
3500
|
"sandbox_attach",
|
|
3384
3501
|
{
|
|
3385
3502
|
description:
|
|
3386
|
-
'Attach this session to a sandbox for
|
|
3503
|
+
'Attach this session to a sandbox for subsequent sandbox operations. The target must be owned and verified ready. A managed-home session currently running on a Connected Machine crosses a safe attempt boundary when attached back to home: OpenGeni checkpoints completed work and continues the same logical turn on home without another user message. A same-target attach is a repair request: it revalidates readiness and advances the route epoch rather than returning unchanged success. Recovery-in-progress/degraded/unrecoverable outcomes are typed. Use a sandboxes_list `id`, or "session"/"default" for home.',
|
|
3387
3504
|
inputSchema: { target: z4.string().min(1) },
|
|
3388
3505
|
},
|
|
3389
3506
|
async ({ target }) => json(await swapActiveSandbox(services, await fleetContext(), target)),
|
|
@@ -3393,7 +3510,7 @@ function registerFleetTools(
|
|
|
3393
3510
|
"sandbox_swap",
|
|
3394
3511
|
{
|
|
3395
3512
|
description:
|
|
3396
|
-
'Swap the active sandbox for this session mid-conversation. Validates ownership and verified readiness, then advances the route epoch. Same-target swaps also revalidate and fence stale route caches. An operation that encountered provider disappearance is not replayed; retry only after a typed recovery-ready result. Use a sandboxes_list `id`, or "session"/"default" for home.',
|
|
3513
|
+
'Swap the active sandbox for this session mid-conversation. Validates ownership and verified readiness, then advances the route epoch. A managed-home session currently running on a Connected Machine crosses a safe attempt boundary when swapped back to home: OpenGeni checkpoints completed work and continues the same logical turn on home without another user message. Same-target swaps also revalidate and fence stale route caches. An operation that encountered provider disappearance is not replayed; retry only after a typed recovery-ready result. Use a sandboxes_list `id`, or "session"/"default" for home.',
|
|
3397
3514
|
inputSchema: { target: z4.string().min(1) },
|
|
3398
3515
|
},
|
|
3399
3516
|
async ({ target }) => json(await swapActiveSandbox(services, await fleetContext(), target)),
|
|
@@ -3421,8 +3538,15 @@ function registerFleetTools(
|
|
|
3421
3538
|
]),
|
|
3422
3539
|
},
|
|
3423
3540
|
},
|
|
3424
|
-
async ({ target, op }) =>
|
|
3425
|
-
|
|
3541
|
+
async ({ target, op }) => {
|
|
3542
|
+
const typedOp = op as RunOnOp;
|
|
3543
|
+
const transientExecEnvironment = await oneOffCodemodeEnvironment(typedOp);
|
|
3544
|
+
return json(
|
|
3545
|
+
await runOnSandbox(services, await fleetContext(), target, typedOp, {
|
|
3546
|
+
...(transientExecEnvironment ? { transientExecEnvironment } : {}),
|
|
3547
|
+
}),
|
|
3548
|
+
);
|
|
3549
|
+
},
|
|
3426
3550
|
);
|
|
3427
3551
|
|
|
3428
3552
|
server.registerTool(
|
|
@@ -3454,13 +3578,14 @@ function registerConnectedMachineTools(
|
|
|
3454
3578
|
server: McpServer,
|
|
3455
3579
|
deps: ApiRouteDeps,
|
|
3456
3580
|
grant: AccessGrant,
|
|
3581
|
+
sessionId: string | null,
|
|
3457
3582
|
json: JsonResult,
|
|
3458
3583
|
): void {
|
|
3459
3584
|
server.registerTool(
|
|
3460
3585
|
"connected_machine_remove",
|
|
3461
3586
|
{
|
|
3462
3587
|
description:
|
|
3463
|
-
"Remove one enrolled self-hosted machine while it is offline. Access is revoked, future heartbeat/reconnect credentials are rejected, session
|
|
3588
|
+
"Remove one enrolled self-hosted machine while it is offline. Access is revoked, future heartbeat/reconnect credentials are rejected, and session, route, lease, archive, and audit history is retained. Idle dependent sessions are detached atomically; machine-home sessions become compute-less (backend none) until another sandbox is selected. Active turns, live leases, and recovery work remain fail-closed blockers whose typed outcome explains what must settle before retrying. Pass the enrollmentId from the Machines surface, never a Modal sandbox id. Reconnecting later requires a fresh human-approved device-flow enrollment.",
|
|
3464
3589
|
inputSchema: {
|
|
3465
3590
|
enrollmentId: z4.string().uuid(),
|
|
3466
3591
|
expectedUpdatedAt: z4.string().datetime({ offset: true }).optional(),
|
|
@@ -3468,13 +3593,31 @@ function registerConnectedMachineTools(
|
|
|
3468
3593
|
},
|
|
3469
3594
|
},
|
|
3470
3595
|
async ({ enrollmentId, expectedUpdatedAt, idempotencyKey }) => {
|
|
3596
|
+
const resourceGrant =
|
|
3597
|
+
sessionId && exactAgentAttemptClaims(grant)
|
|
3598
|
+
? {
|
|
3599
|
+
...grant,
|
|
3600
|
+
subjectId:
|
|
3601
|
+
(await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId))
|
|
3602
|
+
.initiatingHumanSubjectId ?? grant.subjectId,
|
|
3603
|
+
}
|
|
3604
|
+
: grant;
|
|
3605
|
+
const enrollment = (await listEnrollments(deps.db, resourceGrant, { status: "active" })).find(
|
|
3606
|
+
(candidate) => candidate.id === enrollmentId,
|
|
3607
|
+
);
|
|
3608
|
+
if (!enrollment) {
|
|
3609
|
+
throw new Error("machine enrollment not found in this access scope");
|
|
3610
|
+
}
|
|
3611
|
+
if (enrollment.scope === "organization" && !grant.permissions.includes("account:admin")) {
|
|
3612
|
+
throw new Error("missing permission: account:admin");
|
|
3613
|
+
}
|
|
3471
3614
|
const result = await removeEnrollment(deps.db, {
|
|
3472
3615
|
accountId: grant.accountId,
|
|
3473
|
-
workspaceId:
|
|
3616
|
+
workspaceId: enrollment.workspaceId,
|
|
3474
3617
|
enrollmentId,
|
|
3475
3618
|
operationKey: idempotencyKey?.trim() || randomUUID(),
|
|
3476
3619
|
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
3477
|
-
subjectId:
|
|
3620
|
+
subjectId: resourceGrant.subjectId,
|
|
3478
3621
|
});
|
|
3479
3622
|
if (!result) {
|
|
3480
3623
|
throw new Error("machine enrollment not found in this workspace");
|
|
@@ -3518,6 +3661,22 @@ function registerRigTools(
|
|
|
3518
3661
|
sessionId: string | null,
|
|
3519
3662
|
json: JsonResult,
|
|
3520
3663
|
): void {
|
|
3664
|
+
const resourceGrant = async (): Promise<AccessGrant> => {
|
|
3665
|
+
if (!sessionId || exactAgentAttemptClaims(grant) === null) return grant;
|
|
3666
|
+
const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
3667
|
+
return actor.initiatingHumanSubjectId
|
|
3668
|
+
? { ...grant, subjectId: actor.initiatingHumanSubjectId }
|
|
3669
|
+
: grant;
|
|
3670
|
+
};
|
|
3671
|
+
const requireMutableRig = async (rigId: string) => {
|
|
3672
|
+
const rig = await requireRigForApi(deps.db, await resourceGrant(), rigId);
|
|
3673
|
+
if (rig.scope === "organization") {
|
|
3674
|
+
throw new Error(
|
|
3675
|
+
"Organization rig mutation requires account-admin authority through the authenticated REST surface.",
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
return rig;
|
|
3679
|
+
};
|
|
3521
3680
|
if (can("rigs:use")) {
|
|
3522
3681
|
server.registerTool(
|
|
3523
3682
|
"rig_list",
|
|
@@ -3525,7 +3684,7 @@ function registerRigTools(
|
|
|
3525
3684
|
description: "List workspace rigs and their active versions.",
|
|
3526
3685
|
inputSchema: {},
|
|
3527
3686
|
},
|
|
3528
|
-
async () => json({ rigs: await listRigs(deps.db,
|
|
3687
|
+
async () => json({ rigs: await listRigs(deps.db, await resourceGrant()) }),
|
|
3529
3688
|
);
|
|
3530
3689
|
|
|
3531
3690
|
server.registerTool(
|
|
@@ -3540,17 +3699,17 @@ function registerRigTools(
|
|
|
3540
3699
|
},
|
|
3541
3700
|
},
|
|
3542
3701
|
async ({ rigId, versionLimit, changeLimit }) => {
|
|
3543
|
-
const rig = await requireRigForApi(deps.db,
|
|
3702
|
+
const rig = await requireRigForApi(deps.db, await resourceGrant(), rigId);
|
|
3544
3703
|
const [versions, changes] = await Promise.all([
|
|
3545
3704
|
listRigVersionMonitoringSummaries(
|
|
3546
3705
|
deps.db,
|
|
3547
|
-
|
|
3706
|
+
rig.workspaceId,
|
|
3548
3707
|
rig.id,
|
|
3549
3708
|
boundedRigHistoryLimit(versionLimit),
|
|
3550
3709
|
),
|
|
3551
3710
|
listRigChangeMonitoringSummaries(
|
|
3552
3711
|
deps.db,
|
|
3553
|
-
|
|
3712
|
+
rig.workspaceId,
|
|
3554
3713
|
rig.id,
|
|
3555
3714
|
boundedRigHistoryLimit(changeLimit),
|
|
3556
3715
|
),
|
|
@@ -3571,10 +3730,11 @@ function registerRigTools(
|
|
|
3571
3730
|
},
|
|
3572
3731
|
},
|
|
3573
3732
|
async ({ rigId, command, note }) => {
|
|
3574
|
-
const rig = await
|
|
3733
|
+
const rig = await requireMutableRig(rigId);
|
|
3734
|
+
const originGrant = { ...(await resourceGrant()), workspaceId: rig.workspaceId };
|
|
3575
3735
|
const change = await proposeRigChangeForApi(
|
|
3576
3736
|
{ db: deps.db },
|
|
3577
|
-
|
|
3737
|
+
originGrant,
|
|
3578
3738
|
rig,
|
|
3579
3739
|
{
|
|
3580
3740
|
kind: "setup_append",
|
|
@@ -3582,11 +3742,11 @@ function registerRigTools(
|
|
|
3582
3742
|
},
|
|
3583
3743
|
sessionId ? { proposedBy: `session:${sessionId}` } : {},
|
|
3584
3744
|
);
|
|
3585
|
-
const verifying = await beginMcpRigVerificationAttempt(deps,
|
|
3745
|
+
const verifying = await beginMcpRigVerificationAttempt(deps, rig.workspaceId, change.id);
|
|
3586
3746
|
const attempt = verificationAttempt(verifying);
|
|
3587
3747
|
try {
|
|
3588
3748
|
await deps.workflowClient.startRigVerification({
|
|
3589
|
-
workspaceId:
|
|
3749
|
+
workspaceId: rig.workspaceId,
|
|
3590
3750
|
changeId: change.id,
|
|
3591
3751
|
workflowId: `rig-verification-change-${change.id}-attempt-${attempt}`,
|
|
3592
3752
|
});
|
|
@@ -3651,18 +3811,14 @@ function registerRigTools(
|
|
|
3651
3811
|
},
|
|
3652
3812
|
},
|
|
3653
3813
|
async ({ rigId, changeId }) => {
|
|
3654
|
-
const rig = await
|
|
3814
|
+
const rig = await requireMutableRig(rigId);
|
|
3655
3815
|
if (changeId) {
|
|
3656
|
-
const change = await requireRigChangeForApi(deps.db,
|
|
3657
|
-
const verifying = await beginMcpRigVerificationAttempt(
|
|
3658
|
-
deps,
|
|
3659
|
-
grant.workspaceId,
|
|
3660
|
-
change.id,
|
|
3661
|
-
);
|
|
3816
|
+
const change = await requireRigChangeForApi(deps.db, rig.workspaceId, rig.id, changeId);
|
|
3817
|
+
const verifying = await beginMcpRigVerificationAttempt(deps, rig.workspaceId, change.id);
|
|
3662
3818
|
const attempt = verificationAttempt(verifying);
|
|
3663
3819
|
try {
|
|
3664
3820
|
await deps.workflowClient.startRigVerification({
|
|
3665
|
-
workspaceId:
|
|
3821
|
+
workspaceId: rig.workspaceId,
|
|
3666
3822
|
changeId: change.id,
|
|
3667
3823
|
workflowId: `rig-verification-change-${change.id}-attempt-${attempt}`,
|
|
3668
3824
|
});
|
|
@@ -3721,7 +3877,7 @@ function registerRigTools(
|
|
|
3721
3877
|
throw new Error("rig has no active version");
|
|
3722
3878
|
}
|
|
3723
3879
|
await deps.workflowClient.startRigVerification({
|
|
3724
|
-
workspaceId:
|
|
3880
|
+
workspaceId: rig.workspaceId,
|
|
3725
3881
|
versionId: rig.activeVersion.id,
|
|
3726
3882
|
workflowId: `rig-verification-version-${rig.activeVersion.id}-${crypto.randomUUID()}`,
|
|
3727
3883
|
});
|
|
@@ -3758,11 +3914,12 @@ function registerRigTools(
|
|
|
3758
3914
|
},
|
|
3759
3915
|
},
|
|
3760
3916
|
async ({ rigId, changeId }) => {
|
|
3761
|
-
const rig = await
|
|
3762
|
-
const change = await requireRigChangeForApi(deps.db,
|
|
3917
|
+
const rig = await requireMutableRig(rigId);
|
|
3918
|
+
const change = await requireRigChangeForApi(deps.db, rig.workspaceId, rig.id, changeId);
|
|
3919
|
+
const originGrant = { ...(await resourceGrant()), workspaceId: rig.workspaceId };
|
|
3763
3920
|
const promoted = await promoteVerifiedDefinitionEditChangeForApi(
|
|
3764
3921
|
{ db: deps.db },
|
|
3765
|
-
|
|
3922
|
+
originGrant,
|
|
3766
3923
|
rig,
|
|
3767
3924
|
change,
|
|
3768
3925
|
);
|
|
@@ -3841,7 +3998,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3841
3998
|
server.registerTool(
|
|
3842
3999
|
"sessions_list",
|
|
3843
4000
|
{
|
|
3844
|
-
description: `List compact high-level session status in this workspace. Defaults to creation order; use orderBy=updatedAt with decimal activity-revision updatedAfter/updatedThrough tokens for gap-free indexed incremental monitoring independent of application clocks. Cursors are opaque revision-fenced keysets. includeLastMessage is opt-in
|
|
4001
|
+
description: `List compact high-level session status in this workspace. Defaults to creation order; use orderBy=updatedAt with decimal activity-revision updatedAfter/updatedThrough tokens for gap-free indexed incremental monitoring independent of application clocks. Cursors are opaque revision-fenced keysets. includeLastMessage is opt-in and never previews a human/API prompt whose turn was never claimed (still queued, or deleted/edited/cancelled before any claim): waiting work is represented by queuedPromptCount until the turn is claimed. Rendered previews share a deterministic ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 aggregate budget, and omitted previews include a bounded session_events drill-down input (exact message type, direction=before, limit=1, monitoring summary). Use session_get for exact known targets and detailed resources/tools/settings. The list never returns full session objects or history.`,
|
|
3845
4002
|
inputSchema: {
|
|
3846
4003
|
limit: z4.number().int().positive().max(100).optional(),
|
|
3847
4004
|
cursor: z4.string().max(512).optional(),
|
|
@@ -3888,7 +4045,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3888
4045
|
"session_get",
|
|
3889
4046
|
{
|
|
3890
4047
|
description:
|
|
3891
|
-
"Get another session you are managing: status, goal-bearing metadata, resources,
|
|
4048
|
+
"Get another session you are managing: status, goal-bearing metadata, resources, persisted tool refs, exact bounded effectiveToolPolicy, and variableSet attachment (names/ids only, never variable values). effectiveToolPolicy distinguishes selected refs from workspace defaults, mandatory carriers, configured/deferred refs, and dropped refs. Do not call this with your own current session id to reconstruct context; your model-facing conversation history and persistent setting state are already supplied directly. Unbounded agent-set fields are clamped so monitoring another session cannot flood this context.",
|
|
3892
4049
|
inputSchema: { sessionId: z4.string().uuid() },
|
|
3893
4050
|
},
|
|
3894
4051
|
async ({ sessionId }) => {
|
|
@@ -3925,7 +4082,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3925
4082
|
"session_events",
|
|
3926
4083
|
{
|
|
3927
4084
|
description:
|
|
3928
|
-
"Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Use `latest` as an exclusive lookup for the authoritative newest durable sequence in exactly one semantic class; `receipt` is the concise alias for `tool_receipt`, and latest cannot be combined with type or class filters. Add `resultMode=compact` to latest for one bounded result-bearing completion/checkpoint/receipt without another inference. Use nextBefore to page older or explicit after/nextAfter to page forward. Type/class filters run in the RLS-scoped database query. payloadMode none|summary|full controls retained audit payload projection, but every model result is independently byte-capped with explicit truncation and exact covered sequence bounds. Exact retained forensic payloads require the access-controlled REST/SDK events API with mode=forensic&payloadMode=full; generic source bytes never retained by the audit boundary remain unavailable.",
|
|
4085
|
+
"Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Monitoring mode also omits a human/API user.message whose turn was never claimed (still queued, or deleted/edited/cancelled before any claim; waiting work is represented by sessions_list queuedPromptCount); the row appears at its own sequence once the turn is claimed. Use `latest` as an exclusive lookup for the authoritative newest durable sequence in exactly one semantic class; `receipt` is the concise alias for `tool_receipt`, and latest cannot be combined with type or class filters. Add `resultMode=compact` to latest for one bounded result-bearing completion/checkpoint/receipt without another inference. Use nextBefore to page older or explicit after/nextAfter to page forward. Type/class filters run in the RLS-scoped database query. payloadMode none|summary|full controls retained audit payload projection, but every model result is independently byte-capped with explicit truncation and exact covered sequence bounds. Exact retained forensic payloads, including unclaimed prompt rows, require mode=forensic or the access-controlled REST/SDK events API with mode=forensic&payloadMode=full; generic source bytes never retained by the audit boundary remain unavailable.",
|
|
3929
4086
|
inputSchema: {
|
|
3930
4087
|
sessionId: z4.string().uuid(),
|
|
3931
4088
|
after: z4.number().int().nonnegative().optional(),
|
|
@@ -3998,6 +4155,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
3998
4155
|
includeClasses: latestClass ? [latestClass] : (includeClasses ?? []),
|
|
3999
4156
|
excludeClasses: excludeClasses ?? [],
|
|
4000
4157
|
...(mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {}),
|
|
4158
|
+
...(mode === "monitoring" ? { excludeUnclaimedHumanPrompts: true } : {}),
|
|
4001
4159
|
...(latestClass ? { authoritativeLatest: true } : {}),
|
|
4002
4160
|
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
4003
4161
|
});
|
|
@@ -4032,13 +4190,106 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4032
4190
|
);
|
|
4033
4191
|
},
|
|
4034
4192
|
);
|
|
4193
|
+
|
|
4194
|
+
server.registerTool(
|
|
4195
|
+
"session_wait",
|
|
4196
|
+
{
|
|
4197
|
+
description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session); returns immediately when anything already changed. Only turn lifecycle, agent.message.completed, blocking failures, goal facts, and session status/control changes count as a change; raw deltas, tool receipts, and sandbox diagnostics never wake it. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
|
|
4198
|
+
inputSchema: {
|
|
4199
|
+
targets: z4
|
|
4200
|
+
.array(
|
|
4201
|
+
z4.object({
|
|
4202
|
+
sessionId: z4.string().uuid(),
|
|
4203
|
+
afterSequence: z4.number().int().nonnegative(),
|
|
4204
|
+
}),
|
|
4205
|
+
)
|
|
4206
|
+
.min(1)
|
|
4207
|
+
.max(SESSION_WAIT_MAX_TARGETS),
|
|
4208
|
+
includeOwnPendingUpdates: z4
|
|
4209
|
+
.boolean()
|
|
4210
|
+
.optional()
|
|
4211
|
+
.describe(
|
|
4212
|
+
"Also return when your own session has pending machine input (default true).",
|
|
4213
|
+
),
|
|
4214
|
+
maxWaitSeconds: z4.number().int().min(1).max(SESSION_WAIT_MAX_SECONDS).optional(),
|
|
4215
|
+
},
|
|
4216
|
+
},
|
|
4217
|
+
async ({ targets, includeOwnPendingUpdates, maxWaitSeconds }, extra) => {
|
|
4218
|
+
const distinct = new Set(targets.map((target) => target.sessionId));
|
|
4219
|
+
if (distinct.size !== targets.length) {
|
|
4220
|
+
throw new Error("session_wait targets must name distinct sessions");
|
|
4221
|
+
}
|
|
4222
|
+
// Authorize and resolve every target exactly as session_events does,
|
|
4223
|
+
// before any live-fanout subscription exists for it.
|
|
4224
|
+
for (const target of targets) {
|
|
4225
|
+
await authorizeFirstPartySession(deps, grant, target.sessionId, "session.events.read");
|
|
4226
|
+
await requireSession(deps.db, grant.workspaceId, target.sessionId);
|
|
4227
|
+
}
|
|
4228
|
+
const ownSessionId = includeOwnPendingUpdates === false ? null : callerSessionId;
|
|
4229
|
+
// The API serves one transport per POST, so the worker's MCP cancel
|
|
4230
|
+
// notification never reaches this handler; the route binds the HTTP
|
|
4231
|
+
// request's abort to transport.close() (mcp/request-abort.ts), which
|
|
4232
|
+
// aborts `extra.signal` when the worker drops the call on Steer/Pause.
|
|
4233
|
+
// The deadline bounds the wait regardless. Direct handler invocation
|
|
4234
|
+
// (tests) may pass no extra at all.
|
|
4235
|
+
const signal: AbortSignal | undefined = extra?.signal;
|
|
4236
|
+
const workspaceId = grant.workspaceId;
|
|
4237
|
+
// A NATS subscription is live fanout only; the durable session_events
|
|
4238
|
+
// read below is the authority. Bun.serve idleTimeout (255 s) and the
|
|
4239
|
+
// 60 s MCP client request timeout both exceed the 50 s cap.
|
|
4240
|
+
return json(
|
|
4241
|
+
await waitForSessionChanges({
|
|
4242
|
+
targets,
|
|
4243
|
+
ownSessionId,
|
|
4244
|
+
maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
|
|
4245
|
+
signal,
|
|
4246
|
+
source: {
|
|
4247
|
+
reauthorizeTargets: async (sessionIds) => {
|
|
4248
|
+
for (const targetSessionId of sessionIds) {
|
|
4249
|
+
await authorizeFirstPartySession(
|
|
4250
|
+
deps,
|
|
4251
|
+
grant,
|
|
4252
|
+
targetSessionId,
|
|
4253
|
+
"session.events.read",
|
|
4254
|
+
);
|
|
4255
|
+
}
|
|
4256
|
+
},
|
|
4257
|
+
readTargetEvents: async (target) => {
|
|
4258
|
+
const page = await listSessionEventPage(deps.db, workspaceId, target.sessionId, {
|
|
4259
|
+
after: target.afterSequence,
|
|
4260
|
+
direction: "after",
|
|
4261
|
+
limit: SESSION_WAIT_EVENTS_PER_TARGET,
|
|
4262
|
+
payloadMode: "full",
|
|
4263
|
+
includeTypes: SESSION_WAIT_EVENT_TYPES,
|
|
4264
|
+
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
4265
|
+
});
|
|
4266
|
+
return { events: page.events, hasMore: page.hasMore };
|
|
4267
|
+
},
|
|
4268
|
+
readOwnPendingUpdateKinds:
|
|
4269
|
+
ownSessionId === null
|
|
4270
|
+
? null
|
|
4271
|
+
: async () =>
|
|
4272
|
+
(
|
|
4273
|
+
await listOutstandingSessionSystemUpdates(
|
|
4274
|
+
deps.db,
|
|
4275
|
+
workspaceId,
|
|
4276
|
+
ownSessionId,
|
|
4277
|
+
)
|
|
4278
|
+
).map((update) => update.kind),
|
|
4279
|
+
subscribe: (targetSessionId, onEvents) =>
|
|
4280
|
+
deps.bus.subscribe(workspaceId, targetSessionId, onEvents),
|
|
4281
|
+
},
|
|
4282
|
+
}),
|
|
4283
|
+
);
|
|
4284
|
+
},
|
|
4285
|
+
);
|
|
4035
4286
|
}
|
|
4036
4287
|
|
|
4037
4288
|
if (can("sessions:create") && sessionCreateVisible) {
|
|
4038
4289
|
const sessionCreateInput = z4
|
|
4039
4290
|
.object({
|
|
4040
4291
|
initialMessage: z4.string().min(1),
|
|
4041
|
-
instructions: z4.string().min(1).max(
|
|
4292
|
+
instructions: z4.string().min(1).max(SESSION_INSTRUCTIONS_MAX_CHARACTERS).optional(),
|
|
4042
4293
|
goal: z4.unknown().optional(),
|
|
4043
4294
|
resources: z4.array(z4.unknown()).optional(),
|
|
4044
4295
|
tools: z4.array(z4.unknown()).optional(),
|
|
@@ -4099,7 +4350,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4099
4350
|
"session_create",
|
|
4100
4351
|
{
|
|
4101
4352
|
description:
|
|
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
|
|
4353
|
+
"Spawn a new agent session (a worker). The child inherits this session's visibility; a private session can only create a same-owner private child. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. 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
4354
|
inputSchema: sessionCreateInput,
|
|
4104
4355
|
},
|
|
4105
4356
|
async (args) => {
|
|
@@ -4211,7 +4462,11 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4211
4462
|
},
|
|
4212
4463
|
relatedResources: [
|
|
4213
4464
|
{ type: "session", id: targetSessionId },
|
|
4214
|
-
{
|
|
4465
|
+
{
|
|
4466
|
+
type: "session_event",
|
|
4467
|
+
id: accepted.id,
|
|
4468
|
+
state: accepted.type,
|
|
4469
|
+
},
|
|
4215
4470
|
],
|
|
4216
4471
|
timestamp: accepted.occurredAt,
|
|
4217
4472
|
idempotency: { status: replay ? "replayed" : "applied" },
|
|
@@ -4404,6 +4659,85 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4404
4659
|
);
|
|
4405
4660
|
},
|
|
4406
4661
|
);
|
|
4662
|
+
|
|
4663
|
+
server.registerTool(
|
|
4664
|
+
"session_human_input_respond",
|
|
4665
|
+
{
|
|
4666
|
+
description:
|
|
4667
|
+
"Answer (or skip, when the request allows it) a structured human-input request that another session is blocked on, typically a worker you spawned after a `child_requires_action` update named its requestId. Use it only when you actually know the answer; otherwise report the exact blocker to the user. Tool approvals are decided by humans only and cannot be answered here. The response is recorded as an agent-attempt answer and resumes that session's blocked turn.",
|
|
4668
|
+
inputSchema: {
|
|
4669
|
+
sessionId: z4.string().uuid(),
|
|
4670
|
+
requestId: z4.string().uuid(),
|
|
4671
|
+
response: z4.unknown(),
|
|
4672
|
+
idempotencyKey: z4.string().uuid(),
|
|
4673
|
+
},
|
|
4674
|
+
},
|
|
4675
|
+
async ({ sessionId, requestId, response, idempotencyKey }) => {
|
|
4676
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.human_input.write");
|
|
4677
|
+
const context = exactAgentCommandContext(grant, callerSessionId, "first_party_mcp");
|
|
4678
|
+
const parsedResponse = SubmitHumanInputResponseRequest.parse(response);
|
|
4679
|
+
let accepted: Awaited<ReturnType<typeof acceptSessionHumanInputResponse>>;
|
|
4680
|
+
try {
|
|
4681
|
+
accepted = await acceptSessionHumanInputResponse(deps.db, {
|
|
4682
|
+
accountId: grant.accountId,
|
|
4683
|
+
workspaceId: grant.workspaceId,
|
|
4684
|
+
sessionId,
|
|
4685
|
+
requestId,
|
|
4686
|
+
response: parsedResponse,
|
|
4687
|
+
respondedBy: `agent_attempt:${context.callerAttemptId}`,
|
|
4688
|
+
respondedByKind: "agent_attempt",
|
|
4689
|
+
clientEventId: idempotencyKey,
|
|
4690
|
+
});
|
|
4691
|
+
} catch (error) {
|
|
4692
|
+
if (error instanceof HumanInputResponseValidationError) {
|
|
4693
|
+
throw new Error(`human-input response rejected: ${error.message}`, { cause: error });
|
|
4694
|
+
}
|
|
4695
|
+
throw error;
|
|
4696
|
+
}
|
|
4697
|
+
if (accepted.action === "not_found") {
|
|
4698
|
+
throw new Error("human-input request not found");
|
|
4699
|
+
}
|
|
4700
|
+
await publishDurableSessionEvents(
|
|
4701
|
+
deps.bus,
|
|
4702
|
+
grant.workspaceId,
|
|
4703
|
+
sessionId,
|
|
4704
|
+
accepted.events,
|
|
4705
|
+
);
|
|
4706
|
+
if (accepted.workflowWakeRevision !== null) {
|
|
4707
|
+
await deps.workflowClient.signalApprovalDecision({
|
|
4708
|
+
accountId: grant.accountId,
|
|
4709
|
+
workspaceId: grant.workspaceId,
|
|
4710
|
+
sessionId,
|
|
4711
|
+
eventId: accepted.events[0]?.id ?? requestId,
|
|
4712
|
+
workflowId: `session-${sessionId}`,
|
|
4713
|
+
workflowWakeRevision: accepted.workflowWakeRevision,
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
if (accepted.action === "conflict") {
|
|
4717
|
+
throw new Error(`human-input request is ${accepted.request.status}`);
|
|
4718
|
+
}
|
|
4719
|
+
return json(
|
|
4720
|
+
mcpMutationReceipt({
|
|
4721
|
+
operation: "session_human_input_respond",
|
|
4722
|
+
committed: true,
|
|
4723
|
+
outcome: accepted.events.length === 0 ? "replayed" : "updated",
|
|
4724
|
+
changed: accepted.events.length > 0,
|
|
4725
|
+
resource: {
|
|
4726
|
+
type: "session_human_input_request",
|
|
4727
|
+
id: requestId,
|
|
4728
|
+
state: accepted.request.status,
|
|
4729
|
+
},
|
|
4730
|
+
relatedResources: [{ type: "session", id: sessionId }],
|
|
4731
|
+
timestamp: accepted.request.respondedAt ?? new Date().toISOString(),
|
|
4732
|
+
idempotency: {
|
|
4733
|
+
status: accepted.events.length === 0 ? "replayed" : "applied",
|
|
4734
|
+
},
|
|
4735
|
+
facts: { outcome: accepted.request.response?.outcome ?? null },
|
|
4736
|
+
nextAction: { tool: "session_get", arguments: { sessionId } },
|
|
4737
|
+
}),
|
|
4738
|
+
);
|
|
4739
|
+
},
|
|
4740
|
+
);
|
|
4407
4741
|
}
|
|
4408
4742
|
|
|
4409
4743
|
server.registerTool(
|
|
@@ -4441,6 +4775,11 @@ function registerVariableSetTools(
|
|
|
4441
4775
|
sessionId: string | null,
|
|
4442
4776
|
json: JsonResult,
|
|
4443
4777
|
): void {
|
|
4778
|
+
const variableSetAccess = {
|
|
4779
|
+
accountId: grant.accountId,
|
|
4780
|
+
workspaceId: grant.workspaceId,
|
|
4781
|
+
subjectId: grant.subjectId,
|
|
4782
|
+
};
|
|
4444
4783
|
const registerListTool = (name: string, description: string): void => {
|
|
4445
4784
|
server.registerTool(
|
|
4446
4785
|
name,
|
|
@@ -4449,7 +4788,7 @@ function registerVariableSetTools(
|
|
|
4449
4788
|
inputSchema: {},
|
|
4450
4789
|
},
|
|
4451
4790
|
async () => {
|
|
4452
|
-
const variableSets = await listVariableSets(deps.db,
|
|
4791
|
+
const variableSets = await listVariableSets(deps.db, variableSetAccess);
|
|
4453
4792
|
return json({ variableSets, environments: variableSets });
|
|
4454
4793
|
},
|
|
4455
4794
|
);
|
|
@@ -4491,14 +4830,14 @@ function registerVariableSetTools(
|
|
|
4491
4830
|
let created = false;
|
|
4492
4831
|
let variableSet =
|
|
4493
4832
|
targetId !== undefined
|
|
4494
|
-
? await getVariableSet(deps.db,
|
|
4495
|
-
: await getVariableSetByName(deps.db,
|
|
4833
|
+
? await getVariableSet(deps.db, variableSetAccess, targetId)
|
|
4834
|
+
: await getVariableSetByName(deps.db, variableSetAccess, trimmedVariableSetName!);
|
|
4496
4835
|
if (!variableSet && targetId !== undefined) {
|
|
4497
4836
|
throw new Error("variable set/environment not found");
|
|
4498
4837
|
}
|
|
4499
4838
|
if (!variableSet) {
|
|
4500
4839
|
if (
|
|
4501
|
-
(await countVariableSets(deps.db,
|
|
4840
|
+
(await countVariableSets(deps.db, variableSetAccess)) >= MAX_ENVIRONMENTS_PER_WORKSPACE
|
|
4502
4841
|
) {
|
|
4503
4842
|
throw new Error(
|
|
4504
4843
|
`a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE} variable sets`,
|
|
@@ -4507,6 +4846,8 @@ function registerVariableSetTools(
|
|
|
4507
4846
|
variableSet = await createVariableSet(deps.db, {
|
|
4508
4847
|
accountId: grant.accountId,
|
|
4509
4848
|
workspaceId: grant.workspaceId,
|
|
4849
|
+
scope: "workspace",
|
|
4850
|
+
subjectId: grant.subjectId,
|
|
4510
4851
|
name: trimmedVariableSetName!,
|
|
4511
4852
|
});
|
|
4512
4853
|
created = true;
|
|
@@ -4525,6 +4866,7 @@ function registerVariableSetTools(
|
|
|
4525
4866
|
const metadata = await setVariableSetVariable(deps.db, {
|
|
4526
4867
|
accountId: grant.accountId,
|
|
4527
4868
|
workspaceId: grant.workspaceId,
|
|
4869
|
+
subjectId: grant.subjectId,
|
|
4528
4870
|
variableSetId: variableSet.id,
|
|
4529
4871
|
name: parsedName.data,
|
|
4530
4872
|
valueEncrypted: encryptVariableSetValue(key, value),
|
|
@@ -4982,7 +5324,8 @@ function registerGitHubConnectTool(
|
|
|
4982
5324
|
}
|
|
4983
5325
|
|
|
4984
5326
|
// Defense-in-depth for invariant "agents cannot self-attach": the worker's
|
|
4985
|
-
// first-party delegated token
|
|
5327
|
+
// first-party delegated token must carry both exact attachment and use
|
|
5328
|
+
// permissions, so a token narrowed to only one cannot attach a variable set.
|
|
4986
5329
|
// agents calling these MCP tools cannot attach a variable set.
|
|
4987
5330
|
// Explicit detach (variableSetId: null) is also an attachment change and is
|
|
4988
5331
|
// blocked the same way.
|
|
@@ -4990,12 +5333,23 @@ function requireVariableSetsUseForMcpAttachment(
|
|
|
4990
5333
|
grant: AccessGrant,
|
|
4991
5334
|
variableSetId: string | null | undefined,
|
|
4992
5335
|
): void {
|
|
4993
|
-
if (variableSetId
|
|
5336
|
+
if (variableSetId === undefined) return;
|
|
5337
|
+
if (!hasPermission(grant.permissions, "variable-sets:attach")) {
|
|
5338
|
+
throw new Error("missing permission: variable-sets:attach");
|
|
5339
|
+
}
|
|
5340
|
+
if (variableSetId !== null && !hasPermission(grant.permissions, "variable-sets:use")) {
|
|
4994
5341
|
throw new Error("missing permission: variable-sets:use");
|
|
4995
5342
|
}
|
|
4996
5343
|
}
|
|
4997
5344
|
|
|
4998
|
-
|
|
5345
|
+
/**
|
|
5346
|
+
* Project one allowlisted GitHub App repository into the resource an agent or
|
|
5347
|
+
* scheduled task attaches. Every listed repository is in the workspace
|
|
5348
|
+
* allowlist, public or private, so every resource carries the stable ids
|
|
5349
|
+
* that mint the scoped installation token; a bare URI would clone anonymously
|
|
5350
|
+
* and could never push.
|
|
5351
|
+
*/
|
|
5352
|
+
export function repositoryWithScheduledTaskResource(
|
|
4999
5353
|
repository: GitHubRepository,
|
|
5000
5354
|
): GitHubRepository & { resource: ResourceRef } {
|
|
5001
5355
|
const uri = normalizedRepositoryUri(repository.cloneUrl);
|
|
@@ -5007,12 +5361,8 @@ function repositoryWithScheduledTaskResource(
|
|
|
5007
5361
|
ref: repository.defaultBranch,
|
|
5008
5362
|
provider: "github",
|
|
5009
5363
|
mountPath: defaultRepositoryMountPath(uri, "github"),
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
githubInstallationId: repository.installationId,
|
|
5013
|
-
githubRepositoryId: repository.id,
|
|
5014
|
-
}
|
|
5015
|
-
: {}),
|
|
5364
|
+
githubInstallationId: repository.installationId,
|
|
5365
|
+
githubRepositoryId: repository.id,
|
|
5016
5366
|
},
|
|
5017
5367
|
};
|
|
5018
5368
|
}
|