@akira-tl/forgerelay 0.6.0 → 0.6.2
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/CHANGELOG.md +36 -0
- package/README.md +47 -12
- package/dist/activity/mcp-query-tools.js +48 -2
- package/dist/activity/query-service.js +1 -0
- package/dist/cli.js +181 -4
- package/dist/composite-activity.js +155 -0
- package/dist/composite-workspaces.js +197 -0
- package/dist/config.js +4 -1
- package/dist/oauth/router.js +21 -1
- package/dist/oauth-provider.js +32 -4
- package/dist/oauth-store.js +9 -0
- package/dist/remote-auth.js +110 -0
- package/dist/remote-transport.js +196 -0
- package/dist/remote-workspace-relay.js +473 -0
- package/dist/server.js +824 -121
- package/dist/ui/.vite/manifest.json +33 -33
- package/dist/ui/activity-panel-app.html +3 -3
- package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
- package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
- package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
- package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
- package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
- package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
- package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
- package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
- package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
- package/dist/ui/workspace-app.html +4 -4
- package/dist/ui/workspace-lifecycle-app.html +4 -4
- package/dist/user-config.js +131 -5
- package/docs/configuration.md +26 -3
- package/docs/debugging.md +7 -0
- package/docs/versioning.md +11 -19
- package/package.json +5 -2
- package/scripts/ci/verify.mjs +38 -0
- package/scripts/debug/runtime.mjs +23 -1
- package/scripts/debug/runtime.test.mjs +14 -2
- package/scripts/debug/serve.mjs +4 -4
- package/scripts/release/pack.mjs +36 -0
- package/scripts/release/publish.mjs +157 -0
- package/scripts/release/release-gate.test.mjs +73 -16
- package/scripts/release-parity.mjs +5 -13
- package/scripts/release-proof.mjs +28 -19
- package/scripts/release-proof.test.mjs +32 -11
- package/scripts/release-version.mjs +1 -1
- package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
- package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
- package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
- package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
package/dist/server.js
CHANGED
|
@@ -45,6 +45,9 @@ import { createCoreOperationExecutor, } from "./operations/core-operation-execut
|
|
|
45
45
|
import { McpTransportRegistry, } from "./mcp-sessions.js";
|
|
46
46
|
import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
|
|
47
47
|
import { createReviewCheckpointManager } from "./review-checkpoints.js";
|
|
48
|
+
import { CompositeActivityCoordinator } from "./composite-activity.js";
|
|
49
|
+
import { CompositeWorkspaceRegistry } from "./composite-workspaces.js";
|
|
50
|
+
import { RemoteWorkspaceRelay } from "./remote-workspace-relay.js";
|
|
48
51
|
import { hostConversationScopeId, openAiConversationScopeId } from "./request-meta.js";
|
|
49
52
|
import { ACTIVITY_PANEL_APP_LEGACY_URI, ACTIVITY_PANEL_APP_URI_TEMPLATE, MCP_APP_RESOURCE_TEMPLATE_REVISION, readActivityPanelAppManifestEntry, readWorkspaceAppManifestEntry, readWorkspaceLifecycleAppManifestEntry, resolveActivityPanelAppIdentity, resolveWorkspaceAppIdentity, resolveWorkspaceLifecycleAppIdentity, WORKSPACE_APP_LEGACY_URI, WORKSPACE_APP_URI_TEMPLATE, WORKSPACE_LIFECYCLE_APP_LEGACY_URI, WORKSPACE_LIFECYCLE_APP_URI_TEMPLATE, } from "./mcp-app-template.js";
|
|
50
53
|
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
@@ -875,6 +878,39 @@ function toolResultContent(result) {
|
|
|
875
878
|
const content = result.content;
|
|
876
879
|
return Array.isArray(content) ? content : [];
|
|
877
880
|
}
|
|
881
|
+
function remapCompositeToolResult(result, executionWorkspaceId, compositeWorkspaceId, member) {
|
|
882
|
+
if (typeof result !== "object" || result === null)
|
|
883
|
+
return result;
|
|
884
|
+
const record = result;
|
|
885
|
+
const remapped = replaceWorkspaceIdentity(record, executionWorkspaceId, compositeWorkspaceId);
|
|
886
|
+
const meta = typeof remapped._meta === "object" && remapped._meta !== null
|
|
887
|
+
? { ...remapped._meta }
|
|
888
|
+
: undefined;
|
|
889
|
+
if (meta) {
|
|
890
|
+
const card = typeof meta.card === "object" && meta.card !== null
|
|
891
|
+
? { ...meta.card, workspaceId: compositeWorkspaceId, member }
|
|
892
|
+
: undefined;
|
|
893
|
+
if (card)
|
|
894
|
+
meta.card = card;
|
|
895
|
+
remapped._meta = meta;
|
|
896
|
+
}
|
|
897
|
+
const structured = typeof remapped.structuredContent === "object" && remapped.structuredContent !== null
|
|
898
|
+
? { ...remapped.structuredContent, member }
|
|
899
|
+
: undefined;
|
|
900
|
+
if (structured)
|
|
901
|
+
remapped.structuredContent = structured;
|
|
902
|
+
return remapped;
|
|
903
|
+
}
|
|
904
|
+
function replaceWorkspaceIdentity(value, from, to) {
|
|
905
|
+
if (typeof value === "string")
|
|
906
|
+
return value.split(from).join(to);
|
|
907
|
+
if (Array.isArray(value))
|
|
908
|
+
return value.map((entry) => replaceWorkspaceIdentity(entry, from, to));
|
|
909
|
+
if (!value || typeof value !== "object")
|
|
910
|
+
return value;
|
|
911
|
+
return Object.fromEntries(Object.entries(value)
|
|
912
|
+
.map(([key, entry]) => [key, replaceWorkspaceIdentity(entry, from, to)]));
|
|
913
|
+
}
|
|
878
914
|
function toolResultAgentsFiles(result) {
|
|
879
915
|
if (typeof result !== "object" || result === null)
|
|
880
916
|
return [];
|
|
@@ -955,6 +991,14 @@ function activityRelationFor(context) {
|
|
|
955
991
|
...(context.turnId ? { turnId: context.turnId } : {}),
|
|
956
992
|
};
|
|
957
993
|
}
|
|
994
|
+
function activityRequestFor(input, context) {
|
|
995
|
+
if (!context.activityMember || !input || typeof input !== "object" || Array.isArray(input))
|
|
996
|
+
return input;
|
|
997
|
+
return {
|
|
998
|
+
...input,
|
|
999
|
+
member: context.activityMember,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
958
1002
|
function runActivityTool(lifecycle, workspace, conversationScopeId, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
|
|
959
1003
|
return lifecycle.run({
|
|
960
1004
|
tool,
|
|
@@ -969,13 +1013,14 @@ function runActivityTool(lifecycle, workspace, conversationScopeId, tool, reques
|
|
|
969
1013
|
function runActivityToolWithHooks(lifecycle, hooks, workspace, conversationScopeId, request, hookOptions, relation = {}) {
|
|
970
1014
|
return runActivityTool(lifecycle, workspace, conversationScopeId, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
|
|
971
1015
|
}
|
|
972
|
-
function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun) {
|
|
1016
|
+
function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun, routing) {
|
|
973
1017
|
if (config.toolMode === "codex") {
|
|
974
1018
|
registerAppTool(server, "exec_command", {
|
|
975
1019
|
title: "Execute command",
|
|
976
1020
|
description: `Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a processId for write_stdin. Use this for file inspection, tests, builds, package scripts, generators, formatters, and long-running processes. ${buildShellMutationPolicy()} Call open_workspace first and pass workspaceId.`,
|
|
977
1021
|
inputSchema: {
|
|
978
1022
|
workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
|
|
1023
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process."),
|
|
979
1024
|
cmd: z.string().min(1).describe("Shell command to execute."),
|
|
980
1025
|
tty: z
|
|
981
1026
|
.boolean()
|
|
@@ -1012,22 +1057,34 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1012
1057
|
outputSchema: processOutputSchema(),
|
|
1013
1058
|
...toolWidgetDescriptorMeta(config, "shell"),
|
|
1014
1059
|
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
1015
|
-
}, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) =>
|
|
1016
|
-
workspaceId,
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1060
|
+
}, async ({ workspaceId, member, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
|
|
1061
|
+
const target = routing.resolve(workspaceId, member);
|
|
1062
|
+
const context = await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
|
|
1063
|
+
if (routing.isRemote(target.executionWorkspaceId)) {
|
|
1064
|
+
return routing.present(await routing.execCommandRemote(target.executionWorkspaceId, {
|
|
1065
|
+
cmd,
|
|
1066
|
+
...(tty !== undefined ? { tty } : {}),
|
|
1067
|
+
...(columns !== undefined ? { columns } : {}),
|
|
1068
|
+
...(rows !== undefined ? { rows } : {}),
|
|
1069
|
+
...(workingDirectory !== undefined ? { workingDirectory } : {}),
|
|
1070
|
+
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
1071
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1072
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
1073
|
+
}, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
1074
|
+
}
|
|
1075
|
+
return routing.present(await shellRun({
|
|
1076
|
+
workspaceId: target.executionWorkspaceId,
|
|
1077
|
+
command: cmd,
|
|
1078
|
+
surface: "exec_command",
|
|
1079
|
+
tty,
|
|
1080
|
+
columns,
|
|
1081
|
+
rows,
|
|
1082
|
+
workingDirectory,
|
|
1083
|
+
yieldTimeMs,
|
|
1084
|
+
timeoutMs,
|
|
1085
|
+
maxOutputTokens,
|
|
1086
|
+
}, context), target);
|
|
1087
|
+
});
|
|
1031
1088
|
}
|
|
1032
1089
|
if (config.toolMode !== "codex")
|
|
1033
1090
|
return;
|
|
@@ -1036,6 +1093,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1036
1093
|
description: "Poll or write characters to a running process returned by exec_command, or retrieve complete durable process output by outputId. Omit chars or pass an empty string to poll. Waiting never kills the process; pass \\u0003 to explicitly send Ctrl-C.",
|
|
1037
1094
|
inputSchema: {
|
|
1038
1095
|
workspaceId: z.string().describe("Workspace identifier used to start the process."),
|
|
1096
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns the process."),
|
|
1039
1097
|
processId: z.number().int().positive().optional().describe("Canonical process identifier returned by bash or exec_command."),
|
|
1040
1098
|
sessionId: z.number().int().positive().optional().describe("Deprecated alias for processId. Retained for compatibility."),
|
|
1041
1099
|
outputId: z.string().optional().describe("Stable output identifier returned by exec_command. When supplied, retrieve the complete durable output instead of controlling a process."),
|
|
@@ -1060,8 +1118,23 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1060
1118
|
outputSchema: processOutputSchema(),
|
|
1061
1119
|
...toolWidgetDescriptorMeta(config, "shell"),
|
|
1062
1120
|
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
1063
|
-
}, async ({ workspaceId, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
|
|
1064
|
-
const
|
|
1121
|
+
}, async ({ workspaceId, member, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
|
|
1122
|
+
const target = routing.resolve(workspaceId, member);
|
|
1123
|
+
await routing.prepare(target, extra._meta, extra.signal, extra.sessionId);
|
|
1124
|
+
if (routing.isRemote(target.executionWorkspaceId)) {
|
|
1125
|
+
return routing.present(await routing.writeStdinRemote(target.executionWorkspaceId, {
|
|
1126
|
+
...(processId !== undefined ? { processId } : {}),
|
|
1127
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1128
|
+
...(outputId !== undefined ? { outputId } : {}),
|
|
1129
|
+
...(chars !== undefined ? { chars } : {}),
|
|
1130
|
+
...(columns !== undefined ? { columns } : {}),
|
|
1131
|
+
...(rows !== undefined ? { rows } : {}),
|
|
1132
|
+
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
1133
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
1134
|
+
}, routing.hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
1135
|
+
}
|
|
1136
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
1137
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
1065
1138
|
if (outputId !== undefined) {
|
|
1066
1139
|
if (processId !== undefined || sessionId !== undefined || chars !== undefined || columns !== undefined ||
|
|
1067
1140
|
rows !== undefined || yieldTimeMs !== undefined || maxOutputTokens !== undefined) {
|
|
@@ -1072,7 +1145,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1072
1145
|
tool: "write_stdin",
|
|
1073
1146
|
invocation: workspaceHookInvocation(workspace),
|
|
1074
1147
|
payload: { outputId },
|
|
1075
|
-
operation: async () => durableOutputResponse("write_stdin",
|
|
1148
|
+
operation: async () => durableOutputResponse("write_stdin", executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
|
|
1076
1149
|
});
|
|
1077
1150
|
}
|
|
1078
1151
|
const resolvedProcessId = resolveProcessId(processId, sessionId);
|
|
@@ -1089,7 +1162,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1089
1162
|
operation: async () => {
|
|
1090
1163
|
const startedAt = performance.now();
|
|
1091
1164
|
const snapshot = await processSessions.write({
|
|
1092
|
-
workspaceId,
|
|
1165
|
+
workspaceId: executionWorkspaceId,
|
|
1093
1166
|
processId: resolvedProcessId,
|
|
1094
1167
|
chars,
|
|
1095
1168
|
columns,
|
|
@@ -1107,7 +1180,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1107
1180
|
success: snapshot.running || snapshot.exitCode === 0,
|
|
1108
1181
|
durationMs: Math.round(performance.now() - startedAt),
|
|
1109
1182
|
});
|
|
1110
|
-
const response = processToolResponse("write_stdin",
|
|
1183
|
+
const response = processToolResponse("write_stdin", executionWorkspaceId, snapshot, {
|
|
1111
1184
|
processId: resolvedProcessId,
|
|
1112
1185
|
charactersWritten: chars?.length ?? 0,
|
|
1113
1186
|
running: snapshot.running,
|
|
@@ -1119,12 +1192,57 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
1119
1192
|
}
|
|
1120
1193
|
return response;
|
|
1121
1194
|
},
|
|
1122
|
-
});
|
|
1195
|
+
}).then((result) => routing.present(result, target));
|
|
1123
1196
|
});
|
|
1124
1197
|
}
|
|
1125
1198
|
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries) {
|
|
1126
1199
|
const connectionScopeId = `mcp-connection:${randomUUID()}`;
|
|
1200
|
+
const remoteWorkspaces = new RemoteWorkspaceRelay(config.configDir, config.stateDir);
|
|
1201
|
+
const compositeWorkspaces = new CompositeWorkspaceRegistry(config.stateDir);
|
|
1202
|
+
const compositeActivity = new CompositeActivityCoordinator(compositeWorkspaces, activityQueries, remoteWorkspaces);
|
|
1203
|
+
const resolveExecutionTarget = (workspaceId, memberName) => {
|
|
1204
|
+
if (!compositeWorkspaces.has(workspaceId)) {
|
|
1205
|
+
if (memberName !== undefined) {
|
|
1206
|
+
throw new Error(`Workspace ${workspaceId} is not composite and does not accept member.`);
|
|
1207
|
+
}
|
|
1208
|
+
return { executionWorkspaceId: workspaceId };
|
|
1209
|
+
}
|
|
1210
|
+
if (!memberName) {
|
|
1211
|
+
throw new Error(`Composite Workspace ${workspaceId} requires member for this operation.`);
|
|
1212
|
+
}
|
|
1213
|
+
const member = compositeWorkspaces.member(workspaceId, memberName);
|
|
1214
|
+
try {
|
|
1215
|
+
if (!remoteWorkspaces.has(member.workspaceId))
|
|
1216
|
+
workspaces.getWorkspace(member.workspaceId);
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
throw new Error(`Composite Workspace ${workspaceId} member ${member.name} is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1220
|
+
}
|
|
1221
|
+
return {
|
|
1222
|
+
executionWorkspaceId: member.workspaceId,
|
|
1223
|
+
compositeWorkspaceId: workspaceId,
|
|
1224
|
+
memberName: member.name,
|
|
1225
|
+
};
|
|
1226
|
+
};
|
|
1227
|
+
const presentExecutionResult = (result, target) => {
|
|
1228
|
+
if (!target.compositeWorkspaceId || !target.memberName)
|
|
1229
|
+
return result;
|
|
1230
|
+
return remapCompositeToolResult(result, target.executionWorkspaceId, target.compositeWorkspaceId, target.memberName);
|
|
1231
|
+
};
|
|
1127
1232
|
const hostScopeIdFor = (requestMeta, transportSessionId) => hostConversationScopeId(requestMeta, transportSessionId, connectionScopeId);
|
|
1233
|
+
const prepareExecutionContext = async (target, requestMeta, signal, sessionId) => {
|
|
1234
|
+
const conversationScopeId = hostScopeIdFor(requestMeta, sessionId);
|
|
1235
|
+
const turnId = target.compositeWorkspaceId && target.memberName
|
|
1236
|
+
? await compositeActivity.prepareMember(target.compositeWorkspaceId, target.memberName, target.executionWorkspaceId, conversationScopeId)
|
|
1237
|
+
: undefined;
|
|
1238
|
+
return {
|
|
1239
|
+
requestMeta,
|
|
1240
|
+
signal,
|
|
1241
|
+
sessionId,
|
|
1242
|
+
...(turnId ? { turnId } : {}),
|
|
1243
|
+
...(target.memberName ? { activityMember: target.memberName } : {}),
|
|
1244
|
+
};
|
|
1245
|
+
};
|
|
1128
1246
|
const toolDescriptions = buildToolDescriptions(config);
|
|
1129
1247
|
const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
|
|
1130
1248
|
const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
|
|
@@ -1222,11 +1340,59 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1222
1340
|
},
|
|
1223
1341
|
},
|
|
1224
1342
|
});
|
|
1343
|
+
const loadCompositeMemberContext = async (compositeWorkspaceId, memberName, contextPolicy, conversationScopeId, protectedWorkspaceIds) => {
|
|
1344
|
+
const target = resolveExecutionTarget(compositeWorkspaceId, memberName);
|
|
1345
|
+
if (remoteWorkspaces.has(target.executionWorkspaceId)) {
|
|
1346
|
+
const resumed = await remoteWorkspaces.resumeWorkspace(target.executionWorkspaceId, contextPolicy, conversationScopeId);
|
|
1347
|
+
const presented = presentExecutionResult(resumed, target);
|
|
1348
|
+
return {
|
|
1349
|
+
member: memberName,
|
|
1350
|
+
...(presented.structuredContent ?? {}),
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
const opened = await workspaces.openWorkspace({ workspaceId: target.executionWorkspaceId, context: contextPolicy }, { conversationScopeId, protectedWorkspaceIds });
|
|
1354
|
+
const workspace = opened.workspace;
|
|
1355
|
+
const capabilityFingerprint = buildCapabilityFingerprint(config, FORGERELAY_VERSION, {
|
|
1356
|
+
artifactDownloadSupported: isArtifactDownloadSupportedPlatform(),
|
|
1357
|
+
});
|
|
1358
|
+
const capabilityCatalog = capabilityRegistry.catalog(capabilityContextFor(workspace));
|
|
1359
|
+
const agentsFiles = opened.agentsFiles.map((file) => ({
|
|
1360
|
+
path: formatAgentsPath(file.path, workspace.root),
|
|
1361
|
+
content: file.content,
|
|
1362
|
+
}));
|
|
1363
|
+
const availableAgentsFiles = opened.availableAgentsFiles.map((file) => ({
|
|
1364
|
+
path: formatAgentsPath(file.path, workspace.root),
|
|
1365
|
+
}));
|
|
1366
|
+
const skills = workspace.skills
|
|
1367
|
+
.filter((skill) => !skill.disableModelInvocation)
|
|
1368
|
+
.map((skill) => ({ name: skill.name, description: skill.description }));
|
|
1369
|
+
return {
|
|
1370
|
+
member: memberName,
|
|
1371
|
+
workspaceId: compositeWorkspaceId,
|
|
1372
|
+
root: workspace.root,
|
|
1373
|
+
mode: workspace.mode,
|
|
1374
|
+
contextFingerprint: opened.contextFingerprint,
|
|
1375
|
+
capabilityFingerprint,
|
|
1376
|
+
capabilityCatalog,
|
|
1377
|
+
includeBootstrapContext: opened.includeBootstrapContext,
|
|
1378
|
+
...(opened.includeBootstrapContext
|
|
1379
|
+
? {
|
|
1380
|
+
agentsFiles,
|
|
1381
|
+
availableAgentsFiles,
|
|
1382
|
+
skills,
|
|
1383
|
+
skillDiagnostics: redactSkillDiagnosticPaths(workspace.skillDiagnostics),
|
|
1384
|
+
}
|
|
1385
|
+
: {}),
|
|
1386
|
+
instruction: opened.includeBootstrapContext
|
|
1387
|
+
? `Bootstrap context for Composite member ${memberName}. Keep using Composite workspaceId ${compositeWorkspaceId} and pass member=${memberName} for work operations.`
|
|
1388
|
+
: `Composite member ${memberName} context was already delivered for this Host context; keep using Composite workspaceId ${compositeWorkspaceId} with member=${memberName}.`,
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1225
1391
|
const coreOperations = createCoreOperationExecutor({
|
|
1226
1392
|
read: async (input, context) => {
|
|
1227
1393
|
const { workspaceId, ...readInput } = input;
|
|
1228
1394
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1229
|
-
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
|
|
1395
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
|
|
1230
1396
|
signal: context.signal,
|
|
1231
1397
|
tool: toolNames.read,
|
|
1232
1398
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1298,7 +1464,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1298
1464
|
write: async (input, context) => {
|
|
1299
1465
|
const { workspaceId, ...writeInput } = input;
|
|
1300
1466
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1301
|
-
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
|
|
1467
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
|
|
1302
1468
|
signal: context.signal,
|
|
1303
1469
|
tool: toolNames.write,
|
|
1304
1470
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1359,7 +1525,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1359
1525
|
edit: async (input, context) => {
|
|
1360
1526
|
const { workspaceId, ...editInput } = input;
|
|
1361
1527
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1362
|
-
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
|
|
1528
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
|
|
1363
1529
|
signal: context.signal,
|
|
1364
1530
|
tool: toolNames.edit,
|
|
1365
1531
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1421,7 +1587,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1421
1587
|
rename: async (input, context) => {
|
|
1422
1588
|
const { workspaceId, path, newPath } = input;
|
|
1423
1589
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1424
|
-
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
|
|
1590
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
|
|
1425
1591
|
signal: context.signal,
|
|
1426
1592
|
tool: toolNames.rename,
|
|
1427
1593
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1480,7 +1646,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1480
1646
|
delete: async (input, context) => {
|
|
1481
1647
|
const { workspaceId, path, recursive } = input;
|
|
1482
1648
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1483
|
-
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), input, {
|
|
1649
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor(input, context), {
|
|
1484
1650
|
signal: context.signal,
|
|
1485
1651
|
tool: toolNames.delete,
|
|
1486
1652
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1564,7 +1730,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1564
1730
|
maxOutputTokens,
|
|
1565
1731
|
};
|
|
1566
1732
|
let undeliveredProcessId;
|
|
1567
|
-
const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequest, async (activityContext) => {
|
|
1733
|
+
const activityResult = await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), surface, activityRequestFor(activityRequest, context), async (activityContext) => {
|
|
1568
1734
|
try {
|
|
1569
1735
|
const result = await runToolWithHooks(hooks, {
|
|
1570
1736
|
signal: context.signal,
|
|
@@ -1638,7 +1804,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1638
1804
|
const { workspaceId, name, arguments: capabilityArguments, file } = input;
|
|
1639
1805
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1640
1806
|
let changedPaths = [];
|
|
1641
|
-
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), { workspaceId, name, action: "run", arguments: capabilityArguments, file }, {
|
|
1807
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), activityRequestFor({ workspaceId, name, action: "run", arguments: capabilityArguments, file }, context), {
|
|
1642
1808
|
signal: context.signal,
|
|
1643
1809
|
tool: toolNames.capability,
|
|
1644
1810
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1735,6 +1901,26 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1735
1901
|
instructions: buildServerInstructions(config),
|
|
1736
1902
|
});
|
|
1737
1903
|
const workspacePanelStates = new Map();
|
|
1904
|
+
const workspacePanelState = (workspaceId) => {
|
|
1905
|
+
const remembered = workspacePanelStates.get(workspaceId);
|
|
1906
|
+
if (remembered)
|
|
1907
|
+
return remembered;
|
|
1908
|
+
try {
|
|
1909
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1910
|
+
return {
|
|
1911
|
+
workspaceId: workspace.id,
|
|
1912
|
+
root: workspace.root,
|
|
1913
|
+
path: workspace.root,
|
|
1914
|
+
mode: workspace.mode,
|
|
1915
|
+
sourceRoot: workspace.sourceRoot,
|
|
1916
|
+
instruction: `Use workspaceId ${workspace.id} for subsequent calls.`,
|
|
1917
|
+
summary: { mode: workspace.mode },
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
catch {
|
|
1921
|
+
return undefined;
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1738
1924
|
const rememberWorkspacePanelState = (workspaceId, response) => {
|
|
1739
1925
|
if (typeof response._meta !== "object" || response._meta === null)
|
|
1740
1926
|
return;
|
|
@@ -1772,16 +1958,48 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1772
1958
|
server.registerResource("ForgeRelay Activity Panel compatibility", new ResourceTemplate(ACTIVITY_PANEL_APP_URI_TEMPLATE, { list: undefined }), { ...activityPanelResourceMetadata, mimeType: RESOURCE_MIME_TYPE }, async (uri, _variables, extra) => readActivityPanelAppResource(config, uri.toString(), extra.sessionId));
|
|
1773
1959
|
registerAppTool(server, "open_workspace", {
|
|
1774
1960
|
title: "Open workspace",
|
|
1775
|
-
description: "Open or resume a local
|
|
1961
|
+
description: "Open or resume a ForgeRelay Workspace. Ordinary workspaces default to local execution; relay may name a registered remote ForgeRelay. Composite Workspaces use the same open lifecycle but have kind=\"composite\" and a name instead of a mounted root. Reuse the returned workspaceId for later calls. Bootstrap context is delivered automatically only when needed and can be suppressed or refreshed.",
|
|
1776
1962
|
inputSchema: {
|
|
1777
1963
|
action: z
|
|
1778
|
-
.enum(["open", "list"])
|
|
1964
|
+
.enum(["open", "list", "member"])
|
|
1965
|
+
.optional()
|
|
1966
|
+
.describe("Defaults to open. Use list to inspect logical workspaces. Use member to add/remove a named execution member on an existing Composite Workspace."),
|
|
1967
|
+
memberAction: z
|
|
1968
|
+
.enum(["add", "update", "remove"])
|
|
1779
1969
|
.optional()
|
|
1780
|
-
.describe("
|
|
1970
|
+
.describe("Required with action=member."),
|
|
1971
|
+
member: z.object({
|
|
1972
|
+
name: z.string().describe("Stable member name such as code or compute. For update/remove this identifies the existing member."),
|
|
1973
|
+
newName: z.string().optional().describe("Optional replacement member name for memberAction=update."),
|
|
1974
|
+
purpose: z.string().optional().describe("Agent-facing purpose. Required when adding a member; optional replacement when updating."),
|
|
1975
|
+
workspaceId: z.string().optional().describe("Existing ordinary or relayed Workspace to mount. Mutually exclusive with path."),
|
|
1976
|
+
path: z.string().optional().describe("Workspace path to open internally and mount. Mutually exclusive with workspaceId."),
|
|
1977
|
+
relay: z.string().optional().describe("Optional registered remote ForgeRelay alias for a path-backed member."),
|
|
1978
|
+
mode: z.enum(["checkout", "worktree"]).optional(),
|
|
1979
|
+
baseRef: z.string().optional(),
|
|
1980
|
+
newWorktree: z.boolean().optional(),
|
|
1981
|
+
newWorkspace: z.boolean().optional(),
|
|
1982
|
+
}).optional().describe("Composite member definition used by action=member."),
|
|
1983
|
+
kind: z
|
|
1984
|
+
.enum(["workspace", "composite"])
|
|
1985
|
+
.optional()
|
|
1986
|
+
.describe("Workspace kind for action=open. Defaults to workspace. Use composite with name and no path to create or reopen a named Composite Workspace."),
|
|
1987
|
+
name: z
|
|
1988
|
+
.string()
|
|
1989
|
+
.optional()
|
|
1990
|
+
.describe("Composite Workspace name. Used only with kind=\"composite\" when creating/opening by name."),
|
|
1991
|
+
memberName: z
|
|
1992
|
+
.string()
|
|
1993
|
+
.optional()
|
|
1994
|
+
.describe("For action=open on a Composite Workspace, load bootstrap context for this named member without making it an implicit current member."),
|
|
1781
1995
|
path: z
|
|
1782
1996
|
.string()
|
|
1783
1997
|
.optional()
|
|
1784
|
-
.describe("Project path to open. Required for action=open unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
|
|
1998
|
+
.describe("Project path to open for an ordinary Workspace. Required for action=open unless workspaceId is supplied or kind=\"composite\" with name is used. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
|
|
1999
|
+
relay: z
|
|
2000
|
+
.string()
|
|
2001
|
+
.optional()
|
|
2002
|
+
.describe("Optional registered remote ForgeRelay alias. When supplied for action=open, the workspace is opened and executed on that remote instance while this Gateway returns its own workspaceId."),
|
|
1785
2003
|
workspaceId: z
|
|
1786
2004
|
.string()
|
|
1787
2005
|
.optional()
|
|
@@ -1837,8 +2055,17 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1837
2055
|
.describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
|
|
1838
2056
|
},
|
|
1839
2057
|
outputSchema: {
|
|
1840
|
-
action: z.enum(["open", "list"]),
|
|
2058
|
+
action: z.enum(["open", "list", "member"]),
|
|
1841
2059
|
workspaceId: z.string().optional(),
|
|
2060
|
+
memberAction: z.enum(["add", "update", "remove"]).optional(),
|
|
2061
|
+
kind: z.enum(["workspace", "composite"]).optional(),
|
|
2062
|
+
name: z.string().optional(),
|
|
2063
|
+
members: z.array(z.object({
|
|
2064
|
+
name: z.string(),
|
|
2065
|
+
purpose: z.string(),
|
|
2066
|
+
workspaceId: z.string(),
|
|
2067
|
+
})).optional(),
|
|
2068
|
+
memberContext: z.unknown().optional(),
|
|
1842
2069
|
root: z.string().optional(),
|
|
1843
2070
|
mode: z.enum(["checkout", "worktree"]).optional(),
|
|
1844
2071
|
sourceRoot: z.string().optional(),
|
|
@@ -1885,6 +2112,18 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1885
2112
|
agents: z.array(workspaceLocalAgentOutputSchema).optional(),
|
|
1886
2113
|
skillDiagnostics: z.array(workspaceSkillDiagnosticOutputSchema).optional(),
|
|
1887
2114
|
workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
|
|
2115
|
+
compositeWorkspaces: z.array(z.object({
|
|
2116
|
+
workspaceId: z.string(),
|
|
2117
|
+
kind: z.literal("composite"),
|
|
2118
|
+
name: z.string(),
|
|
2119
|
+
members: z.array(z.object({
|
|
2120
|
+
name: z.string(),
|
|
2121
|
+
purpose: z.string(),
|
|
2122
|
+
workspaceId: z.string(),
|
|
2123
|
+
})),
|
|
2124
|
+
createdAt: z.string(),
|
|
2125
|
+
lastUsedAt: z.string(),
|
|
2126
|
+
})).optional(),
|
|
1888
2127
|
summary: workspaceInventorySummaryOutputSchema.optional(),
|
|
1889
2128
|
page: workspaceInventoryPageOutputSchema.optional(),
|
|
1890
2129
|
instruction: z.string(),
|
|
@@ -1896,26 +2135,198 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1896
2135
|
idempotentHint: false,
|
|
1897
2136
|
openWorldHint: false,
|
|
1898
2137
|
},
|
|
1899
|
-
}, async ({ action = "open", path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
|
|
2138
|
+
}, async ({ action = "open", memberAction, member, kind, name, memberName, path, relay, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
|
|
1900
2139
|
const startedAt = performance.now();
|
|
1901
2140
|
const conversationScopeId = openAiConversationScopeId(_meta);
|
|
1902
2141
|
const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
|
|
2142
|
+
if (action === "member") {
|
|
2143
|
+
if (!workspaceId || !compositeWorkspaces.has(workspaceId)) {
|
|
2144
|
+
throw new Error("open_workspace action=member requires an existing Composite Workspace workspaceId.");
|
|
2145
|
+
}
|
|
2146
|
+
if (!memberAction || !member) {
|
|
2147
|
+
throw new Error("open_workspace action=member requires memberAction and member.");
|
|
2148
|
+
}
|
|
2149
|
+
if (kind !== undefined || name !== undefined || memberName !== undefined || path !== undefined || relay !== undefined || mode !== undefined ||
|
|
2150
|
+
baseRef !== undefined || newWorktree !== undefined || newWorkspace !== undefined || context !== undefined ||
|
|
2151
|
+
root !== undefined || status !== undefined || state !== undefined || staleOnly !== undefined ||
|
|
2152
|
+
offset !== undefined || limit !== undefined) {
|
|
2153
|
+
throw new Error("open_workspace action=member accepts workspaceId, memberAction, and member only. Put any Workspace open definition inside member.");
|
|
2154
|
+
}
|
|
2155
|
+
const resolveMemberTargetWorkspaceId = async () => {
|
|
2156
|
+
const byWorkspaceId = typeof member.workspaceId === "string" && member.workspaceId.length > 0;
|
|
2157
|
+
const byPath = typeof member.path === "string" && member.path.length > 0;
|
|
2158
|
+
if (byWorkspaceId === byPath) {
|
|
2159
|
+
throw new Error("A Composite Workspace member target requires exactly one of member.workspaceId or member.path.");
|
|
2160
|
+
}
|
|
2161
|
+
if (byWorkspaceId) {
|
|
2162
|
+
const targetWorkspaceId = member.workspaceId;
|
|
2163
|
+
if (compositeWorkspaces.has(targetWorkspaceId)) {
|
|
2164
|
+
throw new Error("A Composite Workspace cannot be mounted as a Composite Workspace member.");
|
|
2165
|
+
}
|
|
2166
|
+
if (member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
|
|
2167
|
+
member.newWorktree !== undefined || member.newWorkspace !== undefined) {
|
|
2168
|
+
throw new Error("member.workspaceId cannot be combined with relay/mode/baseRef/newWorktree/newWorkspace.");
|
|
2169
|
+
}
|
|
2170
|
+
if (!remoteWorkspaces.has(targetWorkspaceId))
|
|
2171
|
+
workspaces.getWorkspace(targetWorkspaceId);
|
|
2172
|
+
return targetWorkspaceId;
|
|
2173
|
+
}
|
|
2174
|
+
if (member.relay !== undefined) {
|
|
2175
|
+
const opened = await remoteWorkspaces.openWorkspace(member.relay, {
|
|
2176
|
+
path: member.path,
|
|
2177
|
+
...(member.mode ? { mode: member.mode } : {}),
|
|
2178
|
+
...(member.baseRef ? { baseRef: member.baseRef } : {}),
|
|
2179
|
+
...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
|
|
2180
|
+
...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
|
|
2181
|
+
context: "none",
|
|
2182
|
+
});
|
|
2183
|
+
return opened.workspaceId;
|
|
2184
|
+
}
|
|
2185
|
+
const opened = await workspaces.openWorkspace({
|
|
2186
|
+
path: member.path,
|
|
2187
|
+
...(member.mode ? { mode: member.mode } : {}),
|
|
2188
|
+
...(member.baseRef ? { baseRef: member.baseRef } : {}),
|
|
2189
|
+
...(member.newWorktree !== undefined ? { newWorktree: member.newWorktree } : {}),
|
|
2190
|
+
...(member.newWorkspace !== undefined ? { newWorkspace: member.newWorkspace } : {}),
|
|
2191
|
+
context: "none",
|
|
2192
|
+
}, { protectedWorkspaceIds });
|
|
2193
|
+
return opened.workspace.id;
|
|
2194
|
+
};
|
|
2195
|
+
let composite;
|
|
2196
|
+
if (memberAction === "add") {
|
|
2197
|
+
if (member.newName !== undefined) {
|
|
2198
|
+
throw new Error("Adding a Composite Workspace member does not accept member.newName.");
|
|
2199
|
+
}
|
|
2200
|
+
const purpose = member.purpose?.trim();
|
|
2201
|
+
if (!purpose)
|
|
2202
|
+
throw new Error("Adding a Composite Workspace member requires member.purpose.");
|
|
2203
|
+
const targetWorkspaceId = await resolveMemberTargetWorkspaceId();
|
|
2204
|
+
composite = compositeWorkspaces.addMember(workspaceId, {
|
|
2205
|
+
name: member.name,
|
|
2206
|
+
purpose,
|
|
2207
|
+
workspaceId: targetWorkspaceId,
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
else if (memberAction === "update") {
|
|
2211
|
+
const targetFieldsPresent = member.workspaceId !== undefined || member.path !== undefined || member.relay !== undefined ||
|
|
2212
|
+
member.mode !== undefined || member.baseRef !== undefined || member.newWorktree !== undefined ||
|
|
2213
|
+
member.newWorkspace !== undefined;
|
|
2214
|
+
if (member.newName === undefined && member.purpose === undefined && !targetFieldsPresent) {
|
|
2215
|
+
throw new Error("Updating a Composite Workspace member requires newName, purpose, or a replacement Workspace target.");
|
|
2216
|
+
}
|
|
2217
|
+
const targetWorkspaceId = targetFieldsPresent
|
|
2218
|
+
? await resolveMemberTargetWorkspaceId()
|
|
2219
|
+
: undefined;
|
|
2220
|
+
composite = compositeWorkspaces.updateMember(workspaceId, member.name, {
|
|
2221
|
+
...(member.newName !== undefined ? { name: member.newName } : {}),
|
|
2222
|
+
...(member.purpose !== undefined ? { purpose: member.purpose } : {}),
|
|
2223
|
+
...(targetWorkspaceId !== undefined ? { workspaceId: targetWorkspaceId } : {}),
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
if (member.newName !== undefined || member.purpose !== undefined || member.workspaceId !== undefined || member.path !== undefined ||
|
|
2228
|
+
member.relay !== undefined || member.mode !== undefined || member.baseRef !== undefined ||
|
|
2229
|
+
member.newWorktree !== undefined || member.newWorkspace !== undefined) {
|
|
2230
|
+
throw new Error("Removing a Composite Workspace member accepts only member.name.");
|
|
2231
|
+
}
|
|
2232
|
+
composite = compositeWorkspaces.removeMember(workspaceId, member.name);
|
|
2233
|
+
}
|
|
2234
|
+
const memberActionVerb = memberAction === "add"
|
|
2235
|
+
? "Added"
|
|
2236
|
+
: memberAction === "update"
|
|
2237
|
+
? "Updated"
|
|
2238
|
+
: "Removed";
|
|
2239
|
+
const memberActionPreposition = memberAction === "remove" ? "from" : "in";
|
|
2240
|
+
const instruction = [
|
|
2241
|
+
`${memberActionVerb} member ${member.name} ${memberActionPreposition} Composite Workspace ${composite.name} (${composite.id}).`,
|
|
2242
|
+
composite.members.length > 0
|
|
2243
|
+
? `Members: ${composite.members.map((entry) => `${entry.name} — ${entry.purpose}`).join("; ")}.`
|
|
2244
|
+
: "This Composite Workspace currently has no members.",
|
|
2245
|
+
"Use the Composite workspaceId as the top-level handle. Work operations on it require an explicit member name; ForgeRelay never infers a member from tool type or purpose.",
|
|
2246
|
+
].join("\n");
|
|
2247
|
+
const response = {
|
|
2248
|
+
content: [textBlock(instruction)],
|
|
2249
|
+
_meta: {
|
|
2250
|
+
tool: "open_workspace",
|
|
2251
|
+
card: {
|
|
2252
|
+
workspaceId: composite.id,
|
|
2253
|
+
kind: "composite",
|
|
2254
|
+
name: composite.name,
|
|
2255
|
+
path: composite.name,
|
|
2256
|
+
members: composite.members,
|
|
2257
|
+
instruction,
|
|
2258
|
+
summary: { members: composite.members.length },
|
|
2259
|
+
},
|
|
2260
|
+
},
|
|
2261
|
+
structuredContent: {
|
|
2262
|
+
action: "member",
|
|
2263
|
+
workspaceId: composite.id,
|
|
2264
|
+
memberAction,
|
|
2265
|
+
kind: "composite",
|
|
2266
|
+
name: composite.name,
|
|
2267
|
+
members: composite.members,
|
|
2268
|
+
instruction,
|
|
2269
|
+
},
|
|
2270
|
+
};
|
|
2271
|
+
rememberWorkspacePanelState(composite.id, response);
|
|
2272
|
+
return response;
|
|
2273
|
+
}
|
|
1903
2274
|
if (action === "list") {
|
|
1904
|
-
if (path !== undefined || baseRef !== undefined || newWorktree !== undefined ||
|
|
2275
|
+
if (path !== undefined || relay !== undefined || name !== undefined || memberName !== undefined || baseRef !== undefined || newWorktree !== undefined ||
|
|
1905
2276
|
newWorkspace !== undefined || context !== undefined) {
|
|
1906
|
-
throw new Error("open_workspace action=list does not accept path, baseRef, newWorktree, newWorkspace, or context. Use root/workspaceId/mode/status/state/staleOnly for inventory filters.");
|
|
2277
|
+
throw new Error("open_workspace action=list does not accept path, relay, name, memberName, baseRef, newWorktree, newWorkspace, or context. Use kind/root/workspaceId/mode/status/state/staleOnly for inventory filters.");
|
|
2278
|
+
}
|
|
2279
|
+
if (kind === "composite") {
|
|
2280
|
+
if (root !== undefined || mode !== undefined || status !== undefined || state !== undefined ||
|
|
2281
|
+
staleOnly !== undefined || offset !== undefined || limit !== undefined) {
|
|
2282
|
+
throw new Error("Composite Workspace inventory does not accept root/mode/status/state/staleOnly/offset/limit filters; use workspaceId when selecting one Composite Workspace.");
|
|
2283
|
+
}
|
|
2284
|
+
const composites = compositeWorkspaces.list()
|
|
2285
|
+
.filter((entry) => workspaceId === undefined || entry.id === workspaceId)
|
|
2286
|
+
.map((entry) => ({
|
|
2287
|
+
workspaceId: entry.id,
|
|
2288
|
+
kind: entry.kind,
|
|
2289
|
+
name: entry.name,
|
|
2290
|
+
members: entry.members,
|
|
2291
|
+
createdAt: entry.createdAt,
|
|
2292
|
+
lastUsedAt: entry.lastUsedAt,
|
|
2293
|
+
}));
|
|
2294
|
+
const instruction = "Resume a Composite Workspace with open_workspace(action=\"open\", workspaceId=...). Use close_workspace only when the user chooses to dissolve it.";
|
|
2295
|
+
const result = [
|
|
2296
|
+
`Composite Workspace inventory: ${composites.length} matching record${composites.length === 1 ? "" : "s"}.`,
|
|
2297
|
+
...composites.map((entry) => `${entry.name} [${entry.workspaceId}] members=${entry.members.length} last-used=${entry.lastUsedAt}`),
|
|
2298
|
+
instruction,
|
|
2299
|
+
].join("\n");
|
|
2300
|
+
return {
|
|
2301
|
+
content: [textBlock(result)],
|
|
2302
|
+
structuredContent: {
|
|
2303
|
+
action: "list",
|
|
2304
|
+
compositeWorkspaces: composites,
|
|
2305
|
+
instruction,
|
|
2306
|
+
},
|
|
2307
|
+
};
|
|
1907
2308
|
}
|
|
1908
2309
|
const inventory = await workspaces.listWorkspaces({ workspaceId, mode, root, status, state, staleOnly, offset, limit }, { conversationScopeId, protectedWorkspaceIds });
|
|
2310
|
+
const composites = kind === "workspace"
|
|
2311
|
+
? []
|
|
2312
|
+
: compositeWorkspaces.list().map((entry) => ({
|
|
2313
|
+
workspaceId: entry.id,
|
|
2314
|
+
kind: entry.kind,
|
|
2315
|
+
name: entry.name,
|
|
2316
|
+
members: entry.members,
|
|
2317
|
+
createdAt: entry.createdAt,
|
|
2318
|
+
lastUsedAt: entry.lastUsedAt,
|
|
2319
|
+
}));
|
|
1909
2320
|
const nextOffset = inventory.page.offset + inventory.page.limit;
|
|
1910
2321
|
const instruction = [
|
|
1911
2322
|
"Resume a selected workspaceId with open_workspace(action=\"open\", workspaceId=...).",
|
|
1912
|
-
"Use close_workspace only after the user chooses cleanup; never close inventory entries automatically.",
|
|
2323
|
+
"Use close_workspace only after the user chooses cleanup or Composite dissolution; never close inventory entries automatically.",
|
|
1913
2324
|
inventory.page.hasMore
|
|
1914
2325
|
? `More matching workspaces are available; continue with offset=${nextOffset}.`
|
|
1915
2326
|
: undefined,
|
|
1916
2327
|
].filter(Boolean).join(" ");
|
|
1917
2328
|
const result = [
|
|
1918
|
-
`Logical workspace inventory: ${inventory.summary.matching} matching
|
|
2329
|
+
`Logical workspace inventory: ${inventory.summary.matching} matching ordinary records; ${composites.length} Composite Workspace record${composites.length === 1 ? "" : "s"}.`,
|
|
1919
2330
|
`States: active=${inventory.summary.active}, stale=${inventory.summary.stale}, invalid=${inventory.summary.invalid}, closed=${inventory.summary.closed}.`,
|
|
1920
2331
|
...inventory.workspaces.map((entry) => [
|
|
1921
2332
|
entry.label,
|
|
@@ -1927,6 +2338,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1927
2338
|
`root=${entry.root}`,
|
|
1928
2339
|
`last-used=${entry.lastUsedAt}`,
|
|
1929
2340
|
].filter(Boolean).join(" ")),
|
|
2341
|
+
...composites.map((entry) => `${entry.name} [${entry.workspaceId}] kind=composite members=${entry.members.length}`),
|
|
1930
2342
|
instruction,
|
|
1931
2343
|
].join("\n");
|
|
1932
2344
|
logToolCall(config, {
|
|
@@ -1941,6 +2353,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1941
2353
|
structuredContent: {
|
|
1942
2354
|
action: "list",
|
|
1943
2355
|
...inventory,
|
|
2356
|
+
...(composites.length > 0 ? { compositeWorkspaces: composites } : {}),
|
|
1944
2357
|
instruction,
|
|
1945
2358
|
},
|
|
1946
2359
|
};
|
|
@@ -1949,6 +2362,162 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1949
2362
|
staleOnly !== undefined || offset !== undefined || limit !== undefined) {
|
|
1950
2363
|
throw new Error("open_workspace inventory filters root, status, state, staleOnly, offset, and limit are only valid with action=list.");
|
|
1951
2364
|
}
|
|
2365
|
+
const openingComposite = kind === "composite" ||
|
|
2366
|
+
(workspaceId !== undefined && compositeWorkspaces.has(workspaceId));
|
|
2367
|
+
if (openingComposite) {
|
|
2368
|
+
if (relay !== undefined || path !== undefined || mode !== undefined || baseRef !== undefined ||
|
|
2369
|
+
newWorktree !== undefined || newWorkspace !== undefined) {
|
|
2370
|
+
throw new Error("Composite Workspace open accepts name/workspaceId/context only; members are attached separately and keep their own Workspace definitions.");
|
|
2371
|
+
}
|
|
2372
|
+
if (workspaceId !== undefined && kind === "workspace") {
|
|
2373
|
+
throw new Error(`${workspaceId} is a Composite Workspace, not an ordinary Workspace.`);
|
|
2374
|
+
}
|
|
2375
|
+
const composite = workspaceId !== undefined
|
|
2376
|
+
? compositeWorkspaces.open(workspaceId)
|
|
2377
|
+
: compositeWorkspaces.create(name ?? "");
|
|
2378
|
+
const memberContext = memberName
|
|
2379
|
+
? await loadCompositeMemberContext(composite.id, memberName, context ?? "auto", conversationScopeId, protectedWorkspaceIds)
|
|
2380
|
+
: undefined;
|
|
2381
|
+
const instruction = [
|
|
2382
|
+
`This is Composite Workspace ${composite.name} (${composite.id}).`,
|
|
2383
|
+
"It has no mounted working directory of its own. Use the Composite workspaceId as the top-level Workspace handle and explicitly select one named member for member-scoped work operations.",
|
|
2384
|
+
composite.members.length > 0
|
|
2385
|
+
? `Members: ${composite.members.map((member) => `${member.name} — ${member.purpose}`).join("; ")}.`
|
|
2386
|
+
: "This Composite Workspace currently has no members.",
|
|
2387
|
+
"Member names and purposes are structural context and are always returned when this Composite Workspace is opened. context=auto/full/none controls only heavy member bootstrap context, not this Composite identity.",
|
|
2388
|
+
composite.members.length > 0
|
|
2389
|
+
? "Before first work on a member, reopen this Composite Workspace with memberName=<member> and context=auto to receive that member's project bootstrap without creating an implicit current member."
|
|
2390
|
+
: undefined,
|
|
2391
|
+
"Use close_workspace only when the user chooses to dissolve this Composite Workspace; dissolution does not close or clean up member Workspaces.",
|
|
2392
|
+
].join("\n\n");
|
|
2393
|
+
const response = {
|
|
2394
|
+
content: [textBlock(instruction)],
|
|
2395
|
+
_meta: {
|
|
2396
|
+
tool: "open_workspace",
|
|
2397
|
+
card: {
|
|
2398
|
+
workspaceId: composite.id,
|
|
2399
|
+
kind: "composite",
|
|
2400
|
+
name: composite.name,
|
|
2401
|
+
path: composite.name,
|
|
2402
|
+
members: composite.members,
|
|
2403
|
+
instruction,
|
|
2404
|
+
summary: { members: composite.members.length },
|
|
2405
|
+
},
|
|
2406
|
+
},
|
|
2407
|
+
structuredContent: {
|
|
2408
|
+
action: "open",
|
|
2409
|
+
workspaceId: composite.id,
|
|
2410
|
+
kind: "composite",
|
|
2411
|
+
name: composite.name,
|
|
2412
|
+
members: composite.members,
|
|
2413
|
+
...(memberContext ? { memberContext } : {}),
|
|
2414
|
+
instruction,
|
|
2415
|
+
},
|
|
2416
|
+
};
|
|
2417
|
+
logToolCall(config, {
|
|
2418
|
+
tool: "open_workspace",
|
|
2419
|
+
action: "composite",
|
|
2420
|
+
success: true,
|
|
2421
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2422
|
+
});
|
|
2423
|
+
rememberWorkspacePanelState(composite.id, response);
|
|
2424
|
+
return response;
|
|
2425
|
+
}
|
|
2426
|
+
if (name !== undefined || memberName !== undefined) {
|
|
2427
|
+
throw new Error("open_workspace name/memberName are only valid for a Composite Workspace.");
|
|
2428
|
+
}
|
|
2429
|
+
if (relay !== undefined) {
|
|
2430
|
+
if (workspaceId !== undefined) {
|
|
2431
|
+
throw new Error("Relayed open_workspace requires a path; resuming a relayed workspace is not available in this tracer bullet.");
|
|
2432
|
+
}
|
|
2433
|
+
if (!path)
|
|
2434
|
+
throw new Error("Relayed open_workspace requires path.");
|
|
2435
|
+
const opened = await remoteWorkspaces.openWorkspace(relay, {
|
|
2436
|
+
path,
|
|
2437
|
+
mode,
|
|
2438
|
+
baseRef,
|
|
2439
|
+
newWorktree,
|
|
2440
|
+
newWorkspace,
|
|
2441
|
+
context,
|
|
2442
|
+
}, hostScopeIdFor(_meta, sessionId));
|
|
2443
|
+
const relayedSkills = Array.isArray(opened.skills)
|
|
2444
|
+
? opened.skills
|
|
2445
|
+
: [];
|
|
2446
|
+
const relayedCapabilities = Array.isArray(opened.capabilityCatalog)
|
|
2447
|
+
? opened.capabilityCatalog
|
|
2448
|
+
: [];
|
|
2449
|
+
const result = [
|
|
2450
|
+
`Opened relayed workspace ${opened.workspaceId}.`,
|
|
2451
|
+
`Execution remote: ${relay}`,
|
|
2452
|
+
`Root: ${opened.root}`,
|
|
2453
|
+
`Mode: ${opened.mode}`,
|
|
2454
|
+
relayedSkills.length > 0
|
|
2455
|
+
? `Available skills: ${relayedSkills.map((skill) => String(skill.name ?? "")).filter(Boolean).join(", ")}`
|
|
2456
|
+
: undefined,
|
|
2457
|
+
relayedCapabilities.length > 0
|
|
2458
|
+
? `Optional capabilities: ${relayedCapabilities.map((entry) => String(entry.name ?? "")).filter(Boolean).join(", ")}`
|
|
2459
|
+
: undefined,
|
|
2460
|
+
opened.instruction,
|
|
2461
|
+
].filter(Boolean).join("\n");
|
|
2462
|
+
const response = {
|
|
2463
|
+
content: [textBlock(result)],
|
|
2464
|
+
_meta: {
|
|
2465
|
+
tool: "open_workspace",
|
|
2466
|
+
card: {
|
|
2467
|
+
workspaceId: opened.workspaceId,
|
|
2468
|
+
kind: "workspace",
|
|
2469
|
+
root: opened.root,
|
|
2470
|
+
path: opened.root,
|
|
2471
|
+
mode: opened.mode,
|
|
2472
|
+
relay,
|
|
2473
|
+
instruction: opened.instruction,
|
|
2474
|
+
summary: { mode: opened.mode, relay },
|
|
2475
|
+
},
|
|
2476
|
+
},
|
|
2477
|
+
structuredContent: {
|
|
2478
|
+
action: "open",
|
|
2479
|
+
workspaceId: opened.workspaceId,
|
|
2480
|
+
kind: "workspace",
|
|
2481
|
+
root: opened.root,
|
|
2482
|
+
mode: opened.mode,
|
|
2483
|
+
...(opened.sourceRoot ? { sourceRoot: opened.sourceRoot } : {}),
|
|
2484
|
+
...(opened.contextFingerprint !== undefined
|
|
2485
|
+
? { contextFingerprint: opened.contextFingerprint }
|
|
2486
|
+
: {}),
|
|
2487
|
+
...(opened.capabilityFingerprint !== undefined
|
|
2488
|
+
? { capabilityFingerprint: opened.capabilityFingerprint }
|
|
2489
|
+
: {}),
|
|
2490
|
+
...(opened.capabilityCatalog !== undefined
|
|
2491
|
+
? { capabilityCatalog: opened.capabilityCatalog }
|
|
2492
|
+
: {}),
|
|
2493
|
+
...(opened.capabilityGuides !== undefined
|
|
2494
|
+
? { capabilityGuides: opened.capabilityGuides }
|
|
2495
|
+
: {}),
|
|
2496
|
+
...(opened.agentsFiles !== undefined ? { agentsFiles: opened.agentsFiles } : {}),
|
|
2497
|
+
...(opened.availableAgentsFiles !== undefined
|
|
2498
|
+
? { availableAgentsFiles: opened.availableAgentsFiles }
|
|
2499
|
+
: {}),
|
|
2500
|
+
...(opened.skills !== undefined ? { skills: opened.skills } : {}),
|
|
2501
|
+
...(opened.agentProviders !== undefined
|
|
2502
|
+
? { agentProviders: opened.agentProviders }
|
|
2503
|
+
: {}),
|
|
2504
|
+
...(opened.agents !== undefined ? { agents: opened.agents } : {}),
|
|
2505
|
+
...(opened.skillDiagnostics !== undefined
|
|
2506
|
+
? { skillDiagnostics: opened.skillDiagnostics }
|
|
2507
|
+
: {}),
|
|
2508
|
+
instruction: opened.instruction,
|
|
2509
|
+
},
|
|
2510
|
+
};
|
|
2511
|
+
logToolCall(config, {
|
|
2512
|
+
tool: "open_workspace",
|
|
2513
|
+
action: "relay",
|
|
2514
|
+
path: opened.root,
|
|
2515
|
+
success: true,
|
|
2516
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2517
|
+
});
|
|
2518
|
+
rememberWorkspacePanelState(opened.workspaceId, response);
|
|
2519
|
+
return response;
|
|
2520
|
+
}
|
|
1952
2521
|
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
|
|
1953
2522
|
conversationScopeId,
|
|
1954
2523
|
protectedWorkspaceIds,
|
|
@@ -2086,6 +2655,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2086
2655
|
tool: "open_workspace",
|
|
2087
2656
|
card: {
|
|
2088
2657
|
workspaceId: workspace.id,
|
|
2658
|
+
kind: "workspace",
|
|
2089
2659
|
root: workspace.root,
|
|
2090
2660
|
path: workspace.root,
|
|
2091
2661
|
mode: workspace.mode,
|
|
@@ -2118,6 +2688,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2118
2688
|
structuredContent: {
|
|
2119
2689
|
action: "open",
|
|
2120
2690
|
workspaceId: workspace.id,
|
|
2691
|
+
kind: "workspace",
|
|
2121
2692
|
root: workspace.root,
|
|
2122
2693
|
mode: workspace.mode,
|
|
2123
2694
|
sourceRoot: workspace.sourceRoot,
|
|
@@ -2144,12 +2715,41 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2144
2715
|
rememberWorkspacePanelState(workspace.id, response);
|
|
2145
2716
|
return response;
|
|
2146
2717
|
});
|
|
2147
|
-
registerActivityQueryTools(server, activityQueries, connectionScopeId, toolWidgetDescriptorMeta(config, "activity")._meta, config.activityPanelExpanded, config.logging,
|
|
2718
|
+
registerActivityQueryTools(server, activityQueries, connectionScopeId, toolWidgetDescriptorMeta(config, "activity")._meta, config.activityPanelExpanded, config.logging, workspacePanelState, {
|
|
2719
|
+
panel: async (workspaceId, conversationScopeId) => {
|
|
2720
|
+
if (compositeWorkspaces.has(workspaceId)) {
|
|
2721
|
+
return compositeActivity.beginPanel(workspaceId, conversationScopeId);
|
|
2722
|
+
}
|
|
2723
|
+
return remoteWorkspaces.has(workspaceId)
|
|
2724
|
+
? remoteWorkspaces.activityPanel(workspaceId, conversationScopeId)
|
|
2725
|
+
: undefined;
|
|
2726
|
+
},
|
|
2727
|
+
snapshot: async (input, conversationScopeId) => {
|
|
2728
|
+
const compositeTurnId = input.turnId ?? (input.workspaceId && compositeWorkspaces.has(input.workspaceId)
|
|
2729
|
+
? compositeActivity.currentTurnId(conversationScopeId, input.workspaceId)
|
|
2730
|
+
: undefined);
|
|
2731
|
+
if (compositeTurnId) {
|
|
2732
|
+
const composite = await compositeActivity.snapshot(compositeTurnId, input.knownRevision);
|
|
2733
|
+
if (composite)
|
|
2734
|
+
return composite;
|
|
2735
|
+
}
|
|
2736
|
+
return remoteWorkspaces.activitySnapshot(input, conversationScopeId);
|
|
2737
|
+
},
|
|
2738
|
+
detail: async (turnId, activityId, conversationScopeId) => {
|
|
2739
|
+
const composite = await compositeActivity.detail(turnId, activityId);
|
|
2740
|
+
return composite ?? remoteWorkspaces.activityDetail(turnId, activityId, conversationScopeId);
|
|
2741
|
+
},
|
|
2742
|
+
output: async (turnId, outputId, conversationScopeId) => {
|
|
2743
|
+
const composite = await compositeActivity.output(turnId, outputId);
|
|
2744
|
+
return composite ?? remoteWorkspaces.activityOutput(turnId, outputId, conversationScopeId);
|
|
2745
|
+
},
|
|
2746
|
+
});
|
|
2148
2747
|
registerAppTool(server, toolNames.capability, {
|
|
2149
2748
|
title: "Use optional capability",
|
|
2150
2749
|
description: "Describe or run one optional ForgeRelay capability advertised by open_workspace. Use describe when the capability contract is unfamiliar, then read its advertised guide if needed. Run dispatches only explicitly registered capabilities; it cannot invoke arbitrary shell commands, URLs, or methods.",
|
|
2151
2750
|
inputSchema: {
|
|
2152
2751
|
workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
|
|
2752
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name whose capability surface is used."),
|
|
2153
2753
|
name: z
|
|
2154
2754
|
.string()
|
|
2155
2755
|
.regex(/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/)
|
|
@@ -2181,9 +2781,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2181
2781
|
idempotentHint: false,
|
|
2182
2782
|
openWorldHint: true,
|
|
2183
2783
|
},
|
|
2184
|
-
}, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
|
|
2784
|
+
}, async ({ workspaceId, member, name, action, arguments: capabilityArguments, file }, extra) => {
|
|
2785
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
2786
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
2787
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
2788
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
2789
|
+
return presentExecutionResult(await remoteWorkspaces.capability(executionWorkspaceId, {
|
|
2790
|
+
name,
|
|
2791
|
+
action,
|
|
2792
|
+
...(capabilityArguments !== undefined ? { arguments: capabilityArguments } : {}),
|
|
2793
|
+
...(file !== undefined ? { file } : {}),
|
|
2794
|
+
}, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
2795
|
+
}
|
|
2185
2796
|
if (action === "run" && name === "batch.execute") {
|
|
2186
|
-
const workspace = workspaces.getWorkspace(
|
|
2797
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
2187
2798
|
const startedAt = performance.now();
|
|
2188
2799
|
try {
|
|
2189
2800
|
const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
|
|
@@ -2204,7 +2815,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2204
2815
|
success: true,
|
|
2205
2816
|
durationMs: Math.round(performance.now() - startedAt),
|
|
2206
2817
|
});
|
|
2207
|
-
return result;
|
|
2818
|
+
return presentExecutionResult(result, target);
|
|
2208
2819
|
}
|
|
2209
2820
|
catch (error) {
|
|
2210
2821
|
if (extra.signal.aborted)
|
|
@@ -2227,18 +2838,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2227
2838
|
capability: name,
|
|
2228
2839
|
action,
|
|
2229
2840
|
}, result.content, startedAt);
|
|
2230
|
-
return result;
|
|
2841
|
+
return presentExecutionResult(result, target);
|
|
2231
2842
|
}
|
|
2232
2843
|
}
|
|
2233
2844
|
if (action === "run") {
|
|
2234
|
-
return coreOperations.capabilityRun({ workspaceId, name, arguments: capabilityArguments, file },
|
|
2235
|
-
requestMeta: extra._meta,
|
|
2236
|
-
signal: extra.signal,
|
|
2237
|
-
sessionId: extra.sessionId,
|
|
2238
|
-
});
|
|
2845
|
+
return presentExecutionResult(await coreOperations.capabilityRun({ workspaceId: executionWorkspaceId, name, arguments: capabilityArguments, file }, executionContext), target);
|
|
2239
2846
|
}
|
|
2240
|
-
const workspace = workspaces.getWorkspace(
|
|
2241
|
-
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), { workspaceId, name, action, arguments: capabilityArguments, file }, {
|
|
2847
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
2848
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, name, action, arguments: capabilityArguments, file }, executionContext), {
|
|
2242
2849
|
signal: extra.signal,
|
|
2243
2850
|
tool: toolNames.capability,
|
|
2244
2851
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -2291,11 +2898,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2291
2898
|
return result;
|
|
2292
2899
|
}
|
|
2293
2900
|
},
|
|
2294
|
-
});
|
|
2901
|
+
}, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
|
|
2295
2902
|
});
|
|
2296
2903
|
registerAppTool(server, toolNames.closeWorkspace, {
|
|
2297
2904
|
title: "Close workspace",
|
|
2298
|
-
description: "Close one workspace after the user chooses cleanup. Checkout-backed workspaces release only the logical handle. Managed-worktree-backed workspaces
|
|
2905
|
+
description: "Close one workspace after the user chooses cleanup. Composite Workspaces dissolve here: only the Composite identity and member links are removed; member Workspaces, files, processes, worktrees, and relay routes remain intact. Checkout-backed workspaces release only the logical handle. Managed-worktree-backed workspaces run the safe finalize lifecycle (hooks, commit, fast-forward integration, cleanup) and require commitMessage. Running processes block ordinary Workspace closure.",
|
|
2299
2906
|
inputSchema: {
|
|
2300
2907
|
workspaceId: z.string().describe("Workspace identifier to close."),
|
|
2301
2908
|
commitMessage: z
|
|
@@ -2306,7 +2913,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2306
2913
|
},
|
|
2307
2914
|
outputSchema: resultOutputSchema({
|
|
2308
2915
|
workspaceId: z.string(),
|
|
2309
|
-
|
|
2916
|
+
kind: z.enum(["workspace", "composite"]).optional(),
|
|
2917
|
+
mode: z.enum(["checkout", "worktree"]).optional(),
|
|
2918
|
+
name: z.string().optional(),
|
|
2919
|
+
members: z.array(z.object({
|
|
2920
|
+
name: z.string(),
|
|
2921
|
+
purpose: z.string(),
|
|
2922
|
+
workspaceId: z.string(),
|
|
2923
|
+
})).optional(),
|
|
2924
|
+
dissolved: z.boolean().optional(),
|
|
2310
2925
|
sourceRoot: z.string().optional(),
|
|
2311
2926
|
branch: z.string().optional(),
|
|
2312
2927
|
targetBranch: z.string().optional(),
|
|
@@ -2318,6 +2933,48 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2318
2933
|
_meta: {},
|
|
2319
2934
|
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
2320
2935
|
}, async ({ workspaceId, commitMessage }, extra) => {
|
|
2936
|
+
if (compositeWorkspaces.has(workspaceId)) {
|
|
2937
|
+
if (commitMessage !== undefined) {
|
|
2938
|
+
throw new Error("close_workspace commitMessage is not valid when dissolving a Composite Workspace.");
|
|
2939
|
+
}
|
|
2940
|
+
const composite = compositeWorkspaces.dissolve(workspaceId);
|
|
2941
|
+
compositeActivity.forgetComposite(workspaceId);
|
|
2942
|
+
workspacePanelStates.delete(workspaceId);
|
|
2943
|
+
const result = [
|
|
2944
|
+
`Dissolved Composite Workspace ${composite.name} (${workspaceId}).`,
|
|
2945
|
+
composite.members.length > 0
|
|
2946
|
+
? `Preserved member Workspaces: ${composite.members.map((member) => `${member.name} [${member.workspaceId}]`).join(", ")}.`
|
|
2947
|
+
: "The Composite Workspace had no members.",
|
|
2948
|
+
"Member Workspace handles, managed worktrees, processes, files, and Workspace Relay routes were not closed or cleaned up.",
|
|
2949
|
+
].join("\n");
|
|
2950
|
+
return {
|
|
2951
|
+
content: [textBlock(result)],
|
|
2952
|
+
_meta: {
|
|
2953
|
+
tool: toolNames.closeWorkspace,
|
|
2954
|
+
card: {
|
|
2955
|
+
workspaceId,
|
|
2956
|
+
kind: "composite",
|
|
2957
|
+
name: composite.name,
|
|
2958
|
+
members: composite.members,
|
|
2959
|
+
dissolved: true,
|
|
2960
|
+
payload: { content: [textBlock(result)] },
|
|
2961
|
+
},
|
|
2962
|
+
},
|
|
2963
|
+
structuredContent: {
|
|
2964
|
+
result,
|
|
2965
|
+
workspaceId,
|
|
2966
|
+
kind: "composite",
|
|
2967
|
+
name: composite.name,
|
|
2968
|
+
members: composite.members,
|
|
2969
|
+
dissolved: true,
|
|
2970
|
+
},
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
if (remoteWorkspaces.has(workspaceId)) {
|
|
2974
|
+
const response = await remoteWorkspaces.closeWorkspace(workspaceId, commitMessage, hostScopeIdFor(extra._meta, extra.sessionId));
|
|
2975
|
+
workspacePanelStates.delete(workspaceId);
|
|
2976
|
+
return response;
|
|
2977
|
+
}
|
|
2321
2978
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
2322
2979
|
const response = await runToolWithHooks(hooks, {
|
|
2323
2980
|
signal: extra.signal,
|
|
@@ -2429,6 +3086,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2429
3086
|
workspaceId: z
|
|
2430
3087
|
.string()
|
|
2431
3088
|
.describe("Workspace identifier returned by open_workspace."),
|
|
3089
|
+
member: z
|
|
3090
|
+
.string()
|
|
3091
|
+
.optional()
|
|
3092
|
+
.describe("Required for a Composite Workspace; explicit member name that owns this operation."),
|
|
2432
3093
|
path: z
|
|
2433
3094
|
.string()
|
|
2434
3095
|
.optional()
|
|
@@ -2466,27 +3127,27 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2466
3127
|
}),
|
|
2467
3128
|
...toolWidgetDescriptorMeta(config, "read"),
|
|
2468
3129
|
annotations: { readOnlyHint: true },
|
|
2469
|
-
}, async ({ workspaceId, path, paths, offset, limit }, extra) => {
|
|
3130
|
+
}, async ({ workspaceId, member, path, paths, offset, limit }, extra) => {
|
|
2470
3131
|
if ((path === undefined) === (paths === undefined)) {
|
|
2471
3132
|
throw new Error("read requires exactly one of path or paths.");
|
|
2472
3133
|
}
|
|
3134
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
3135
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3136
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3137
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3138
|
+
return presentExecutionResult(await remoteWorkspaces.read(executionWorkspaceId, { path, paths, offset, limit }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3139
|
+
}
|
|
2473
3140
|
if (path !== undefined) {
|
|
2474
|
-
return coreOperations.read({ workspaceId, path, offset, limit },
|
|
2475
|
-
requestMeta: extra._meta,
|
|
2476
|
-
signal: extra.signal,
|
|
2477
|
-
sessionId: extra.sessionId,
|
|
2478
|
-
});
|
|
3141
|
+
return presentExecutionResult(await coreOperations.read({ workspaceId: executionWorkspaceId, path, offset, limit }, executionContext), target);
|
|
2479
3142
|
}
|
|
2480
|
-
const workspace = workspaces.getWorkspace(
|
|
3143
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
2481
3144
|
let response;
|
|
2482
|
-
await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, { workspaceId, paths, offset, limit }, async (parentContext) => {
|
|
3145
|
+
await runActivityTool(activityLifecycle, workspace, hostScopeIdFor(extra._meta, extra.sessionId), toolNames.read, activityRequestFor({ workspaceId: executionWorkspaceId, paths, offset, limit }, executionContext), async (parentContext) => {
|
|
2483
3146
|
const execution = await executeBulkRead({
|
|
2484
3147
|
paths: paths,
|
|
2485
3148
|
signal: extra.signal,
|
|
2486
|
-
run: (childPath) => coreOperations.read({ workspaceId, path: childPath, offset, limit }, {
|
|
2487
|
-
|
|
2488
|
-
signal: extra.signal,
|
|
2489
|
-
sessionId: extra.sessionId,
|
|
3149
|
+
run: (childPath) => coreOperations.read({ workspaceId: executionWorkspaceId, path: childPath, offset, limit }, {
|
|
3150
|
+
...executionContext,
|
|
2490
3151
|
parentActivityId: parentContext.activityId,
|
|
2491
3152
|
turnId: parentContext.turnId,
|
|
2492
3153
|
}),
|
|
@@ -2527,10 +3188,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2527
3188
|
};
|
|
2528
3189
|
}, (summary) => summary.failed > 0
|
|
2529
3190
|
? { type: "failed", error: `${summary.failed} of ${summary.childCount} child Reads failed.` }
|
|
2530
|
-
: { type: "succeeded" });
|
|
3191
|
+
: { type: "succeeded" }, activityRelationFor(executionContext));
|
|
2531
3192
|
if (!response)
|
|
2532
3193
|
throw new Error("Bulk Read completed without a response.");
|
|
2533
|
-
return response;
|
|
3194
|
+
return presentExecutionResult(response, target);
|
|
2534
3195
|
});
|
|
2535
3196
|
if (config.toolMode !== "codex") {
|
|
2536
3197
|
registerAppTool(server, toolNames.write, {
|
|
@@ -2540,6 +3201,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2540
3201
|
workspaceId: z
|
|
2541
3202
|
.string()
|
|
2542
3203
|
.describe("Workspace identifier returned by open_workspace."),
|
|
3204
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
|
|
2543
3205
|
path: z
|
|
2544
3206
|
.string()
|
|
2545
3207
|
.describe("File path to write, relative to the workspace root or absolute inside the OS temp directory."),
|
|
@@ -2548,11 +3210,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2548
3210
|
outputSchema: resultOutputSchema(),
|
|
2549
3211
|
...toolWidgetDescriptorMeta(config, "write"),
|
|
2550
3212
|
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
2551
|
-
}, async ({ workspaceId, ...input }, extra) =>
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
3213
|
+
}, async ({ workspaceId, member, ...input }, extra) => {
|
|
3214
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
3215
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3216
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3217
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3218
|
+
return presentExecutionResult(await remoteWorkspaces.write(executionWorkspaceId, input, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3219
|
+
}
|
|
3220
|
+
return presentExecutionResult(await coreOperations.write({ workspaceId: executionWorkspaceId, ...input }, executionContext), target);
|
|
3221
|
+
});
|
|
2556
3222
|
registerAppTool(server, toolNames.edit, {
|
|
2557
3223
|
title: "Edit file",
|
|
2558
3224
|
description: toolDescriptions.edit,
|
|
@@ -2560,6 +3226,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2560
3226
|
workspaceId: z
|
|
2561
3227
|
.string()
|
|
2562
3228
|
.describe("Workspace identifier returned by open_workspace."),
|
|
3229
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
|
|
2563
3230
|
path: z
|
|
2564
3231
|
.string()
|
|
2565
3232
|
.optional()
|
|
@@ -2593,22 +3260,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2593
3260
|
}),
|
|
2594
3261
|
...toolWidgetDescriptorMeta(config, "edit"),
|
|
2595
3262
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
2596
|
-
}, async ({ workspaceId, path, paths, edits }, extra) => {
|
|
3263
|
+
}, async ({ workspaceId, member, path, paths, edits }, extra) => {
|
|
2597
3264
|
if ((path === undefined) === (paths === undefined)) {
|
|
2598
3265
|
throw new Error("edit requires exactly one of path or paths.");
|
|
2599
3266
|
}
|
|
3267
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
3268
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3269
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3270
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3271
|
+
return presentExecutionResult(await remoteWorkspaces.edit(executionWorkspaceId, { path, paths, edits }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3272
|
+
}
|
|
2600
3273
|
if (path !== undefined) {
|
|
2601
|
-
return coreOperations.edit({ workspaceId, path, edits },
|
|
2602
|
-
requestMeta: extra._meta,
|
|
2603
|
-
signal: extra.signal,
|
|
2604
|
-
sessionId: extra.sessionId,
|
|
2605
|
-
});
|
|
3274
|
+
return presentExecutionResult(await coreOperations.edit({ workspaceId: executionWorkspaceId, path, edits }, executionContext), target);
|
|
2606
3275
|
}
|
|
2607
|
-
return nativeBulkMutations.edit({ workspaceId, paths: paths, edits },
|
|
2608
|
-
requestMeta: extra._meta,
|
|
2609
|
-
signal: extra.signal,
|
|
2610
|
-
sessionId: extra.sessionId,
|
|
2611
|
-
});
|
|
3276
|
+
return presentExecutionResult(await nativeBulkMutations.edit({ workspaceId: executionWorkspaceId, paths: paths, edits }, executionContext), target);
|
|
2612
3277
|
});
|
|
2613
3278
|
}
|
|
2614
3279
|
registerAppTool(server, toolNames.rename, {
|
|
@@ -2616,6 +3281,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2616
3281
|
description: toolDescriptions.rename,
|
|
2617
3282
|
inputSchema: {
|
|
2618
3283
|
workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
|
|
3284
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
|
|
2619
3285
|
path: z.string().describe("Source file or directory path relative to the workspace root, or absolute inside the OS temp directory."),
|
|
2620
3286
|
newPath: z.string().describe("Destination path relative to the workspace root, or absolute inside the OS temp directory. The destination must not already exist."),
|
|
2621
3287
|
},
|
|
@@ -2626,16 +3292,21 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2626
3292
|
}),
|
|
2627
3293
|
...toolWidgetDescriptorMeta(config, "edit"),
|
|
2628
3294
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
2629
|
-
}, async ({ workspaceId, path, newPath }, extra) =>
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
3295
|
+
}, async ({ workspaceId, member, path, newPath }, extra) => {
|
|
3296
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
3297
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3298
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3299
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3300
|
+
return presentExecutionResult(await remoteWorkspaces.rename(executionWorkspaceId, { path, newPath }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3301
|
+
}
|
|
3302
|
+
return presentExecutionResult(await coreOperations.rename({ workspaceId: executionWorkspaceId, path, newPath }, executionContext), target);
|
|
3303
|
+
});
|
|
2634
3304
|
registerAppTool(server, toolNames.delete, {
|
|
2635
3305
|
title: "Delete path",
|
|
2636
3306
|
description: toolDescriptions.delete,
|
|
2637
3307
|
inputSchema: {
|
|
2638
3308
|
workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
|
|
3309
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this operation."),
|
|
2639
3310
|
path: z.string().optional().describe("One file or directory path to delete. Use exactly one of path or paths."),
|
|
2640
3311
|
paths: z
|
|
2641
3312
|
.array(z.string())
|
|
@@ -2661,22 +3332,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2661
3332
|
}),
|
|
2662
3333
|
...toolWidgetDescriptorMeta(config, "edit"),
|
|
2663
3334
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
2664
|
-
}, async ({ workspaceId, path, paths, recursive }, extra) => {
|
|
3335
|
+
}, async ({ workspaceId, member, path, paths, recursive }, extra) => {
|
|
2665
3336
|
if ((path === undefined) === (paths === undefined)) {
|
|
2666
3337
|
throw new Error("delete requires exactly one of path or paths.");
|
|
2667
3338
|
}
|
|
3339
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
3340
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3341
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3342
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3343
|
+
return presentExecutionResult(await remoteWorkspaces.delete(executionWorkspaceId, { path, paths, recursive }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3344
|
+
}
|
|
2668
3345
|
if (path !== undefined) {
|
|
2669
|
-
return coreOperations.delete({ workspaceId, path, recursive },
|
|
2670
|
-
requestMeta: extra._meta,
|
|
2671
|
-
signal: extra.signal,
|
|
2672
|
-
sessionId: extra.sessionId,
|
|
2673
|
-
});
|
|
3346
|
+
return presentExecutionResult(await coreOperations.delete({ workspaceId: executionWorkspaceId, path, recursive }, executionContext), target);
|
|
2674
3347
|
}
|
|
2675
|
-
return nativeBulkMutations.delete({ workspaceId, paths: paths, recursive },
|
|
2676
|
-
requestMeta: extra._meta,
|
|
2677
|
-
signal: extra.signal,
|
|
2678
|
-
sessionId: extra.sessionId,
|
|
2679
|
-
});
|
|
3348
|
+
return presentExecutionResult(await nativeBulkMutations.delete({ workspaceId: executionWorkspaceId, paths: paths, recursive }, executionContext), target);
|
|
2680
3349
|
});
|
|
2681
3350
|
if (config.toolMode === "codex") {
|
|
2682
3351
|
registerAppTool(server, "apply_patch", {
|
|
@@ -2686,6 +3355,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2686
3355
|
workspaceId: z
|
|
2687
3356
|
.string()
|
|
2688
3357
|
.describe("Workspace identifier returned by open_workspace."),
|
|
3358
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this patch."),
|
|
2689
3359
|
patch: z
|
|
2690
3360
|
.string()
|
|
2691
3361
|
.describe("Patch text enclosed by *** Begin Patch and *** End Patch markers."),
|
|
@@ -2701,9 +3371,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2701
3371
|
}),
|
|
2702
3372
|
...toolWidgetDescriptorMeta(config, "edit"),
|
|
2703
3373
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
2704
|
-
}, async ({ workspaceId, patch }, extra) => {
|
|
2705
|
-
const
|
|
2706
|
-
|
|
3374
|
+
}, async ({ workspaceId, member, patch }, extra) => {
|
|
3375
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
3376
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3377
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3378
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3379
|
+
return presentExecutionResult(await remoteWorkspaces.applyPatch(executionWorkspaceId, { patch }, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3380
|
+
}
|
|
3381
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
3382
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, hostScopeIdFor(extra._meta, extra.sessionId), activityRequestFor({ workspaceId: executionWorkspaceId, patch }, executionContext), {
|
|
2707
3383
|
signal: extra.signal,
|
|
2708
3384
|
tool: "apply_patch",
|
|
2709
3385
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -2731,7 +3407,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2731
3407
|
_meta: {
|
|
2732
3408
|
tool: "apply_patch",
|
|
2733
3409
|
card: {
|
|
2734
|
-
workspaceId,
|
|
3410
|
+
workspaceId: executionWorkspaceId,
|
|
2735
3411
|
path: displayPath,
|
|
2736
3412
|
summary: {
|
|
2737
3413
|
files: applied.files.length,
|
|
@@ -2750,7 +3426,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2750
3426
|
},
|
|
2751
3427
|
};
|
|
2752
3428
|
},
|
|
2753
|
-
});
|
|
3429
|
+
}, activityRelationFor(executionContext)).then((result) => presentExecutionResult(result, target));
|
|
2754
3430
|
});
|
|
2755
3431
|
}
|
|
2756
3432
|
if (config.toolMode !== "codex") {
|
|
@@ -2761,6 +3437,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2761
3437
|
workspaceId: z
|
|
2762
3438
|
.string()
|
|
2763
3439
|
.describe("Workspace identifier returned by open_workspace."),
|
|
3440
|
+
member: z.string().optional().describe("Required for a Composite Workspace; explicit member name that owns this process operation."),
|
|
2764
3441
|
action: z
|
|
2765
3442
|
.enum(["run", "process", "output"])
|
|
2766
3443
|
.optional()
|
|
@@ -2834,16 +3511,36 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2834
3511
|
outputSchema: processOutputSchema(),
|
|
2835
3512
|
...toolWidgetDescriptorMeta(config, "shell"),
|
|
2836
3513
|
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
2837
|
-
}, async ({ workspaceId, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
|
|
2838
|
-
const
|
|
3514
|
+
}, async ({ workspaceId, member, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
|
|
3515
|
+
const target = resolveExecutionTarget(workspaceId, member);
|
|
3516
|
+
const executionWorkspaceId = target.executionWorkspaceId;
|
|
3517
|
+
const executionContext = await prepareExecutionContext(target, extra._meta, extra.signal, extra.sessionId);
|
|
3518
|
+
if (remoteWorkspaces.has(executionWorkspaceId)) {
|
|
3519
|
+
return presentExecutionResult(await remoteWorkspaces.bash(executionWorkspaceId, {
|
|
3520
|
+
action,
|
|
3521
|
+
...(command !== undefined ? { command } : {}),
|
|
3522
|
+
...(processId !== undefined ? { processId } : {}),
|
|
3523
|
+
...(outputId !== undefined ? { outputId } : {}),
|
|
3524
|
+
...(input !== undefined ? { input } : {}),
|
|
3525
|
+
...(interrupt !== undefined ? { interrupt } : {}),
|
|
3526
|
+
...(tty !== undefined ? { tty } : {}),
|
|
3527
|
+
...(columns !== undefined ? { columns } : {}),
|
|
3528
|
+
...(rows !== undefined ? { rows } : {}),
|
|
3529
|
+
...(workingDirectory !== undefined ? { workingDirectory } : {}),
|
|
3530
|
+
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
3531
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
3532
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
3533
|
+
}, hostScopeIdFor(extra._meta, extra.sessionId)), target);
|
|
3534
|
+
}
|
|
3535
|
+
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
2839
3536
|
if (action === "run") {
|
|
2840
3537
|
if (!command)
|
|
2841
3538
|
throw new Error("bash action=run requires command.");
|
|
2842
3539
|
if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
|
|
2843
3540
|
throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
|
|
2844
3541
|
}
|
|
2845
|
-
return coreOperations.shellRun({
|
|
2846
|
-
workspaceId,
|
|
3542
|
+
return presentExecutionResult(await coreOperations.shellRun({
|
|
3543
|
+
workspaceId: executionWorkspaceId,
|
|
2847
3544
|
command,
|
|
2848
3545
|
surface: "bash",
|
|
2849
3546
|
tty,
|
|
@@ -2853,11 +3550,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2853
3550
|
yieldTimeMs,
|
|
2854
3551
|
timeoutMs,
|
|
2855
3552
|
maxOutputTokens,
|
|
2856
|
-
},
|
|
2857
|
-
requestMeta: extra._meta,
|
|
2858
|
-
signal: extra.signal,
|
|
2859
|
-
sessionId: extra.sessionId,
|
|
2860
|
-
});
|
|
3553
|
+
}, executionContext), target);
|
|
2861
3554
|
}
|
|
2862
3555
|
if (action === "output") {
|
|
2863
3556
|
if (!outputId)
|
|
@@ -2872,8 +3565,8 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2872
3565
|
tool: toolNames.shell,
|
|
2873
3566
|
invocation: workspaceHookInvocation(workspace),
|
|
2874
3567
|
payload: { action, outputId },
|
|
2875
|
-
operation: async () => durableOutputResponse(toolNames.shell,
|
|
2876
|
-
});
|
|
3568
|
+
operation: async () => durableOutputResponse(toolNames.shell, executionWorkspaceId, readWorkspaceBashOutput(bashOutputStore, executionWorkspaceId, outputId)),
|
|
3569
|
+
}).then((result) => presentExecutionResult(result, target));
|
|
2877
3570
|
}
|
|
2878
3571
|
if (outputId !== undefined)
|
|
2879
3572
|
throw new Error("bash action=process does not accept outputId.");
|
|
@@ -2901,7 +3594,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2901
3594
|
operation: async () => {
|
|
2902
3595
|
const startedAt = performance.now();
|
|
2903
3596
|
const snapshot = await processSessions.write({
|
|
2904
|
-
workspaceId,
|
|
3597
|
+
workspaceId: executionWorkspaceId,
|
|
2905
3598
|
processId,
|
|
2906
3599
|
chars: interrupt ? "\u0003" : input,
|
|
2907
3600
|
columns,
|
|
@@ -2919,7 +3612,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2919
3612
|
success: snapshot.running || snapshot.exitCode === 0,
|
|
2920
3613
|
durationMs: Math.round(performance.now() - startedAt),
|
|
2921
3614
|
});
|
|
2922
|
-
const response = processToolResponse(toolNames.shell,
|
|
3615
|
+
const response = processToolResponse(toolNames.shell, executionWorkspaceId, snapshot, {
|
|
2923
3616
|
action,
|
|
2924
3617
|
processId,
|
|
2925
3618
|
inputLength: input?.length ?? 0,
|
|
@@ -2931,12 +3624,20 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2931
3624
|
if (!snapshot.running) {
|
|
2932
3625
|
recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId);
|
|
2933
3626
|
}
|
|
2934
|
-
return response;
|
|
3627
|
+
return presentExecutionResult(response, target);
|
|
2935
3628
|
},
|
|
2936
3629
|
});
|
|
2937
3630
|
});
|
|
2938
3631
|
}
|
|
2939
|
-
registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context)
|
|
3632
|
+
registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context), {
|
|
3633
|
+
resolve: resolveExecutionTarget,
|
|
3634
|
+
prepare: prepareExecutionContext,
|
|
3635
|
+
present: presentExecutionResult,
|
|
3636
|
+
isRemote: (workspaceId) => remoteWorkspaces.has(workspaceId),
|
|
3637
|
+
execCommandRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.execCommand(workspaceId, input, conversationScopeId),
|
|
3638
|
+
writeStdinRemote: (workspaceId, input, conversationScopeId) => remoteWorkspaces.writeStdin(workspaceId, input, conversationScopeId),
|
|
3639
|
+
hostScopeIdFor,
|
|
3640
|
+
});
|
|
2940
3641
|
return server;
|
|
2941
3642
|
}
|
|
2942
3643
|
export function createServer(config = loadConfig(), options = {}) {
|
|
@@ -3069,6 +3770,8 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
3069
3770
|
});
|
|
3070
3771
|
app.use(createForgeRelayAuthRouter({
|
|
3071
3772
|
provider: oauthProvider,
|
|
3773
|
+
cliAuthenticationProvider: oauthProvider,
|
|
3774
|
+
instanceId: config.instanceId,
|
|
3072
3775
|
issuerUrl: new URL(config.publicBaseUrl),
|
|
3073
3776
|
resourceServerUrl,
|
|
3074
3777
|
scopesSupported: config.oauth.scopes,
|