@opengeni/api-router 2.6.4 → 2.10.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/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
CreateScheduledTaskRequest,
|
|
4
|
+
boundSessionMcpText as capSessionDiscoveryText,
|
|
5
|
+
compactSessionMcpListRow,
|
|
6
|
+
sessionMcpIncludesRelatedWork,
|
|
4
7
|
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
5
8
|
defaultRepositoryMountPath,
|
|
6
9
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
@@ -30,10 +33,13 @@ import {
|
|
|
30
33
|
type SessionAuthorizationSurface,
|
|
31
34
|
type Session,
|
|
32
35
|
type WorkspaceMemoryPromptMode,
|
|
36
|
+
type WorkspaceArtifactMutationResponse,
|
|
33
37
|
type ScheduledTask,
|
|
34
38
|
UpdateScheduledTaskRequest,
|
|
35
39
|
normalizeWorkspaceArtifactSlug,
|
|
36
40
|
WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES,
|
|
41
|
+
WORKSPACE_ARTIFACT_REQUESTED_TOOLS_MAX,
|
|
42
|
+
WORKSPACE_ARTIFACT_SOURCE_MAX_FILES,
|
|
37
43
|
SESSION_GOAL_PROGRESS_MAX_BYTES,
|
|
38
44
|
SESSION_GOAL_RATIONALE_MAX_BYTES,
|
|
39
45
|
SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
|
|
@@ -67,6 +73,7 @@ import {
|
|
|
67
73
|
encryptVariableSetValue,
|
|
68
74
|
getSession,
|
|
69
75
|
getSessionGoal,
|
|
76
|
+
getSessionMcpMonitoringSummary,
|
|
70
77
|
getSessionQueueSnapshot,
|
|
71
78
|
getSessionTurn,
|
|
72
79
|
getOrCreatePreferenceRegistrySnapshot,
|
|
@@ -101,9 +108,9 @@ import {
|
|
|
101
108
|
serializeEffectiveSessionControl,
|
|
102
109
|
setSessionGoalStatusWithEvent,
|
|
103
110
|
recordSessionGoalProgressWithEvent,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
waitForSessionInputWithEvent,
|
|
112
|
+
SESSION_INPUT_WAIT_MAX_SECONDS,
|
|
113
|
+
SESSION_INPUT_WAIT_MIN_SECONDS,
|
|
107
114
|
setVariableSetVariable,
|
|
108
115
|
updateSessionGoalWithEvent,
|
|
109
116
|
upsertSessionGoalWithEvent,
|
|
@@ -114,6 +121,7 @@ import {
|
|
|
114
121
|
listWorkspaceArtifacts,
|
|
115
122
|
publishWorkspaceArtifactVersion,
|
|
116
123
|
rollbackWorkspaceArtifact,
|
|
124
|
+
setWorkspaceArtifactStatus,
|
|
117
125
|
archiveTaskNote,
|
|
118
126
|
createTaskNote,
|
|
119
127
|
listTaskNotes,
|
|
@@ -154,6 +162,7 @@ import {
|
|
|
154
162
|
requireLiveAgentAttemptAuthorization,
|
|
155
163
|
requireSessionAuthorization,
|
|
156
164
|
requireSessionAuthorizationListScope,
|
|
165
|
+
resolveWorkspaceCatalogSettings,
|
|
157
166
|
SessionAuthorizationDeniedError,
|
|
158
167
|
SessionAuthorizationUnavailableError,
|
|
159
168
|
saveWorkspaceMemoryWithSlackPublication,
|
|
@@ -162,7 +171,6 @@ import {
|
|
|
162
171
|
} from "@opengeni/core";
|
|
163
172
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
164
173
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
165
|
-
import { retryWhileMissing } from "@opengeni/storage";
|
|
166
174
|
import {
|
|
167
175
|
githubBindingStatus,
|
|
168
176
|
listWorkspaceGitHubInstallationBindings,
|
|
@@ -170,6 +178,12 @@ import {
|
|
|
170
178
|
} from "../github-access";
|
|
171
179
|
import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
|
|
172
180
|
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
181
|
+
import {
|
|
182
|
+
projectWorkspaceArtifactDetailProvenance,
|
|
183
|
+
projectWorkspaceArtifactMutationProvenance,
|
|
184
|
+
projectWorkspaceArtifactVersionProvenance,
|
|
185
|
+
redactWorkspaceArtifactListProvenance,
|
|
186
|
+
} from "../workspace-artifact-provenance";
|
|
173
187
|
import {
|
|
174
188
|
assertSocialConnectionProvider,
|
|
175
189
|
socialMentionsLive,
|
|
@@ -231,10 +245,12 @@ import {
|
|
|
231
245
|
type FleetServices,
|
|
232
246
|
type RunOnOp,
|
|
233
247
|
} from "@opengeni/core";
|
|
248
|
+
import { getSessionBackgroundCommand } from "@opengeni/db/session-background-commands";
|
|
234
249
|
import {
|
|
235
250
|
boundSessionEventCompactResult,
|
|
236
251
|
boundSessionEventMcpPage,
|
|
237
252
|
boundSessionDetailMcp,
|
|
253
|
+
boundSessionCompactDetailMcp,
|
|
238
254
|
boundRigDetailMcp,
|
|
239
255
|
SESSION_EVENT_MCP_MAX_BYTES,
|
|
240
256
|
} from "./session-view";
|
|
@@ -277,6 +293,10 @@ import { registerRememberTools } from "./remember";
|
|
|
277
293
|
import { mintSandboxCodemodeToken } from "@opengeni/runtime/sandbox";
|
|
278
294
|
import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
|
|
279
295
|
import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
|
|
296
|
+
import {
|
|
297
|
+
prepareWorkspaceArtifactContent,
|
|
298
|
+
readWorkspaceArtifactContent,
|
|
299
|
+
} from "../workspace-artifact-content";
|
|
280
300
|
|
|
281
301
|
export type McpServerOptions = {
|
|
282
302
|
// Origin of the HTTP request that reached the MCP route. Browser-oriented
|
|
@@ -424,7 +444,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
424
444
|
goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
425
445
|
goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
426
446
|
goal_progress: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
427
|
-
|
|
447
|
+
wait_for_input: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
428
448
|
goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
429
449
|
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
430
450
|
memory_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
@@ -498,6 +518,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
498
518
|
// Blocking wait inside a running turn: the live attempt's own session is the
|
|
499
519
|
// self target, so the tool exists only for session-scoped grants.
|
|
500
520
|
session_wait: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
521
|
+
command_wait: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
501
522
|
session_create: { allOf: ["sessions:create"] },
|
|
502
523
|
session_send_message: { allOf: ["sessions:control"] },
|
|
503
524
|
session_pause: { allOf: ["sessions:control"] },
|
|
@@ -617,6 +638,8 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
617
638
|
artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
618
639
|
artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
619
640
|
artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
641
|
+
artifacts_archive: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
642
|
+
artifacts_restore: { sessionRequired: true, allOf: ["artifacts:publish"] },
|
|
620
643
|
sandbox_file_publish: {
|
|
621
644
|
sessionRequired: true,
|
|
622
645
|
allOf: ["files:read", "files:upload"],
|
|
@@ -805,8 +828,11 @@ export function buildOpenGeniMcpServer(
|
|
|
805
828
|
},
|
|
806
829
|
);
|
|
807
830
|
}
|
|
808
|
-
//
|
|
809
|
-
|
|
831
|
+
// PolicyMcpServer applies each tool's own permission contract. Register this
|
|
832
|
+
// mixed group for every session so session-level wait_for_input remains
|
|
833
|
+
// available with sessions:control even when goals:manage is intentionally
|
|
834
|
+
// absent.
|
|
835
|
+
if (sessionId !== null) {
|
|
810
836
|
registerGoalTools(server, deps, grant, sessionId, json);
|
|
811
837
|
}
|
|
812
838
|
if (sessionId !== null && options.workspaceMemoryEnabled === true) {
|
|
@@ -1718,7 +1744,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1718
1744
|
async ({ id, triggerId }) => {
|
|
1719
1745
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1720
1746
|
if (task.action.kind === "agent_turn") {
|
|
1721
|
-
await validateScheduledTaskTarget({
|
|
1747
|
+
const targetSession = await validateScheduledTaskTarget({
|
|
1722
1748
|
db: deps.db,
|
|
1723
1749
|
sessionAuthorization: deps.sessionAuthorization,
|
|
1724
1750
|
authorizationSurface: "first_party_mcp",
|
|
@@ -1730,8 +1756,16 @@ export function buildOpenGeniMcpServer(
|
|
|
1730
1756
|
agentConfig: task.agentConfig,
|
|
1731
1757
|
missingTargetStatus: 404,
|
|
1732
1758
|
});
|
|
1759
|
+
const catalogSourceSettings = deps.catalogSourceSettings ?? deps.settings;
|
|
1760
|
+
const catalogSettings = (
|
|
1761
|
+
await resolveWorkspaceCatalogSettings(deps.db, catalogSourceSettings, {
|
|
1762
|
+
accountId: grant.accountId,
|
|
1763
|
+
workspaceId: grant.workspaceId,
|
|
1764
|
+
...(targetSession ? { retainedProductModelId: targetSession.model } : {}),
|
|
1765
|
+
})
|
|
1766
|
+
).settings;
|
|
1733
1767
|
await validateScheduledTaskMachineTarget({
|
|
1734
|
-
settings:
|
|
1768
|
+
settings: catalogSettings,
|
|
1735
1769
|
db: deps.db,
|
|
1736
1770
|
grant,
|
|
1737
1771
|
runMode: task.runMode,
|
|
@@ -2563,6 +2597,12 @@ function registerGoalTools(
|
|
|
2563
2597
|
SESSION_GOAL_PROGRESS_MAX_BYTES,
|
|
2564
2598
|
"goal progress note",
|
|
2565
2599
|
);
|
|
2600
|
+
const inputWaitReasonSchema = boundedGoalToolString(2 * 1024, "session input wait reason").refine(
|
|
2601
|
+
(value) => value.trim().length > 0,
|
|
2602
|
+
{
|
|
2603
|
+
message: "session input wait reason must not be blank",
|
|
2604
|
+
},
|
|
2605
|
+
);
|
|
2566
2606
|
server.registerTool(
|
|
2567
2607
|
"goal_set",
|
|
2568
2608
|
{
|
|
@@ -2721,38 +2761,36 @@ function registerGoalTools(
|
|
|
2721
2761
|
);
|
|
2722
2762
|
|
|
2723
2763
|
server.registerTool(
|
|
2724
|
-
"
|
|
2764
|
+
"wait_for_input",
|
|
2725
2765
|
{
|
|
2726
2766
|
description:
|
|
2727
|
-
"
|
|
2767
|
+
"End the current turn and wait out of turn for relevant session input. This is self-only and does not require a goal. Use it for long or uncertain waits instead of sleeping or repeatedly calling session_wait/command_wait. timeoutSeconds is a relative safety-wake duration; OpenGeni persists its absolute deadline, and timeout never cancels a background command. A human/API prompt, agent message or Steer, child terminal result, scheduled input, terminal background-command result, or the deadline wakes the session. Always end your turn immediately after the tool succeeds. Use goal_pause instead when the active goal itself should stop pending a human decision.",
|
|
2728
2768
|
inputSchema: {
|
|
2729
|
-
reason:
|
|
2730
|
-
|
|
2769
|
+
reason: inputWaitReasonSchema,
|
|
2770
|
+
timeoutSeconds: z4
|
|
2731
2771
|
.number()
|
|
2732
2772
|
.int()
|
|
2733
|
-
.min(
|
|
2734
|
-
.max(
|
|
2773
|
+
.min(SESSION_INPUT_WAIT_MIN_SECONDS)
|
|
2774
|
+
.max(SESSION_INPUT_WAIT_MAX_SECONDS),
|
|
2735
2775
|
idempotencyKey: z4.string().uuid().optional(),
|
|
2736
2776
|
},
|
|
2737
2777
|
},
|
|
2738
|
-
async ({ reason,
|
|
2739
|
-
await authorizeFirstPartySession(deps, grant, sessionId, "session.
|
|
2778
|
+
async ({ reason, timeoutSeconds, idempotencyKey }) => {
|
|
2779
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.control");
|
|
2740
2780
|
const context = exactAgentCommandContext(grant, sessionId);
|
|
2741
|
-
// Without a caller-supplied key the operation is idempotent per (goal
|
|
2742
|
-
// target, turn, exact arguments): a replacement attempt replaying the
|
|
2743
|
-
// same call receives the stored result instead of re-stamping the hold,
|
|
2744
|
-
// while a genuinely new same-turn wait (different reason/deadline) is a
|
|
2745
|
-
// new operation whose later hold wins.
|
|
2746
2781
|
const operationKey =
|
|
2747
2782
|
idempotencyKey ??
|
|
2748
|
-
`
|
|
2749
|
-
.update(JSON.stringify({ reason,
|
|
2783
|
+
`wait-for-input:${context.callerTurnId}:${createHash("sha256")
|
|
2784
|
+
.update(JSON.stringify({ reason, timeoutSeconds }))
|
|
2750
2785
|
.digest("hex")
|
|
2751
2786
|
.slice(0, 32)}`;
|
|
2752
|
-
const {
|
|
2753
|
-
|
|
2787
|
+
const { events, operationId, replay, deadlineAt } = await waitForSessionInputWithEvent(
|
|
2788
|
+
deps.db,
|
|
2789
|
+
grant.workspaceId,
|
|
2790
|
+
sessionId,
|
|
2791
|
+
{
|
|
2754
2792
|
reason,
|
|
2755
|
-
|
|
2793
|
+
timeoutSeconds,
|
|
2756
2794
|
command: {
|
|
2757
2795
|
accountId: grant.accountId,
|
|
2758
2796
|
actor: {
|
|
@@ -2764,16 +2802,16 @@ function registerGoalTools(
|
|
|
2764
2802
|
},
|
|
2765
2803
|
operationKey,
|
|
2766
2804
|
},
|
|
2767
|
-
}
|
|
2805
|
+
},
|
|
2806
|
+
);
|
|
2768
2807
|
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
2769
2808
|
return json({
|
|
2770
|
-
status: "
|
|
2771
|
-
|
|
2772
|
-
untilAt,
|
|
2809
|
+
status: "waiting_for_input",
|
|
2810
|
+
deadlineAt,
|
|
2773
2811
|
operationId,
|
|
2774
2812
|
replay,
|
|
2775
2813
|
nextAction:
|
|
2776
|
-
"End your turn now
|
|
2814
|
+
"End your turn now. Relevant input or the timeout deadline will start a new turn; the timeout does not cancel commands.",
|
|
2777
2815
|
});
|
|
2778
2816
|
},
|
|
2779
2817
|
);
|
|
@@ -2913,42 +2951,42 @@ function registerWorkspaceArtifactTools(
|
|
|
2913
2951
|
const authorize = async () => {
|
|
2914
2952
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
|
|
2915
2953
|
};
|
|
2916
|
-
const
|
|
2954
|
+
const canReadProvenanceSession = (sourceSessionId: string): Promise<boolean> =>
|
|
2955
|
+
authorizeFirstPartySession(deps, grant, sourceSessionId, "session.read")
|
|
2956
|
+
.then(() => true)
|
|
2957
|
+
.catch((error) => {
|
|
2958
|
+
if (error instanceof SessionAuthorizationDeniedError) return false;
|
|
2959
|
+
throw error;
|
|
2960
|
+
});
|
|
2961
|
+
const mutation = async (response: WorkspaceArtifactMutationResponse) =>
|
|
2962
|
+
await projectWorkspaceArtifactMutationProvenance(response, canReadProvenanceSession);
|
|
2963
|
+
const prepare = (
|
|
2964
|
+
html: string,
|
|
2965
|
+
source?: { entrypoint: string; files: Array<{ path: string; content: string }> },
|
|
2966
|
+
requestedTools?: Array<{ serverId: string; toolName: string }>,
|
|
2967
|
+
) => {
|
|
2917
2968
|
if (!deps.objectStorage) throw new Error("Object storage is not configured");
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
}
|
|
2924
|
-
const contentSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
2925
|
-
const contentKey = `workspaces/${grant.workspaceId}/workspace-artifacts/blobs/${contentSha256}.html`;
|
|
2926
|
-
return {
|
|
2927
|
-
contentKey,
|
|
2928
|
-
contentSha256,
|
|
2929
|
-
sizeBytes: bytes.byteLength,
|
|
2930
|
-
persistContent: async () => {
|
|
2931
|
-
await deps.objectStorage!.putObject({
|
|
2932
|
-
key: contentKey,
|
|
2933
|
-
contentType: "text/html; charset=utf-8",
|
|
2934
|
-
body: bytes,
|
|
2935
|
-
sha256: contentSha256,
|
|
2936
|
-
});
|
|
2937
|
-
},
|
|
2938
|
-
};
|
|
2969
|
+
return prepareWorkspaceArtifactContent(deps.objectStorage, grant.workspaceId, {
|
|
2970
|
+
html,
|
|
2971
|
+
...(source ? { source } : {}),
|
|
2972
|
+
...(requestedTools ? { requestedTools } : {}),
|
|
2973
|
+
});
|
|
2939
2974
|
};
|
|
2940
2975
|
const provenance = (
|
|
2941
2976
|
idempotencyKey: string,
|
|
2942
|
-
sourceToolName:
|
|
2977
|
+
sourceToolName:
|
|
2978
|
+
| "artifacts_create"
|
|
2979
|
+
| "artifacts_publish"
|
|
2980
|
+
| "artifacts_rollback"
|
|
2981
|
+
| "artifacts_archive"
|
|
2982
|
+
| "artifacts_restore",
|
|
2943
2983
|
) => {
|
|
2944
2984
|
const claims = attempt();
|
|
2945
2985
|
return {
|
|
2946
2986
|
accountId: grant.accountId,
|
|
2947
2987
|
workspaceId: grant.workspaceId,
|
|
2948
2988
|
operationKey: `attempt:${createHash("sha256")
|
|
2949
|
-
.update(
|
|
2950
|
-
`${claims.sessionId}:${claims.turnId}:${claims.attemptId}:${claims.executionGeneration}:${idempotencyKey}`,
|
|
2951
|
-
)
|
|
2989
|
+
.update(`${claims.sessionId}:${claims.turnId}:${idempotencyKey}`)
|
|
2952
2990
|
.digest("hex")}`,
|
|
2953
2991
|
actorSubjectId: grant.subjectId,
|
|
2954
2992
|
sourceSessionId: claims.sessionId,
|
|
@@ -2968,7 +3006,11 @@ function registerWorkspaceArtifactTools(
|
|
|
2968
3006
|
},
|
|
2969
3007
|
async () => {
|
|
2970
3008
|
await authorize();
|
|
2971
|
-
return json(
|
|
3009
|
+
return json(
|
|
3010
|
+
redactWorkspaceArtifactListProvenance(
|
|
3011
|
+
await listWorkspaceArtifacts(deps.db, grant.workspaceId),
|
|
3012
|
+
),
|
|
3013
|
+
);
|
|
2972
3014
|
},
|
|
2973
3015
|
);
|
|
2974
3016
|
|
|
@@ -2985,21 +3027,27 @@ function registerWorkspaceArtifactTools(
|
|
|
2985
3027
|
async ({ artifactId, versionId }) => {
|
|
2986
3028
|
await authorize();
|
|
2987
3029
|
if (!deps.objectStorage) throw new Error("Object storage is not configured");
|
|
2988
|
-
const [
|
|
3030
|
+
const [rawDetail, ref] = await Promise.all([
|
|
2989
3031
|
getWorkspaceArtifact(deps.db, grant.workspaceId, artifactId),
|
|
2990
3032
|
getWorkspaceArtifactContentRef(deps.db, grant.workspaceId, artifactId, versionId),
|
|
2991
3033
|
]);
|
|
2992
|
-
const
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3034
|
+
const sourceAuthorizations = new Map<string, Promise<boolean>>();
|
|
3035
|
+
const canReadSourceSession = (sourceSessionId: string): Promise<boolean> => {
|
|
3036
|
+
const existing = sourceAuthorizations.get(sourceSessionId);
|
|
3037
|
+
if (existing) return existing;
|
|
3038
|
+
const decision = canReadProvenanceSession(sourceSessionId);
|
|
3039
|
+
sourceAuthorizations.set(sourceSessionId, decision);
|
|
3040
|
+
return decision;
|
|
3041
|
+
};
|
|
3042
|
+
const [detail, projectedVersion] = await Promise.all([
|
|
3043
|
+
projectWorkspaceArtifactDetailProvenance(rawDetail, canReadSourceSession),
|
|
3044
|
+
projectWorkspaceArtifactVersionProvenance(ref.version, canReadSourceSession),
|
|
3045
|
+
]);
|
|
3046
|
+
const content = await readWorkspaceArtifactContent(deps.objectStorage, ref);
|
|
2999
3047
|
return json({
|
|
3000
3048
|
detail,
|
|
3001
|
-
version:
|
|
3002
|
-
|
|
3049
|
+
version: projectedVersion,
|
|
3050
|
+
...content,
|
|
3003
3051
|
});
|
|
3004
3052
|
},
|
|
3005
3053
|
);
|
|
@@ -3018,23 +3066,49 @@ function registerWorkspaceArtifactTools(
|
|
|
3018
3066
|
.max(96)
|
|
3019
3067
|
.optional(),
|
|
3020
3068
|
html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
|
|
3069
|
+
source: z4
|
|
3070
|
+
.object({
|
|
3071
|
+
entrypoint: z4.string().min(1).max(256),
|
|
3072
|
+
files: z4
|
|
3073
|
+
.array(
|
|
3074
|
+
z4.object({
|
|
3075
|
+
path: z4.string().min(1).max(256),
|
|
3076
|
+
content: z4.string(),
|
|
3077
|
+
}),
|
|
3078
|
+
)
|
|
3079
|
+
.min(1)
|
|
3080
|
+
.max(WORKSPACE_ARTIFACT_SOURCE_MAX_FILES),
|
|
3081
|
+
})
|
|
3082
|
+
.optional(),
|
|
3083
|
+
requestedTools: z4
|
|
3084
|
+
.array(
|
|
3085
|
+
z4.object({
|
|
3086
|
+
serverId: z4.string().min(1).max(256),
|
|
3087
|
+
toolName: z4.string().min(1).max(512),
|
|
3088
|
+
}),
|
|
3089
|
+
)
|
|
3090
|
+
.max(WORKSPACE_ARTIFACT_REQUESTED_TOOLS_MAX)
|
|
3091
|
+
.optional(),
|
|
3021
3092
|
idempotencyKey: z4.string().min(1).max(200),
|
|
3022
3093
|
},
|
|
3023
3094
|
},
|
|
3024
|
-
async ({ title, description, slug, html, idempotencyKey }) => {
|
|
3095
|
+
async ({ title, description, slug, html, source, requestedTools, idempotencyKey }) => {
|
|
3025
3096
|
await authorize();
|
|
3026
3097
|
const artifactId = crypto.randomUUID();
|
|
3027
3098
|
const slugBase = slug ?? (normalizeWorkspaceArtifactSlug(title) || "artifact");
|
|
3028
3099
|
const resolvedSlug = slug ?? `${slugBase.slice(0, 87)}-${artifactId.slice(0, 8)}`;
|
|
3029
3100
|
return json(
|
|
3030
|
-
await
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3101
|
+
await mutation(
|
|
3102
|
+
await createWorkspaceArtifact(deps.db, {
|
|
3103
|
+
artifactId,
|
|
3104
|
+
slug: resolvedSlug,
|
|
3105
|
+
requestedSlug: slug ?? null,
|
|
3106
|
+
title,
|
|
3107
|
+
description: description ?? null,
|
|
3108
|
+
...prepare(html, source, requestedTools),
|
|
3109
|
+
...provenance(idempotencyKey, "artifacts_create"),
|
|
3110
|
+
}),
|
|
3111
|
+
),
|
|
3038
3112
|
);
|
|
3039
3113
|
},
|
|
3040
3114
|
);
|
|
@@ -3050,20 +3124,54 @@ function registerWorkspaceArtifactTools(
|
|
|
3050
3124
|
title: z4.string().min(1).max(120).optional(),
|
|
3051
3125
|
description: z4.string().max(2000).nullable().optional(),
|
|
3052
3126
|
html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
|
|
3127
|
+
source: z4
|
|
3128
|
+
.object({
|
|
3129
|
+
entrypoint: z4.string().min(1).max(256),
|
|
3130
|
+
files: z4
|
|
3131
|
+
.array(
|
|
3132
|
+
z4.object({
|
|
3133
|
+
path: z4.string().min(1).max(256),
|
|
3134
|
+
content: z4.string(),
|
|
3135
|
+
}),
|
|
3136
|
+
)
|
|
3137
|
+
.min(1)
|
|
3138
|
+
.max(WORKSPACE_ARTIFACT_SOURCE_MAX_FILES),
|
|
3139
|
+
})
|
|
3140
|
+
.optional(),
|
|
3141
|
+
requestedTools: z4
|
|
3142
|
+
.array(
|
|
3143
|
+
z4.object({
|
|
3144
|
+
serverId: z4.string().min(1).max(256),
|
|
3145
|
+
toolName: z4.string().min(1).max(512),
|
|
3146
|
+
}),
|
|
3147
|
+
)
|
|
3148
|
+
.max(WORKSPACE_ARTIFACT_REQUESTED_TOOLS_MAX)
|
|
3149
|
+
.optional(),
|
|
3053
3150
|
idempotencyKey: z4.string().min(1).max(200),
|
|
3054
3151
|
},
|
|
3055
3152
|
},
|
|
3056
|
-
async ({
|
|
3153
|
+
async ({
|
|
3154
|
+
artifactId,
|
|
3155
|
+
expectedCurrentVersionId,
|
|
3156
|
+
title,
|
|
3157
|
+
description,
|
|
3158
|
+
html,
|
|
3159
|
+
source,
|
|
3160
|
+
requestedTools,
|
|
3161
|
+
idempotencyKey,
|
|
3162
|
+
}) => {
|
|
3057
3163
|
await authorize();
|
|
3058
3164
|
return json(
|
|
3059
|
-
await
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3165
|
+
await mutation(
|
|
3166
|
+
await publishWorkspaceArtifactVersion(deps.db, {
|
|
3167
|
+
artifactId,
|
|
3168
|
+
expectedCurrentVersionId,
|
|
3169
|
+
...(title !== undefined ? { title } : {}),
|
|
3170
|
+
...(description !== undefined ? { description } : {}),
|
|
3171
|
+
...prepare(html, source, requestedTools),
|
|
3172
|
+
...provenance(idempotencyKey, "artifacts_publish"),
|
|
3173
|
+
}),
|
|
3174
|
+
),
|
|
3067
3175
|
);
|
|
3068
3176
|
},
|
|
3069
3177
|
);
|
|
@@ -3084,13 +3192,71 @@ function registerWorkspaceArtifactTools(
|
|
|
3084
3192
|
async ({ artifactId, versionId, expectedCurrentVersionId, reason, idempotencyKey }) => {
|
|
3085
3193
|
await authorize();
|
|
3086
3194
|
return json(
|
|
3087
|
-
await
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3195
|
+
await mutation(
|
|
3196
|
+
await rollbackWorkspaceArtifact(deps.db, {
|
|
3197
|
+
artifactId,
|
|
3198
|
+
versionId,
|
|
3199
|
+
expectedCurrentVersionId,
|
|
3200
|
+
reason,
|
|
3201
|
+
...provenance(idempotencyKey, "artifacts_rollback"),
|
|
3202
|
+
}),
|
|
3203
|
+
),
|
|
3204
|
+
);
|
|
3205
|
+
},
|
|
3206
|
+
);
|
|
3207
|
+
|
|
3208
|
+
server.registerTool(
|
|
3209
|
+
"artifacts_archive",
|
|
3210
|
+
{
|
|
3211
|
+
description:
|
|
3212
|
+
"Unpublish an active workspace artifact without deleting its immutable versions or retained source.",
|
|
3213
|
+
inputSchema: {
|
|
3214
|
+
artifactId: z4.string().uuid(),
|
|
3215
|
+
expectedCurrentVersionId: z4.string().uuid(),
|
|
3216
|
+
reason: z4.string().min(1).max(4096),
|
|
3217
|
+
idempotencyKey: z4.string().min(1).max(200),
|
|
3218
|
+
},
|
|
3219
|
+
},
|
|
3220
|
+
async ({ artifactId, expectedCurrentVersionId, reason, idempotencyKey }) => {
|
|
3221
|
+
await authorize();
|
|
3222
|
+
return json(
|
|
3223
|
+
await mutation(
|
|
3224
|
+
await setWorkspaceArtifactStatus(deps.db, {
|
|
3225
|
+
artifactId,
|
|
3226
|
+
status: "archived",
|
|
3227
|
+
expectedCurrentVersionId,
|
|
3228
|
+
reason,
|
|
3229
|
+
...provenance(idempotencyKey, "artifacts_archive"),
|
|
3230
|
+
}),
|
|
3231
|
+
),
|
|
3232
|
+
);
|
|
3233
|
+
},
|
|
3234
|
+
);
|
|
3235
|
+
|
|
3236
|
+
server.registerTool(
|
|
3237
|
+
"artifacts_restore",
|
|
3238
|
+
{
|
|
3239
|
+
description:
|
|
3240
|
+
"Republish an archived workspace artifact at its unchanged current immutable version.",
|
|
3241
|
+
inputSchema: {
|
|
3242
|
+
artifactId: z4.string().uuid(),
|
|
3243
|
+
expectedCurrentVersionId: z4.string().uuid(),
|
|
3244
|
+
reason: z4.string().min(1).max(4096),
|
|
3245
|
+
idempotencyKey: z4.string().min(1).max(200),
|
|
3246
|
+
},
|
|
3247
|
+
},
|
|
3248
|
+
async ({ artifactId, expectedCurrentVersionId, reason, idempotencyKey }) => {
|
|
3249
|
+
await authorize();
|
|
3250
|
+
return json(
|
|
3251
|
+
await mutation(
|
|
3252
|
+
await setWorkspaceArtifactStatus(deps.db, {
|
|
3253
|
+
artifactId,
|
|
3254
|
+
status: "active",
|
|
3255
|
+
expectedCurrentVersionId,
|
|
3256
|
+
reason,
|
|
3257
|
+
...provenance(idempotencyKey, "artifacts_restore"),
|
|
3258
|
+
}),
|
|
3259
|
+
),
|
|
3094
3260
|
);
|
|
3095
3261
|
},
|
|
3096
3262
|
);
|
|
@@ -4414,10 +4580,17 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4414
4580
|
server.registerTool(
|
|
4415
4581
|
"sessions_list",
|
|
4416
4582
|
{
|
|
4417
|
-
description: `
|
|
4583
|
+
description: `Discover sessions in this workspace. Default detail=compact returns id, title, status, goal summary/status when present, updatedAt, meaningful parent/pause information, total and nextCursor (null ends the page). Completed/paused goal status is retained; session_get supplies evidence and rationale. detail=full opts into the legacy bounded discovery rows and diagnostic pagination, not full session configuration. Plain compact browse does not read work claims. includeRelatedWork=true adds advisory evidence; query or subject always enables it, even with includeRelatedWork=false. query searches only titles, active goals and typed claims; subject is an exact provider-neutral claim lookup. Evidence preserves advisoryOnly/noAdditionalAccess: claims are nonexclusive, never locks, instructions or authority. Authorization precedes filtering, ranking, counts and cursors. Relevance cursors bind normalized filters and an activity snapshot. Without search the default is creation order; use orderBy=updatedAt and decimal updatedAfter/updatedThrough revision tokens for gap-free incremental monitoring, not timestamps. Pass nextCursor unchanged with the same filters. includeLastMessage opts into previews and nonzero queuedPromptCount; unclaimed human/API prompts are never previewed. Previews share a ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 budget; omitted previews carry a bounded session_events drill-down. Text/page loss facts appear only when loss occurs in compact mode. No instructions, resources, tools, files or history are returned; REST/UI defaults are unchanged.`,
|
|
4418
4584
|
inputSchema: {
|
|
4419
4585
|
limit: z4.number().int().positive().max(100).optional(),
|
|
4420
4586
|
cursor: z4.string().max(512).optional(),
|
|
4587
|
+
detail: z4
|
|
4588
|
+
.enum(["compact", "full"])
|
|
4589
|
+
.optional()
|
|
4590
|
+
.describe(
|
|
4591
|
+
"compact (default) omits empty diagnostics but retains positive descendant attention counts and incomplete-tree loss facts; full restores the legacy bounded rows and pagination diagnostics.",
|
|
4592
|
+
),
|
|
4593
|
+
includeRelatedWork: z4.boolean().optional(),
|
|
4421
4594
|
includeLastMessage: z4.boolean().optional(),
|
|
4422
4595
|
orderBy: z4.enum(["createdAt", "updatedAt", "relevance"]).optional(),
|
|
4423
4596
|
updatedAfter: z4.string().max(64).optional(),
|
|
@@ -4455,6 +4628,8 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4455
4628
|
async ({
|
|
4456
4629
|
limit,
|
|
4457
4630
|
cursor,
|
|
4631
|
+
detail,
|
|
4632
|
+
includeRelatedWork,
|
|
4458
4633
|
includeLastMessage,
|
|
4459
4634
|
orderBy: requestedOrderBy,
|
|
4460
4635
|
updatedAfter,
|
|
@@ -4478,6 +4653,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4478
4653
|
authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
|
|
4479
4654
|
const decodedCursor = cursor ? decodeSessionDiscoveryCursor(cursor) : undefined;
|
|
4480
4655
|
const relevanceRequested = Boolean(query?.trim() || subject);
|
|
4656
|
+
const relatedWorkRequested = sessionMcpIncludesRelatedWork({
|
|
4657
|
+
detail,
|
|
4658
|
+
includeRelatedWork,
|
|
4659
|
+
query,
|
|
4660
|
+
subject,
|
|
4661
|
+
});
|
|
4481
4662
|
if (relevanceRequested && !deps.settings.workDiscoveryEnabled) {
|
|
4482
4663
|
observeWorkDiscovery(deps.observability, {
|
|
4483
4664
|
surface: "first_party_mcp",
|
|
@@ -4524,18 +4705,24 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4524
4705
|
...(parentSessionId !== undefined ? { parentSessionId } : {}),
|
|
4525
4706
|
...(subject ? { subject: subject as WorkClaimSubjectFilter } : {}),
|
|
4526
4707
|
...(claimLimit !== undefined ? { claimLimit } : {}),
|
|
4527
|
-
includeWorkDiscovery: deps.settings.workDiscoveryEnabled,
|
|
4708
|
+
includeWorkDiscovery: deps.settings.workDiscoveryEnabled && relatedWorkRequested,
|
|
4528
4709
|
subjectId: grant.subjectId,
|
|
4529
4710
|
...(authorizationScope ? { authorizationScope } : {}),
|
|
4530
4711
|
});
|
|
4531
|
-
const result =
|
|
4712
|
+
const result =
|
|
4713
|
+
detail === "full"
|
|
4714
|
+
? capSessionDiscoveryPage(page, includeLastMessage === true)
|
|
4715
|
+
: capSessionDiscoveryCompactPage(page, {
|
|
4716
|
+
includeLastMessage: includeLastMessage === true,
|
|
4717
|
+
includeRelatedWork: relatedWorkRequested,
|
|
4718
|
+
});
|
|
4532
4719
|
observeWorkDiscovery(deps.observability, {
|
|
4533
4720
|
surface: "first_party_mcp",
|
|
4534
4721
|
mode,
|
|
4535
4722
|
outcome: result.sessions.length === 0 ? "empty" : "ok",
|
|
4536
4723
|
authorizationScope: metricAuthorizationScope,
|
|
4537
4724
|
durationMs: performance.now() - startedAtMs,
|
|
4538
|
-
responseBytes: result
|
|
4725
|
+
responseBytes: Buffer.byteLength(JSON.stringify(result, null, 2), "utf8"),
|
|
4539
4726
|
...summarizeWorkDiscoveryRows(result.sessions),
|
|
4540
4727
|
});
|
|
4541
4728
|
return json(result);
|
|
@@ -4560,10 +4747,13 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4560
4747
|
"session_get",
|
|
4561
4748
|
{
|
|
4562
4749
|
description:
|
|
4563
|
-
"Get another session you are managing
|
|
4564
|
-
inputSchema: {
|
|
4750
|
+
"Get another session you are managing. Default detail=compact returns status, goal (including completion evidence or pause rationale), latest recorded goal progress, meaningful pause/wait state, queue counts, active turn and lastSequence for session_wait. Goal completion is not a terminal child result: join with session_wait waitFor=completion. Use detail=full for the legacy bounded configuration including resources, persisted tool refs, effectiveToolPolicy and variableSet ids (never variable values). Full mode is configuration, not a substitute for compact goal/progress facts. Do not use your own session id to reconstruct context; conversation history and persistent settings are supplied directly. Text loss is explicit; REST/UI defaults are unchanged.",
|
|
4751
|
+
inputSchema: {
|
|
4752
|
+
sessionId: z4.string().uuid(),
|
|
4753
|
+
detail: z4.enum(["compact", "full"]).optional(),
|
|
4754
|
+
},
|
|
4565
4755
|
},
|
|
4566
|
-
async ({ sessionId }) => {
|
|
4756
|
+
async ({ sessionId, detail }) => {
|
|
4567
4757
|
const authorization = await authorizeFirstPartySession(
|
|
4568
4758
|
deps,
|
|
4569
4759
|
grant,
|
|
@@ -4582,12 +4772,18 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4582
4772
|
},
|
|
4583
4773
|
authorization?.relatedSessionAccess ?? "root",
|
|
4584
4774
|
);
|
|
4775
|
+
if (detail !== "full") {
|
|
4776
|
+
return json(
|
|
4777
|
+
boundSessionCompactDetailMcp(
|
|
4778
|
+
projected,
|
|
4779
|
+
await getSessionMcpMonitoringSummary(deps.db, grant.workspaceId, sessionId),
|
|
4780
|
+
queue,
|
|
4781
|
+
),
|
|
4782
|
+
);
|
|
4783
|
+
}
|
|
4585
4784
|
return json(
|
|
4586
4785
|
boundSessionDetailMcp(
|
|
4587
|
-
await withMcpEffectivePolicy(deps, grant.workspaceId, grant.subjectId,
|
|
4588
|
-
...projected,
|
|
4589
|
-
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
|
|
4590
|
-
}),
|
|
4786
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, grant.subjectId, projected),
|
|
4591
4787
|
),
|
|
4592
4788
|
);
|
|
4593
4789
|
},
|
|
@@ -4709,7 +4905,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4709
4905
|
server.registerTool(
|
|
4710
4906
|
"session_wait",
|
|
4711
4907
|
{
|
|
4712
|
-
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
|
|
4908
|
+
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 a background-command result), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for one short wait inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works. Do not immediately repeat it after timedOut=true unless new evidence suggests completion within one more window; for long or uncertain waits call wait_for_input once and end the turn. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session). waitFor=change is the default and returns on turn lifecycle, agent.message.completed, terminal background commands, blocking failures, goal facts, or session status/control changes. waitFor=completion is the child-result join: it ignores progress, completed commentary messages, goal facts, background commands, maintenance turns, and continuation segment settlements and returns only for a result-bearing final turn or a blocking state. A goal.completed event records goal state but is not a terminal child result. Raw deltas, tool receipts, sandbox diagnostics, and unrelated progress never wake either mode. 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.`,
|
|
4713
4909
|
inputSchema: {
|
|
4714
4910
|
targets: z4
|
|
4715
4911
|
.array(
|
|
@@ -4809,6 +5005,127 @@ function registerWorkspaceOrchestrationTools(
|
|
|
4809
5005
|
);
|
|
4810
5006
|
},
|
|
4811
5007
|
);
|
|
5008
|
+
|
|
5009
|
+
if (callerSessionId !== null) {
|
|
5010
|
+
server.registerTool(
|
|
5011
|
+
"command_wait",
|
|
5012
|
+
{
|
|
5013
|
+
description: `Wait briefly for one background command owned by this session to settle, without provider-specific polling or write_stdin loops. The durable command row is authoritative. This returns immediately when the command is already terminal or otherwise waits up to maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) for its session.command.finished event or other pending input for this session. timedOut=true never cancels the command. Do not immediately repeat command_wait after a timeout unless new evidence suggests completion within one more window; for long or uncertain work call wait_for_input once and end the turn.`,
|
|
5014
|
+
inputSchema: {
|
|
5015
|
+
commandId: z4.string().uuid(),
|
|
5016
|
+
maxWaitSeconds: z4.number().int().min(1).max(SESSION_WAIT_MAX_SECONDS).optional(),
|
|
5017
|
+
},
|
|
5018
|
+
},
|
|
5019
|
+
async ({ commandId, maxWaitSeconds }, extra) => {
|
|
5020
|
+
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.events.read");
|
|
5021
|
+
const initialSession = await requireSession(deps.db, grant.workspaceId, callerSessionId);
|
|
5022
|
+
let command = await getSessionBackgroundCommand(deps.db, {
|
|
5023
|
+
accountId: grant.accountId,
|
|
5024
|
+
workspaceId: grant.workspaceId,
|
|
5025
|
+
sessionId: callerSessionId,
|
|
5026
|
+
commandId,
|
|
5027
|
+
});
|
|
5028
|
+
if (!command) throw new Error("Background command not found in this session");
|
|
5029
|
+
|
|
5030
|
+
const terminal = () => command?.state === "exited" || command?.state === "lost";
|
|
5031
|
+
let waitResult: Awaited<ReturnType<typeof waitForSessionChanges>> | null = null;
|
|
5032
|
+
if (!terminal()) {
|
|
5033
|
+
waitResult = await waitForSessionChanges({
|
|
5034
|
+
targets: [{ sessionId: callerSessionId, afterSequence: initialSession.lastSequence }],
|
|
5035
|
+
ownSessionId: callerSessionId,
|
|
5036
|
+
maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
|
|
5037
|
+
targetEventTypes: ["session.command.finished"],
|
|
5038
|
+
targetEventMatches: (event) => {
|
|
5039
|
+
const payload = event.payload;
|
|
5040
|
+
return (
|
|
5041
|
+
payload !== null &&
|
|
5042
|
+
typeof payload === "object" &&
|
|
5043
|
+
!Array.isArray(payload) &&
|
|
5044
|
+
(payload as Record<string, unknown>).commandId === commandId
|
|
5045
|
+
);
|
|
5046
|
+
},
|
|
5047
|
+
signal: extra?.signal,
|
|
5048
|
+
source: {
|
|
5049
|
+
readTargetEvents: async (target) => {
|
|
5050
|
+
const page = await listSessionEventPage(
|
|
5051
|
+
deps.db,
|
|
5052
|
+
grant.workspaceId,
|
|
5053
|
+
target.sessionId,
|
|
5054
|
+
{
|
|
5055
|
+
after: target.afterSequence,
|
|
5056
|
+
direction: "after",
|
|
5057
|
+
limit: SESSION_WAIT_EVENTS_PER_TARGET,
|
|
5058
|
+
payloadMode: "full",
|
|
5059
|
+
includeTypes: ["session.command.finished"],
|
|
5060
|
+
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
5061
|
+
},
|
|
5062
|
+
);
|
|
5063
|
+
return { events: page.events, hasMore: page.hasMore };
|
|
5064
|
+
},
|
|
5065
|
+
readOwnPendingUpdateKinds: async () =>
|
|
5066
|
+
(
|
|
5067
|
+
await listOutstandingSessionSystemUpdates(
|
|
5068
|
+
deps.db,
|
|
5069
|
+
grant.workspaceId,
|
|
5070
|
+
callerSessionId,
|
|
5071
|
+
)
|
|
5072
|
+
).map((update) => update.kind),
|
|
5073
|
+
subscribe: (targetSessionId, onEvents) =>
|
|
5074
|
+
deps.bus.subscribe(grant.workspaceId, targetSessionId, onEvents),
|
|
5075
|
+
},
|
|
5076
|
+
});
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
command =
|
|
5080
|
+
(await getSessionBackgroundCommand(deps.db, {
|
|
5081
|
+
accountId: grant.accountId,
|
|
5082
|
+
workspaceId: grant.workspaceId,
|
|
5083
|
+
sessionId: callerSessionId,
|
|
5084
|
+
commandId,
|
|
5085
|
+
})) ?? command;
|
|
5086
|
+
const isTerminal = terminal();
|
|
5087
|
+
const ownPendingUpdateKinds = (
|
|
5088
|
+
await listOutstandingSessionSystemUpdates(deps.db, grant.workspaceId, callerSessionId)
|
|
5089
|
+
).map((update) => update.kind);
|
|
5090
|
+
const ownPendingUpdates = ownPendingUpdateKinds.length;
|
|
5091
|
+
if (waitResult) {
|
|
5092
|
+
// A bounded wait may cross a host/session revocation boundary. The
|
|
5093
|
+
// durable command and pending-input reads above are not returned
|
|
5094
|
+
// until the target is authorized again at the response boundary.
|
|
5095
|
+
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.events.read");
|
|
5096
|
+
}
|
|
5097
|
+
const timedOut =
|
|
5098
|
+
(waitResult?.timedOut ?? false) && !isTerminal && ownPendingUpdates === 0;
|
|
5099
|
+
return json({
|
|
5100
|
+
command,
|
|
5101
|
+
terminal: isTerminal,
|
|
5102
|
+
waitedMs: waitResult?.waitedMs ?? 0,
|
|
5103
|
+
timedOut,
|
|
5104
|
+
aborted: waitResult?.aborted ?? false,
|
|
5105
|
+
liveFanout: waitResult?.liveFanout ?? true,
|
|
5106
|
+
ownPendingUpdates,
|
|
5107
|
+
ownPendingUpdateKinds,
|
|
5108
|
+
...(isTerminal
|
|
5109
|
+
? {
|
|
5110
|
+
outputLocator: {
|
|
5111
|
+
eventType: "sandbox.command.output.delta",
|
|
5112
|
+
commandId,
|
|
5113
|
+
},
|
|
5114
|
+
}
|
|
5115
|
+
: {}),
|
|
5116
|
+
nextAction: isTerminal
|
|
5117
|
+
? ownPendingUpdates > 0
|
|
5118
|
+
? "End this turn to receive the queued command result or other machine input."
|
|
5119
|
+
: "The command is terminal; inspect output events only if more detail is needed."
|
|
5120
|
+
: ownPendingUpdates > 0
|
|
5121
|
+
? "End this turn to receive pending machine input."
|
|
5122
|
+
: waitResult?.timedOut
|
|
5123
|
+
? "Do not immediately repeat this short wait without new completion evidence; use wait_for_input for a long or uncertain wait."
|
|
5124
|
+
: "The wait was aborted; follow the newer session control or input.",
|
|
5125
|
+
});
|
|
5126
|
+
},
|
|
5127
|
+
);
|
|
5128
|
+
}
|
|
4812
5129
|
}
|
|
4813
5130
|
|
|
4814
5131
|
if (can("sessions:create") && sessionCreateVisible) {
|
|
@@ -6177,36 +6494,90 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
|
|
|
6177
6494
|
}
|
|
6178
6495
|
}
|
|
6179
6496
|
|
|
6180
|
-
function
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6497
|
+
export function capSessionDiscoveryCompactPage(
|
|
6498
|
+
page: Awaited<ReturnType<typeof listSessionDiscoverySummaries>>,
|
|
6499
|
+
options: { includeLastMessage?: boolean; includeRelatedWork?: boolean } = {},
|
|
6500
|
+
maxBytes = SESSION_DISCOVERY_PAGE_MAX_BYTES,
|
|
6184
6501
|
) {
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6502
|
+
let previewBytes = 0;
|
|
6503
|
+
const projected = page.sessions.map((session) => {
|
|
6504
|
+
const row = {
|
|
6505
|
+
...compactSessionMcpListRow(session, options.includeRelatedWork),
|
|
6506
|
+
...(options.includeLastMessage && session.queuedPromptCount > 0
|
|
6507
|
+
? { queuedPromptCount: session.queuedPromptCount }
|
|
6508
|
+
: {}),
|
|
6509
|
+
};
|
|
6510
|
+
if (!options.includeLastMessage) return row;
|
|
6511
|
+
const preview = capSessionDiscoveryText(
|
|
6512
|
+
session.latestMessage?.preview ?? null,
|
|
6513
|
+
600,
|
|
6514
|
+
session.latestMessage?.previewOriginalChars,
|
|
6515
|
+
);
|
|
6516
|
+
if (!session.latestMessage) return row;
|
|
6517
|
+
const bytes = Buffer.byteLength(preview.text ?? "", "utf8");
|
|
6518
|
+
const omitted = previewBytes + bytes > SESSION_DISCOVERY_PREVIEW_MAX_BYTES;
|
|
6519
|
+
if (!omitted) previewBytes += bytes;
|
|
6520
|
+
return {
|
|
6521
|
+
...row,
|
|
6522
|
+
latestMessage: {
|
|
6523
|
+
type: session.latestMessage.type,
|
|
6524
|
+
...(omitted
|
|
6525
|
+
? {
|
|
6526
|
+
previewOmitted: true,
|
|
6527
|
+
previewOmissionReason: SESSION_DISCOVERY_PREVIEW_OMISSION_REASON,
|
|
6528
|
+
previewDrillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
|
|
6529
|
+
previewDrillDownInput: sessionDiscoveryPreviewDrillDownInput(
|
|
6530
|
+
session.id,
|
|
6531
|
+
session.latestMessage.type,
|
|
6532
|
+
),
|
|
6533
|
+
}
|
|
6534
|
+
: {
|
|
6535
|
+
preview: preview.text,
|
|
6536
|
+
...(preview.truncated ? { previewTruncated: true } : {}),
|
|
6537
|
+
}),
|
|
6538
|
+
},
|
|
6539
|
+
};
|
|
6540
|
+
});
|
|
6541
|
+
let kept = projected;
|
|
6542
|
+
const build = () => {
|
|
6543
|
+
const dropped = kept.length < projected.length;
|
|
6544
|
+
const last = page.sessions[kept.length - 1];
|
|
6545
|
+
const cursor =
|
|
6546
|
+
dropped && last
|
|
6547
|
+
? {
|
|
6548
|
+
orderBy: page.orderBy,
|
|
6549
|
+
sortRank: last.sortRank,
|
|
6550
|
+
sortRevision: last.sortRevision,
|
|
6551
|
+
sortAt: last.sortAt,
|
|
6552
|
+
id: last.id,
|
|
6553
|
+
snapshotAt: page.snapshotAt,
|
|
6554
|
+
snapshotRevision: page.snapshotRevision,
|
|
6555
|
+
updatedAfter: page.updatedAfter,
|
|
6556
|
+
filterHash: page.filterHash,
|
|
6557
|
+
}
|
|
6558
|
+
: page.nextCursor;
|
|
6559
|
+
return {
|
|
6560
|
+
sessions: kept,
|
|
6561
|
+
total: page.total,
|
|
6562
|
+
nextCursor: cursor ? encodeSessionDiscoveryCursor(cursor) : null,
|
|
6563
|
+
...(page.orderBy === "updatedAt" ? { updatedThrough: page.updatedThrough } : {}),
|
|
6564
|
+
...(dropped
|
|
6565
|
+
? {
|
|
6566
|
+
responseTruncated: true,
|
|
6567
|
+
truncationReason: `response exceeded ${maxBytes} bytes; continue with nextCursor`,
|
|
6568
|
+
}
|
|
6569
|
+
: {}),
|
|
6570
|
+
};
|
|
6571
|
+
};
|
|
6572
|
+
let result = build();
|
|
6573
|
+
while (Buffer.byteLength(JSON.stringify(result, null, 2), "utf8") > maxBytes && kept.length > 1) {
|
|
6574
|
+
kept = kept.slice(0, -1);
|
|
6575
|
+
result = build();
|
|
6192
6576
|
}
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
// count. Iterate to a stable marker width so the reported omission includes
|
|
6196
|
-
// the characters replaced by the marker itself.
|
|
6197
|
-
let bodyChars = maxChars;
|
|
6198
|
-
let marker = "";
|
|
6199
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
6200
|
-
const omittedChars = Math.max(0, sourceChars - bodyChars);
|
|
6201
|
-
marker = `…[${omittedChars} chars truncated]…`;
|
|
6202
|
-
const nextBodyChars = Math.max(0, maxChars - Array.from(marker).length);
|
|
6203
|
-
if (nextBodyChars === bodyChars) break;
|
|
6204
|
-
bodyChars = nextBodyChars;
|
|
6577
|
+
if (Buffer.byteLength(JSON.stringify(result, null, 2), "utf8") > maxBytes) {
|
|
6578
|
+
throw new RangeError(`sessions_list compact metadata exceeds its ${maxBytes}-byte envelope`);
|
|
6205
6579
|
}
|
|
6206
|
-
return
|
|
6207
|
-
text: `${projectedChars.slice(0, bodyChars).join("")}${marker}`,
|
|
6208
|
-
truncated: true,
|
|
6209
|
-
};
|
|
6580
|
+
return result;
|
|
6210
6581
|
}
|
|
6211
6582
|
|
|
6212
6583
|
export function capSessionDiscoveryPage(
|