agent-console-server 3.4.0 → 3.5.1
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/README.md +14 -12
- package/dist/EphemeralAgentEventTypes-DGkHIIcW.js +838 -0
- package/dist/cli.js +104 -26
- package/dist/contracts.d.ts +2 -2
- package/dist/contracts.js +2 -3
- package/dist/{constants-eNCfNezp.d.ts → getSignedText-DQwqr8CH.d.ts} +267 -8
- package/dist/index.d.ts +321 -11
- package/dist/index.js +3 -4
- package/dist/{readToken-BSQZ8ZVf.js → spawnPtyShell-CtAiD2BH.js} +1105 -116
- package/package.json +9 -5
- package/dist/EphemeralAgentEventTypes-Cm5oNQXu.js +0 -274
- package/dist/ServerMessage-Deh-bExC.js +0 -363
|
@@ -1,25 +1,147 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as getSignedText, V as serverMessageSchema, _ as driverCommandSchema, et as agentEventSchema, ft as todoStatusSchema, in as DEFAULT_HOSTNAME, ln as SCHEME_PAIRING_CODE_DURATION, on as HOST_SCHEME, r as handshakeMessageSchema, t as EphemeralAgentEventTypes, u as commandSchema, un as SHELL_OUTPUT_LENGTH, wt as permissionModeSchema } from "./EphemeralAgentEventTypes-DGkHIIcW.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import { ID_SEPARATOR, InvalidOperationError, Operation, exhaustiveGuard, getOrCreate, getResult, getResultAsync, noop } from "@esposter/shared";
|
|
4
|
-
import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import { ID_SEPARATOR, InvalidOperationError, Operation, SITE_NAME, createUniqueArraySchema, exhaustiveGuard, getOrCreate, getResult, getResultAsync, noop, withFinalizerAsync } from "@esposter/shared";
|
|
7
5
|
import { homedir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { dirname, join, relative, win32 } from "node:path";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify } from "node:crypto";
|
|
11
9
|
import { once } from "node:events";
|
|
10
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
11
|
+
import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
13
|
+
import { getAssetKeys, getRawAsset, isSea } from "node:sea";
|
|
12
14
|
import { createServer } from "node:http";
|
|
15
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
16
|
+
//#region src/services/device/constants.ts
|
|
17
|
+
const STATE_DIRECTORY_NAME = ".agent-console-server";
|
|
18
|
+
const DEVICES_FILENAME = "devices.json";
|
|
19
|
+
const HOST_KEY_FILENAME = "host-key.pem";
|
|
20
|
+
const SECRET_BYTE_LENGTH = 32;
|
|
21
|
+
const PRINTED_PAIRING_CODE_DURATION = Temporal.Duration.from({ minutes: 10 }).total("milliseconds");
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/services/device/getStateDirectory.ts
|
|
24
|
+
const getStateDirectory = () => join(homedir(), STATE_DIRECTORY_NAME);
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/services/device/writeStateFile.ts
|
|
27
|
+
const writeStateFile = (stateDirectory, filename, content) => {
|
|
28
|
+
const filePath = join(stateDirectory, filename);
|
|
29
|
+
const temporaryFilePath = `${filePath}.${crypto.randomUUID()}.tmp`;
|
|
30
|
+
mkdirSync(stateDirectory, { recursive: true });
|
|
31
|
+
getResult(() => {
|
|
32
|
+
writeFileSync(temporaryFilePath, content, {
|
|
33
|
+
flag: "wx",
|
|
34
|
+
mode: 384
|
|
35
|
+
});
|
|
36
|
+
renameSync(temporaryFilePath, filePath);
|
|
37
|
+
}).match(noop, (error) => {
|
|
38
|
+
getResult(() => {
|
|
39
|
+
rmSync(temporaryFilePath, { force: true });
|
|
40
|
+
}).match(noop, console.error);
|
|
41
|
+
throw error;
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/services/device/readHostKey.ts
|
|
46
|
+
const readHostKey = (stateDirectory) => {
|
|
47
|
+
const hostKeyPath = join(stateDirectory, HOST_KEY_FILENAME);
|
|
48
|
+
if (existsSync(hostKeyPath)) return createPrivateKey(readFileSync(hostKeyPath, "utf8"));
|
|
49
|
+
const { privateKey } = generateKeyPairSync("ed25519");
|
|
50
|
+
writeStateFile(stateDirectory, HOST_KEY_FILENAME, privateKey.export({
|
|
51
|
+
format: "pem",
|
|
52
|
+
type: "pkcs8"
|
|
53
|
+
}));
|
|
54
|
+
return privateKey;
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/models/device/Device.ts
|
|
58
|
+
const deviceSchema = z.object({
|
|
59
|
+
createdAt: z.coerce.date(),
|
|
60
|
+
credentialHash: z.string().min(1),
|
|
61
|
+
id: z.string().min(1),
|
|
62
|
+
name: z.string(),
|
|
63
|
+
origin: z.string()
|
|
64
|
+
});
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/services/device/readDevices.ts
|
|
67
|
+
const readDevices = (stateDirectory) => {
|
|
68
|
+
const devicesPath = join(stateDirectory, DEVICES_FILENAME);
|
|
69
|
+
if (!existsSync(devicesPath)) return [];
|
|
70
|
+
return z.array(deviceSchema).parse(JSON.parse(readFileSync(devicesPath, "utf8")));
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/services/device/writeDevices.ts
|
|
74
|
+
const writeDevices = (stateDirectory, devices) => {
|
|
75
|
+
writeStateFile(stateDirectory, DEVICES_FILENAME, JSON.stringify(devices, void 0, 2));
|
|
76
|
+
};
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/services/handshake/checkIsSignatureValid.ts
|
|
79
|
+
const checkIsSignatureValid = (key, purpose, port, nonce, signature) => verify(void 0, Buffer.from(getSignedText(purpose, port, nonce)), key, Buffer.from(signature, "base64url"));
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/services/handshake/signNonce.ts
|
|
82
|
+
const signNonce = (privateKey, purpose, port, nonce) => sign(void 0, Buffer.from(getSignedText(purpose, port, nonce)), privateKey).toString("base64url");
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/services/server/constants.ts
|
|
85
|
+
const HOST_PROBE_TIMEOUT_MS = 2e3;
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/services/shared/readMessageText.ts
|
|
88
|
+
const readMessageText = (data) => Buffer.isBuffer(data) ? data.toString() : Array.isArray(data) ? Buffer.concat(data).toString() : Buffer.from(data).toString();
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/services/server/sendToRunningHost.ts
|
|
91
|
+
const sendToRunningHost = async (hostname, port, hostKey, createMessage) => {
|
|
92
|
+
const nonce = randomBytes(32).toString("base64url");
|
|
93
|
+
const webSocket = new WebSocket(`ws://${hostname}:${port}`, { handshakeTimeout: HOST_PROBE_TIMEOUT_MS });
|
|
94
|
+
const isHost = await getResultAsync(async () => {
|
|
95
|
+
const signal = AbortSignal.timeout(HOST_PROBE_TIMEOUT_MS);
|
|
96
|
+
await once(webSocket, "open", { signal });
|
|
97
|
+
const pendingMessage = once(webSocket, "message", { signal });
|
|
98
|
+
webSocket.send(JSON.stringify({
|
|
99
|
+
nonce,
|
|
100
|
+
type: "Challenge"
|
|
101
|
+
}));
|
|
102
|
+
const [data] = await pendingMessage;
|
|
103
|
+
const serverMessage = serverMessageSchema.parse(JSON.parse(readMessageText(data)));
|
|
104
|
+
if (serverMessage.type !== "Proof" || serverMessage.port !== port || !checkIsSignatureValid(createPublicKey(hostKey), "HostProof", port, nonce, serverMessage.signature)) return false;
|
|
105
|
+
if (!createMessage) return true;
|
|
106
|
+
const pendingClose = once(webSocket, "close", { signal });
|
|
107
|
+
webSocket.send(JSON.stringify(createMessage(signNonce(hostKey, "OwnerProof", port, serverMessage.nonce))));
|
|
108
|
+
await pendingClose;
|
|
109
|
+
return true;
|
|
110
|
+
}).match((isHolderHost) => isHolderHost, () => false);
|
|
111
|
+
webSocket.terminate();
|
|
112
|
+
return isHost;
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/services/device/runDevicesCommand.ts
|
|
116
|
+
const runDevicesCommand = async ({ hostKey, hostname, port, revokeDeviceId, stateDirectory }, writeLine) => {
|
|
117
|
+
const devices = readDevices(stateDirectory);
|
|
118
|
+
if (!revokeDeviceId) {
|
|
119
|
+
if (devices.length === 0) writeLine("No page is connected to this host.");
|
|
120
|
+
for (const { createdAt, id, name, origin } of devices) writeLine(`${id} ${name} ${origin} since ${createdAt.toISOString()}`);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (!devices.some(({ id }) => id === revokeDeviceId)) {
|
|
124
|
+
writeLine(`No device has the id ${revokeDeviceId}.`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
writeDevices(stateDirectory, devices.filter(({ id }) => id !== revokeDeviceId));
|
|
128
|
+
await sendToRunningHost(hostname, port, hostKey, (signature) => ({
|
|
129
|
+
deviceId: revokeDeviceId,
|
|
130
|
+
signature,
|
|
131
|
+
type: "Revoke"
|
|
132
|
+
}));
|
|
133
|
+
writeLine(`Removed ${revokeDeviceId}. It has to connect again to use this host.`);
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
13
137
|
//#region src/services/drivers/claudeAgentSdk/getEventId.ts
|
|
14
138
|
const getEventId = (sourceId, suffix) => `${sourceId}${ID_SEPARATOR}${suffix}`;
|
|
15
139
|
//#endregion
|
|
16
|
-
//#region src/services/drivers/claudeAgentSdk/
|
|
17
|
-
const
|
|
18
|
-
if (openSessionMap.get(sessionId) !== openSession) return;
|
|
19
|
-
for (const pendingPermission of openSession.pendingPermissionMap.values()) pendingPermission.settle("Deny", "");
|
|
140
|
+
//#region src/services/drivers/claudeAgentSdk/toSessionClosedEvents.ts
|
|
141
|
+
const toSessionClosedEvents = (message) => {
|
|
20
142
|
const createdAt = /* @__PURE__ */ new Date();
|
|
21
143
|
const closedId = getEventId(crypto.randomUUID(), "SessionState");
|
|
22
|
-
|
|
144
|
+
return [...message ? [{
|
|
23
145
|
createdAt,
|
|
24
146
|
id: getEventId(closedId, "HostError"),
|
|
25
147
|
message,
|
|
@@ -29,7 +151,14 @@ const closeOpenSession = (sessionId, openSession, message, { emit, onSessionsCha
|
|
|
29
151
|
id: closedId,
|
|
30
152
|
state: "Closed",
|
|
31
153
|
type: "SessionState"
|
|
32
|
-
}]
|
|
154
|
+
}];
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/services/drivers/claudeAgentSdk/closeOpenSession.ts
|
|
158
|
+
const closeOpenSession = (sessionId, openSession, message, { emit, onSessionsChange, openSessionMap }) => {
|
|
159
|
+
if (openSessionMap.get(sessionId) !== openSession) return;
|
|
160
|
+
for (const pendingPermission of openSession.pendingPermissionMap.values()) pendingPermission.settle("Deny", "");
|
|
161
|
+
emit(sessionId, toSessionClosedEvents(message));
|
|
33
162
|
openSessionMap.delete(sessionId);
|
|
34
163
|
onSessionsChange();
|
|
35
164
|
};
|
|
@@ -845,6 +974,20 @@ const watchSession = async (sessionId, openSession, { emit, onSessionsChange, op
|
|
|
845
974
|
]);
|
|
846
975
|
};
|
|
847
976
|
//#endregion
|
|
977
|
+
//#region src/services/installer/constants.ts
|
|
978
|
+
const HOST_EXECUTABLE_FILENAME = "agent-console-host.exe";
|
|
979
|
+
const CLAUDE_CODE_EXECUTABLE_FILENAME = "claude.exe";
|
|
980
|
+
const HOST_SCHEME_REGISTRY_KEY = win32.join("HKCU", "Software", "Classes", HOST_SCHEME);
|
|
981
|
+
const HOST_DIRECTORY_ENVIRONMENT_VARIABLE = `${SITE_NAME.toUpperCase()}_HOST_DIRECTORY`;
|
|
982
|
+
const HOST_INSTALL_DIRECTORY_SEGMENTS = [SITE_NAME, "Host"];
|
|
983
|
+
const NODE_PTY_DIRECTORY_NAME = "node-pty";
|
|
984
|
+
//#endregion
|
|
985
|
+
//#region src/services/installer/getHostInstallDirectory.ts
|
|
986
|
+
const getHostInstallDirectory = () => join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), ...HOST_INSTALL_DIRECTORY_SEGMENTS);
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/services/installer/getClaudeCodeExecutablePath.ts
|
|
989
|
+
const getClaudeCodeExecutablePath = () => isSea() ? join(getHostInstallDirectory(), CLAUDE_CODE_EXECUTABLE_FILENAME) : void 0;
|
|
990
|
+
//#endregion
|
|
848
991
|
//#region src/services/drivers/claudeAgentSdk/createSessionOpener.ts
|
|
849
992
|
const createSessionOpener = ({ emit, onSessionOpen, onSessionsChange, openSessionMap, taskRegistry }) => async ({ cwd, isFork, resumeAt, resumeFrom, sessionId }) => {
|
|
850
993
|
const mapper = createSdkMessageMapper();
|
|
@@ -852,6 +995,7 @@ const createSessionOpener = ({ emit, onSessionOpen, onSessionsChange, openSessio
|
|
|
852
995
|
const toolUseResultMap = resumeFrom ? await readToolUseResultMap(resumeFrom) : /* @__PURE__ */ new Map();
|
|
853
996
|
const input = createInputQueue();
|
|
854
997
|
const pendingPermissionMap = /* @__PURE__ */ new Map();
|
|
998
|
+
const claudeCodeExecutablePath = getClaudeCodeExecutablePath();
|
|
855
999
|
const sessionQuery = query({
|
|
856
1000
|
options: {
|
|
857
1001
|
canUseTool: createPermissionBridge(sessionId, pendingPermissionMap, emit),
|
|
@@ -863,6 +1007,7 @@ const createSessionOpener = ({ emit, onSessionOpen, onSessionsChange, openSessio
|
|
|
863
1007
|
},
|
|
864
1008
|
includeHookEvents: true,
|
|
865
1009
|
includePartialMessages: true,
|
|
1010
|
+
...claudeCodeExecutablePath ? { pathToClaudeCodeExecutable: claudeCodeExecutablePath } : {},
|
|
866
1011
|
settingSources: SETTING_SOURCES,
|
|
867
1012
|
systemPrompt: {
|
|
868
1013
|
preset: "claude_code",
|
|
@@ -909,6 +1054,30 @@ const createSessionOpener = ({ emit, onSessionOpen, onSessionsChange, openSessio
|
|
|
909
1054
|
return sessionId;
|
|
910
1055
|
};
|
|
911
1056
|
//#endregion
|
|
1057
|
+
//#region src/services/drivers/claudeAgentSdk/listSessionSummaries.ts
|
|
1058
|
+
const listSessionSummaries = async (openSessionMap) => {
|
|
1059
|
+
const sessionInfos = await listSessions({ limit: 50 });
|
|
1060
|
+
const savedSessionIds = new Set(sessionInfos.map(({ sessionId }) => sessionId));
|
|
1061
|
+
const unsavedSessions = [...openSessionMap].filter(([id]) => !savedSessionIds.has(id)).map(([id, { cwd, lastActivityAt, state, title }]) => ({
|
|
1062
|
+
cwd,
|
|
1063
|
+
id,
|
|
1064
|
+
lastActivityAt,
|
|
1065
|
+
state,
|
|
1066
|
+
title
|
|
1067
|
+
}));
|
|
1068
|
+
const savedSessions = sessionInfos.map((sessionInfo) => {
|
|
1069
|
+
const openSession = openSessionMap.get(sessionInfo.sessionId);
|
|
1070
|
+
return {
|
|
1071
|
+
cwd: sessionInfo.cwd ?? "",
|
|
1072
|
+
id: sessionInfo.sessionId,
|
|
1073
|
+
lastActivityAt: openSession?.lastActivityAt ?? new Date(sessionInfo.lastModified),
|
|
1074
|
+
state: openSession?.state ?? "Closed",
|
|
1075
|
+
title: openSession?.title || getSessionTitle(sessionInfo)
|
|
1076
|
+
};
|
|
1077
|
+
});
|
|
1078
|
+
return [...unsavedSessions, ...savedSessions];
|
|
1079
|
+
};
|
|
1080
|
+
//#endregion
|
|
912
1081
|
//#region src/services/drivers/claudeAgentSdk/readSessionCwd.ts
|
|
913
1082
|
const readSessionCwd = async (sessionId) => {
|
|
914
1083
|
const sessionInfo = await getSessionInfo(sessionId);
|
|
@@ -1051,6 +1220,9 @@ const createClaudeAgentSdkDriver = ({ onEvents, onSessionOpen, onSessionsChange
|
|
|
1051
1220
|
});
|
|
1052
1221
|
};
|
|
1053
1222
|
return {
|
|
1223
|
+
backgroundTasks: async (sessionId) => {
|
|
1224
|
+
await getOpenSession(sessionId).query.backgroundTasks();
|
|
1225
|
+
},
|
|
1054
1226
|
close: async () => {
|
|
1055
1227
|
for (const sessionId of openSessionMap.keys()) closeSession(sessionId);
|
|
1056
1228
|
await taskRegistry.drain();
|
|
@@ -1076,28 +1248,7 @@ const createClaudeAgentSdkDriver = ({ onEvents, onSessionOpen, onSessionsChange
|
|
|
1076
1248
|
interrupt: async (sessionId) => {
|
|
1077
1249
|
await getOpenSession(sessionId).query.interrupt();
|
|
1078
1250
|
},
|
|
1079
|
-
listSessions:
|
|
1080
|
-
const sessionInfos = await listSessions({ limit: 50 });
|
|
1081
|
-
const savedSessionIds = new Set(sessionInfos.map(({ sessionId }) => sessionId));
|
|
1082
|
-
const unsavedSessions = [...openSessionMap].filter(([id]) => !savedSessionIds.has(id)).map(([id, { cwd, lastActivityAt, state, title }]) => ({
|
|
1083
|
-
cwd,
|
|
1084
|
-
id,
|
|
1085
|
-
lastActivityAt,
|
|
1086
|
-
state,
|
|
1087
|
-
title
|
|
1088
|
-
}));
|
|
1089
|
-
const savedSessions = sessionInfos.map((sessionInfo) => {
|
|
1090
|
-
const openSession = openSessionMap.get(sessionInfo.sessionId);
|
|
1091
|
-
return {
|
|
1092
|
-
cwd: sessionInfo.cwd ?? "",
|
|
1093
|
-
id: sessionInfo.sessionId,
|
|
1094
|
-
lastActivityAt: openSession?.lastActivityAt ?? new Date(sessionInfo.lastModified),
|
|
1095
|
-
state: openSession?.state ?? "Closed",
|
|
1096
|
-
title: openSession?.title || getSessionTitle(sessionInfo)
|
|
1097
|
-
};
|
|
1098
|
-
});
|
|
1099
|
-
return [...unsavedSessions, ...savedSessions];
|
|
1100
|
-
},
|
|
1251
|
+
listSessions: () => listSessionSummaries(openSessionMap),
|
|
1101
1252
|
prompt,
|
|
1102
1253
|
resolvePermission: (sessionId, requestId, behavior, message) => {
|
|
1103
1254
|
getOpenSession(sessionId).pendingPermissionMap.get(requestId)?.settle(behavior, message);
|
|
@@ -1143,22 +1294,86 @@ const createClaudeAgentSdkDriver = ({ onEvents, onSessionOpen, onSessionsChange
|
|
|
1143
1294
|
const { mapper, query } = getOpenSession(sessionId);
|
|
1144
1295
|
await query.setPermissionMode(permissionMode);
|
|
1145
1296
|
emit(sessionId, [mapper.updateSettings(crypto.randomUUID(), /* @__PURE__ */ new Date(), { permissionMode })]);
|
|
1297
|
+
},
|
|
1298
|
+
stopTask: async (sessionId, taskId) => {
|
|
1299
|
+
await getOpenSession(sessionId).query.stopTask(taskId);
|
|
1146
1300
|
}
|
|
1147
1301
|
};
|
|
1148
1302
|
};
|
|
1149
1303
|
//#endregion
|
|
1150
|
-
//#region src/services/
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1304
|
+
//#region src/services/drivers/window/constants.ts
|
|
1305
|
+
const SESSION_SUBCOMMAND = "session";
|
|
1306
|
+
const SESSION_SECRET_ENVIRONMENT_VARIABLE = `${SITE_NAME.toUpperCase()}_SESSION_SECRET`;
|
|
1307
|
+
const SESSION_SECRET_BYTE_LENGTH = 32;
|
|
1308
|
+
const SESSION_WINDOW_CONNECT_TIMEOUT = Temporal.Duration.from({ seconds: 30 }).total("milliseconds");
|
|
1309
|
+
//#endregion
|
|
1310
|
+
//#region src/models/window/BaseChildMessage.ts
|
|
1311
|
+
const createBaseChildMessageSchema = (typeSchema) => z.object({ type: typeSchema });
|
|
1312
|
+
//#endregion
|
|
1313
|
+
//#region src/models/window/ChildMessageType.ts
|
|
1314
|
+
let ChildMessageType = /* @__PURE__ */ function(ChildMessageType) {
|
|
1315
|
+
ChildMessageType["CommandError"] = "CommandError";
|
|
1316
|
+
ChildMessageType["CommandResult"] = "CommandResult";
|
|
1317
|
+
ChildMessageType["Events"] = "Events";
|
|
1318
|
+
ChildMessageType["SessionOpen"] = "SessionOpen";
|
|
1319
|
+
ChildMessageType["SessionsChange"] = "SessionsChange";
|
|
1320
|
+
return ChildMessageType;
|
|
1321
|
+
}({});
|
|
1322
|
+
const childMessageTypeSchema = z.enum(ChildMessageType);
|
|
1323
|
+
//#endregion
|
|
1324
|
+
//#region src/models/window/ChildCommandErrorMessage.ts
|
|
1325
|
+
const childCommandErrorMessageSchema = z.object({
|
|
1326
|
+
...createBaseChildMessageSchema(z.literal("CommandError")).shape,
|
|
1327
|
+
commandId: z.string(),
|
|
1328
|
+
message: z.string().min(1)
|
|
1329
|
+
});
|
|
1330
|
+
//#endregion
|
|
1331
|
+
//#region src/models/window/ChildCommandResultMessage.ts
|
|
1332
|
+
const childCommandResultMessageSchema = z.object({
|
|
1333
|
+
...createBaseChildMessageSchema(z.literal("CommandResult")).shape,
|
|
1334
|
+
commandId: z.string().min(1),
|
|
1335
|
+
sessionId: z.string()
|
|
1336
|
+
});
|
|
1337
|
+
//#endregion
|
|
1338
|
+
//#region src/models/window/ChildEventsMessage.ts
|
|
1339
|
+
const childEventsMessageSchema = z.object({
|
|
1340
|
+
...createBaseChildMessageSchema(z.literal("Events")).shape,
|
|
1341
|
+
events: createUniqueArraySchema(agentEventSchema, "id"),
|
|
1342
|
+
sessionId: z.string().min(1)
|
|
1343
|
+
});
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region src/models/window/ChildSessionOpenMessage.ts
|
|
1346
|
+
const childSessionOpenMessageSchema = z.object({
|
|
1347
|
+
...createBaseChildMessageSchema(z.literal("SessionOpen")).shape,
|
|
1348
|
+
sessionId: z.string().min(1)
|
|
1349
|
+
});
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#region src/models/window/ChildSessionsChangeMessage.ts
|
|
1352
|
+
const childSessionsChangeMessageSchema = z.object({ ...createBaseChildMessageSchema(z.literal("SessionsChange")).shape });
|
|
1353
|
+
//#endregion
|
|
1354
|
+
//#region src/models/window/ChildMessage.ts
|
|
1355
|
+
const childMessageSchema = z.discriminatedUnion("type", [
|
|
1356
|
+
childCommandErrorMessageSchema,
|
|
1357
|
+
childCommandResultMessageSchema,
|
|
1358
|
+
childEventsMessageSchema,
|
|
1359
|
+
childSessionOpenMessageSchema,
|
|
1360
|
+
childSessionsChangeMessageSchema
|
|
1361
|
+
]);
|
|
1362
|
+
//#endregion
|
|
1363
|
+
//#region src/services/drivers/window/sendChildCommand.ts
|
|
1364
|
+
const sendChildCommand = (child, command) => {
|
|
1365
|
+
if (child.webSocket.readyState !== WebSocket.OPEN) throw new InvalidOperationError(Operation.Update, child.cwd, "the session's window closed");
|
|
1366
|
+
const reply = Promise.withResolvers();
|
|
1367
|
+
child.pendingReplyMap.set(command.id, reply);
|
|
1368
|
+
child.webSocket.send(JSON.stringify(command));
|
|
1369
|
+
return reply.promise;
|
|
1370
|
+
};
|
|
1371
|
+
//#endregion
|
|
1372
|
+
//#region src/services/drivers/window/takeChildReply.ts
|
|
1373
|
+
const takeChildReply = ({ pendingReplyMap }, commandId) => {
|
|
1374
|
+
const reply = pendingReplyMap.get(commandId);
|
|
1375
|
+
pendingReplyMap.delete(commandId);
|
|
1376
|
+
return reply;
|
|
1162
1377
|
};
|
|
1163
1378
|
//#endregion
|
|
1164
1379
|
//#region src/services/server/checkIsTokenValid.ts
|
|
@@ -1169,38 +1384,312 @@ const checkIsTokenValid = (candidate, token) => {
|
|
|
1169
1384
|
return candidateBuffer.length === tokenBuffer.length && timingSafeEqual(candidateBuffer, tokenBuffer);
|
|
1170
1385
|
};
|
|
1171
1386
|
//#endregion
|
|
1172
|
-
//#region src/services/
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1387
|
+
//#region src/services/drivers/window/createWindowDriver.ts
|
|
1388
|
+
const createWindowDriver = ({ onEvents, onSessionOpen, onSessionsChange }, { launchSessionWindow, writeLine }) => {
|
|
1389
|
+
const windowSessionMap = /* @__PURE__ */ new Map();
|
|
1390
|
+
const childSet = /* @__PURE__ */ new Set();
|
|
1391
|
+
const pendingLaunchMap = /* @__PURE__ */ new Map();
|
|
1392
|
+
const taskRegistry = createTaskRegistry();
|
|
1393
|
+
const webSocketServer = new WebSocketServer({ noServer: true });
|
|
1394
|
+
const httpServer = createServer((_request, response) => {
|
|
1395
|
+
response.writeHead(405).end();
|
|
1396
|
+
});
|
|
1397
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
1398
|
+
const secret = request.headers.authorization?.replace(/^Bearer /u, "") ?? "";
|
|
1399
|
+
const pendingLaunchEntry = [...pendingLaunchMap].find(([launchSecret]) => checkIsTokenValid(secret, launchSecret));
|
|
1400
|
+
if (!pendingLaunchEntry) {
|
|
1401
|
+
socket.end("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const [launchSecret, { connection, timeout }] = pendingLaunchEntry;
|
|
1405
|
+
pendingLaunchMap.delete(launchSecret);
|
|
1406
|
+
clearTimeout(timeout);
|
|
1407
|
+
webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
|
|
1408
|
+
connection.resolve(webSocket);
|
|
1409
|
+
});
|
|
1410
|
+
});
|
|
1411
|
+
let listening;
|
|
1412
|
+
const listen = async () => {
|
|
1413
|
+
httpServer.listen(0, DEFAULT_HOSTNAME);
|
|
1414
|
+
await once(httpServer, "listening");
|
|
1415
|
+
const address = httpServer.address();
|
|
1416
|
+
return typeof address === "object" && address ? address.port : 0;
|
|
1417
|
+
};
|
|
1418
|
+
const getWindowSession = (sessionId) => {
|
|
1419
|
+
const windowSession = windowSessionMap.get(sessionId);
|
|
1420
|
+
if (!windowSession) throw new InvalidOperationError(Operation.Read, sessionId, "the session is not open on this host");
|
|
1421
|
+
return windowSession;
|
|
1422
|
+
};
|
|
1423
|
+
const closeWindowSession = (sessionId, windowSession) => {
|
|
1424
|
+
if (windowSessionMap.get(sessionId) !== windowSession) return;
|
|
1425
|
+
windowSessionMap.delete(sessionId);
|
|
1426
|
+
if (windowSession.state !== "Closed") onEvents(sessionId, toSessionClosedEvents(""));
|
|
1427
|
+
writeLine(`A session in ${windowSession.child.cwd} closed, and its window with it.`);
|
|
1428
|
+
onSessionsChange();
|
|
1429
|
+
windowSession.child.webSocket.close();
|
|
1430
|
+
};
|
|
1431
|
+
const handleChildMessage = (child, childMessage) => {
|
|
1432
|
+
switch (childMessage.type) {
|
|
1433
|
+
case "CommandError": {
|
|
1434
|
+
const reply = takeChildReply(child, childMessage.commandId);
|
|
1435
|
+
if (reply) reply.reject(new Error(childMessage.message));
|
|
1436
|
+
else console.error(childMessage.message);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
case "CommandResult":
|
|
1440
|
+
takeChildReply(child, childMessage.commandId)?.resolve(childMessage.sessionId);
|
|
1441
|
+
return;
|
|
1442
|
+
case "Events": {
|
|
1443
|
+
const { events, sessionId } = childMessage;
|
|
1444
|
+
const windowSession = windowSessionMap.get(sessionId);
|
|
1445
|
+
const isHeld = windowSession?.child === child;
|
|
1446
|
+
if (isHeld) {
|
|
1447
|
+
windowSession.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1448
|
+
for (const event of events) if (event.type === "SessionState") windowSession.state = event.state;
|
|
1186
1449
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
newEvents.push(event);
|
|
1450
|
+
onEvents(sessionId, events);
|
|
1451
|
+
if (isHeld && windowSession.state === "Closed") closeWindowSession(sessionId, windowSession);
|
|
1452
|
+
return;
|
|
1191
1453
|
}
|
|
1192
|
-
|
|
1454
|
+
case "SessionOpen":
|
|
1455
|
+
windowSessionMap.set(childMessage.sessionId, {
|
|
1456
|
+
child,
|
|
1457
|
+
cwd: child.cwd,
|
|
1458
|
+
lastActivityAt: /* @__PURE__ */ new Date(),
|
|
1459
|
+
state: "Idle",
|
|
1460
|
+
title: ""
|
|
1461
|
+
});
|
|
1462
|
+
writeLine(`A session in ${child.cwd} opened in a window of its own.`);
|
|
1463
|
+
onSessionOpen(childMessage.sessionId);
|
|
1464
|
+
return;
|
|
1465
|
+
case "SessionsChange":
|
|
1466
|
+
onSessionsChange();
|
|
1467
|
+
return;
|
|
1468
|
+
default: exhaustiveGuard(childMessage);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
const receive = (child, data) => {
|
|
1472
|
+
getResult(() => childMessageSchema.parse(JSON.parse(readMessageText(data)))).match((childMessage) => {
|
|
1473
|
+
handleChildMessage(child, childMessage);
|
|
1474
|
+
}, console.error);
|
|
1475
|
+
};
|
|
1476
|
+
const removeChild = (child) => {
|
|
1477
|
+
childSet.delete(child);
|
|
1478
|
+
for (const reply of child.pendingReplyMap.values()) reply.reject(new InvalidOperationError(Operation.Update, child.cwd, "the session's window closed"));
|
|
1479
|
+
child.pendingReplyMap.clear();
|
|
1480
|
+
for (const [sessionId, windowSession] of windowSessionMap) if (windowSession.child === child) closeWindowSession(sessionId, windowSession);
|
|
1481
|
+
};
|
|
1482
|
+
const openWindow = async (cwd) => {
|
|
1483
|
+
const port = await (listening ??= listen());
|
|
1484
|
+
const secret = randomBytes(32).toString("base64url");
|
|
1485
|
+
const connection = Promise.withResolvers();
|
|
1486
|
+
const timeout = setTimeout(() => {
|
|
1487
|
+
pendingLaunchMap.delete(secret);
|
|
1488
|
+
connection.reject(new InvalidOperationError(Operation.Create, cwd, "the session's window did not start"));
|
|
1489
|
+
}, SESSION_WINDOW_CONNECT_TIMEOUT);
|
|
1490
|
+
pendingLaunchMap.set(secret, {
|
|
1491
|
+
connection,
|
|
1492
|
+
timeout
|
|
1493
|
+
});
|
|
1494
|
+
launchSessionWindow({
|
|
1495
|
+
port,
|
|
1496
|
+
secret
|
|
1497
|
+
});
|
|
1498
|
+
const webSocket = await connection.promise;
|
|
1499
|
+
const child = {
|
|
1500
|
+
cwd,
|
|
1501
|
+
pendingReplyMap: /* @__PURE__ */ new Map(),
|
|
1502
|
+
webSocket
|
|
1503
|
+
};
|
|
1504
|
+
childSet.add(child);
|
|
1505
|
+
webSocket.on("message", (data) => {
|
|
1506
|
+
receive(child, data);
|
|
1507
|
+
});
|
|
1508
|
+
webSocket.on("close", () => {
|
|
1509
|
+
removeChild(child);
|
|
1510
|
+
});
|
|
1511
|
+
return child;
|
|
1512
|
+
};
|
|
1513
|
+
const openInWindow = async (cwd, command) => {
|
|
1514
|
+
const child = await openWindow(cwd);
|
|
1515
|
+
return getResultAsync(() => sendChildCommand(child, command)).match((sessionId) => sessionId, (error) => {
|
|
1516
|
+
child.webSocket.close();
|
|
1517
|
+
throw error;
|
|
1518
|
+
});
|
|
1519
|
+
};
|
|
1520
|
+
const forward = (sessionId, command) => sendChildCommand(getWindowSession(sessionId).child, command);
|
|
1521
|
+
const forwardInBackground = (sessionId, command) => {
|
|
1522
|
+
const reply = forward(sessionId, command);
|
|
1523
|
+
taskRegistry.run(() => getResultAsync(() => reply).match(noop, console.error));
|
|
1524
|
+
};
|
|
1525
|
+
const resumeSession = async (sessionId) => {
|
|
1526
|
+
if (windowSessionMap.has(sessionId)) return sessionId;
|
|
1527
|
+
return openInWindow(await readSessionCwd(sessionId), {
|
|
1528
|
+
id: crypto.randomUUID(),
|
|
1529
|
+
sessionId,
|
|
1530
|
+
type: "Resume"
|
|
1531
|
+
});
|
|
1532
|
+
};
|
|
1533
|
+
return {
|
|
1534
|
+
backgroundTasks: async (sessionId) => {
|
|
1535
|
+
await forward(sessionId, {
|
|
1536
|
+
id: crypto.randomUUID(),
|
|
1537
|
+
sessionId,
|
|
1538
|
+
type: "BackgroundTasks"
|
|
1539
|
+
});
|
|
1193
1540
|
},
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1541
|
+
close: async () => {
|
|
1542
|
+
for (const { connection, timeout } of pendingLaunchMap.values()) {
|
|
1543
|
+
clearTimeout(timeout);
|
|
1544
|
+
connection.reject(new InvalidOperationError(Operation.Create, createWindowDriver.name, "the host is stopping"));
|
|
1545
|
+
}
|
|
1546
|
+
pendingLaunchMap.clear();
|
|
1547
|
+
const children = [...childSet];
|
|
1548
|
+
for (const { webSocket } of children) webSocket.terminate();
|
|
1549
|
+
await Promise.all(children.map(({ webSocket }) => once(webSocket, "close")));
|
|
1550
|
+
await taskRegistry.drain();
|
|
1551
|
+
if (!listening) return;
|
|
1552
|
+
await listening;
|
|
1553
|
+
httpServer.close();
|
|
1554
|
+
await once(httpServer, "close");
|
|
1555
|
+
},
|
|
1556
|
+
closeSession: (sessionId) => {
|
|
1557
|
+
closeWindowSession(sessionId, getWindowSession(sessionId));
|
|
1558
|
+
},
|
|
1559
|
+
createSession: (cwd) => openInWindow(cwd, {
|
|
1560
|
+
cwd,
|
|
1561
|
+
id: crypto.randomUUID(),
|
|
1562
|
+
type: "CreateSession"
|
|
1563
|
+
}),
|
|
1564
|
+
forkSession: async (sessionId, messageUuid) => openInWindow(await readSessionCwd(sessionId), {
|
|
1565
|
+
id: crypto.randomUUID(),
|
|
1566
|
+
messageUuid,
|
|
1567
|
+
sessionId,
|
|
1568
|
+
type: "Fork"
|
|
1569
|
+
}),
|
|
1570
|
+
interrupt: async (sessionId) => {
|
|
1571
|
+
await forward(sessionId, {
|
|
1572
|
+
id: crypto.randomUUID(),
|
|
1573
|
+
sessionId,
|
|
1574
|
+
type: "Interrupt"
|
|
1575
|
+
});
|
|
1576
|
+
},
|
|
1577
|
+
listSessions: () => listSessionSummaries(windowSessionMap),
|
|
1578
|
+
prompt: (sessionId, text, attachments) => {
|
|
1579
|
+
forwardInBackground(sessionId, {
|
|
1580
|
+
attachments,
|
|
1581
|
+
id: crypto.randomUUID(),
|
|
1582
|
+
sessionId,
|
|
1583
|
+
text,
|
|
1584
|
+
type: "Prompt"
|
|
1585
|
+
});
|
|
1586
|
+
},
|
|
1587
|
+
resolvePermission: (sessionId, requestId, behavior, message) => {
|
|
1588
|
+
forwardInBackground(sessionId, {
|
|
1589
|
+
behavior,
|
|
1590
|
+
id: crypto.randomUUID(),
|
|
1591
|
+
message,
|
|
1592
|
+
requestId,
|
|
1593
|
+
sessionId,
|
|
1594
|
+
type: "PermissionVerdict"
|
|
1595
|
+
});
|
|
1596
|
+
},
|
|
1597
|
+
resumeAt: async (sessionId, messageUuid) => {
|
|
1598
|
+
const cwd = await readSessionCwd(sessionId);
|
|
1599
|
+
const windowSession = windowSessionMap.get(sessionId);
|
|
1600
|
+
if (windowSession) closeWindowSession(sessionId, windowSession);
|
|
1601
|
+
return openInWindow(cwd, {
|
|
1602
|
+
id: crypto.randomUUID(),
|
|
1603
|
+
messageUuid,
|
|
1604
|
+
sessionId,
|
|
1605
|
+
type: "ResumeAt"
|
|
1606
|
+
});
|
|
1607
|
+
},
|
|
1608
|
+
resumeSession,
|
|
1609
|
+
rewindFiles: async (sessionId, messageUuid) => {
|
|
1610
|
+
await resumeSession(sessionId);
|
|
1611
|
+
await forward(sessionId, {
|
|
1612
|
+
id: crypto.randomUUID(),
|
|
1613
|
+
messageUuid,
|
|
1614
|
+
sessionId,
|
|
1615
|
+
type: "RewindFiles"
|
|
1616
|
+
});
|
|
1617
|
+
},
|
|
1618
|
+
runSlashCommand: (sessionId, name, commandArguments) => {
|
|
1619
|
+
forwardInBackground(sessionId, {
|
|
1620
|
+
arguments: commandArguments,
|
|
1621
|
+
id: crypto.randomUUID(),
|
|
1622
|
+
name,
|
|
1623
|
+
sessionId,
|
|
1624
|
+
type: "SlashCommand"
|
|
1625
|
+
});
|
|
1626
|
+
},
|
|
1627
|
+
setModel: async (sessionId, model) => {
|
|
1628
|
+
await forward(sessionId, {
|
|
1629
|
+
id: crypto.randomUUID(),
|
|
1630
|
+
model,
|
|
1631
|
+
sessionId,
|
|
1632
|
+
type: "SetModel"
|
|
1633
|
+
});
|
|
1634
|
+
},
|
|
1635
|
+
setPermissionMode: async (sessionId, permissionMode) => {
|
|
1636
|
+
await forward(sessionId, {
|
|
1637
|
+
id: crypto.randomUUID(),
|
|
1638
|
+
permissionMode,
|
|
1639
|
+
sessionId,
|
|
1640
|
+
type: "SetPermissionMode"
|
|
1641
|
+
});
|
|
1642
|
+
},
|
|
1643
|
+
stopTask: async (sessionId, taskId) => {
|
|
1644
|
+
await forward(sessionId, {
|
|
1645
|
+
id: crypto.randomUUID(),
|
|
1646
|
+
sessionId,
|
|
1647
|
+
taskId,
|
|
1648
|
+
type: "StopTask"
|
|
1649
|
+
});
|
|
1197
1650
|
}
|
|
1198
1651
|
};
|
|
1199
1652
|
};
|
|
1200
1653
|
//#endregion
|
|
1654
|
+
//#region src/services/drivers/window/launchSessionWindow.ts
|
|
1655
|
+
const SESSION_COMMAND_ENVIRONMENT_VARIABLE_PREFIX = `${SITE_NAME.toUpperCase()}_SESSION_COMMAND_`;
|
|
1656
|
+
const launchSessionWindow = ({ port, secret }) => {
|
|
1657
|
+
const hostCommand = isSea() ? [process.execPath] : [process.execPath, process.argv[1] ?? ""];
|
|
1658
|
+
const quotedHostCommand = hostCommand.map((_, index) => `"%${SESSION_COMMAND_ENVIRONMENT_VARIABLE_PREFIX}${index}%"`).join(" ");
|
|
1659
|
+
spawn("cmd.exe", [
|
|
1660
|
+
"/d",
|
|
1661
|
+
"/s",
|
|
1662
|
+
"/c",
|
|
1663
|
+
`"start "" ${quotedHostCommand} ${SESSION_SUBCOMMAND} --port ${port}"`
|
|
1664
|
+
], {
|
|
1665
|
+
env: {
|
|
1666
|
+
...process.env,
|
|
1667
|
+
...Object.fromEntries(hostCommand.map((part, index) => [`${SESSION_COMMAND_ENVIRONMENT_VARIABLE_PREFIX}${index}`, part])),
|
|
1668
|
+
[SESSION_SECRET_ENVIRONMENT_VARIABLE]: secret
|
|
1669
|
+
},
|
|
1670
|
+
stdio: "ignore",
|
|
1671
|
+
windowsVerbatimArguments: true
|
|
1672
|
+
}).on("error", console.error);
|
|
1673
|
+
};
|
|
1674
|
+
//#endregion
|
|
1675
|
+
//#region src/services/drivers/window/formatSessionLogLine.ts
|
|
1676
|
+
const formatSessionLogLine = (event) => {
|
|
1677
|
+
switch (event.type) {
|
|
1678
|
+
case "AssistantMessage": return event.parentToolUseId ? "" : `Claude: ${event.text}`;
|
|
1679
|
+
case "HostError": return `Error: ${event.message}`;
|
|
1680
|
+
case "PermissionRequest": return `Claude asks to use ${event.toolName}. Answer in the page.`;
|
|
1681
|
+
case "ToolUse": return event.parentToolUseId ? "" : `Claude uses ${event.name}`;
|
|
1682
|
+
case "UserMessage": return event.parentToolUseId ? "" : `You: ${event.text}`;
|
|
1683
|
+
default: return "";
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
//#endregion
|
|
1201
1687
|
//#region src/services/server/handleCommand.ts
|
|
1202
1688
|
const handleCommand = async (driver, command) => {
|
|
1203
1689
|
switch (command.type) {
|
|
1690
|
+
case "BackgroundTasks":
|
|
1691
|
+
await driver.backgroundTasks(command.sessionId);
|
|
1692
|
+
return "";
|
|
1204
1693
|
case "CloseSession":
|
|
1205
1694
|
driver.closeSession(command.sessionId);
|
|
1206
1695
|
return "";
|
|
@@ -1230,6 +1719,281 @@ const handleCommand = async (driver, command) => {
|
|
|
1230
1719
|
case "SlashCommand":
|
|
1231
1720
|
driver.runSlashCommand(command.sessionId, command.name, command.arguments);
|
|
1232
1721
|
return "";
|
|
1722
|
+
case "StopTask":
|
|
1723
|
+
await driver.stopTask(command.sessionId, command.taskId);
|
|
1724
|
+
return "";
|
|
1725
|
+
default: return exhaustiveGuard(command);
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
//#endregion
|
|
1729
|
+
//#region src/services/drivers/window/serveSessionChild.ts
|
|
1730
|
+
const serveSessionChild = async (webSocket, createDriver, writeLine) => {
|
|
1731
|
+
const taskRegistry = createTaskRegistry();
|
|
1732
|
+
const send = (childMessage) => {
|
|
1733
|
+
if (webSocket.readyState === WebSocket.OPEN) webSocket.send(JSON.stringify(childMessage));
|
|
1734
|
+
};
|
|
1735
|
+
const driver = createDriver({
|
|
1736
|
+
onEvents: (sessionId, events) => {
|
|
1737
|
+
for (const event of events) {
|
|
1738
|
+
const line = formatSessionLogLine(event);
|
|
1739
|
+
if (line) writeLine(line);
|
|
1740
|
+
}
|
|
1741
|
+
send({
|
|
1742
|
+
events,
|
|
1743
|
+
sessionId,
|
|
1744
|
+
type: "Events"
|
|
1745
|
+
});
|
|
1746
|
+
},
|
|
1747
|
+
onSessionOpen: (sessionId) => {
|
|
1748
|
+
send({
|
|
1749
|
+
sessionId,
|
|
1750
|
+
type: "SessionOpen"
|
|
1751
|
+
});
|
|
1752
|
+
},
|
|
1753
|
+
onSessionsChange: () => {
|
|
1754
|
+
send({ type: "SessionsChange" });
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
const receive = async (text) => {
|
|
1758
|
+
const command = getResult(() => driverCommandSchema.parse(JSON.parse(text))).orTee((error) => {
|
|
1759
|
+
send({
|
|
1760
|
+
commandId: "",
|
|
1761
|
+
message: error.message,
|
|
1762
|
+
type: "CommandError"
|
|
1763
|
+
});
|
|
1764
|
+
}).unwrapOr(void 0);
|
|
1765
|
+
if (!command) return;
|
|
1766
|
+
await getResultAsync(() => handleCommand(driver, command)).match((sessionId) => {
|
|
1767
|
+
send({
|
|
1768
|
+
commandId: command.id,
|
|
1769
|
+
sessionId,
|
|
1770
|
+
type: "CommandResult"
|
|
1771
|
+
});
|
|
1772
|
+
}, (error) => {
|
|
1773
|
+
send({
|
|
1774
|
+
commandId: command.id,
|
|
1775
|
+
message: error.message,
|
|
1776
|
+
type: "CommandError"
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
};
|
|
1780
|
+
webSocket.on("message", (data) => {
|
|
1781
|
+
taskRegistry.run(() => receive(readMessageText(data)));
|
|
1782
|
+
});
|
|
1783
|
+
await once(webSocket, "close");
|
|
1784
|
+
await driver.close();
|
|
1785
|
+
await taskRegistry.drain();
|
|
1786
|
+
};
|
|
1787
|
+
//#endregion
|
|
1788
|
+
//#region src/services/drivers/window/runSessionChild.ts
|
|
1789
|
+
const runSessionChild = async (port, writeLine) => {
|
|
1790
|
+
const secret = process.env[SESSION_SECRET_ENVIRONMENT_VARIABLE] ?? "";
|
|
1791
|
+
Reflect.deleteProperty(process.env, SESSION_SECRET_ENVIRONMENT_VARIABLE);
|
|
1792
|
+
process.title = `${SITE_NAME} session`;
|
|
1793
|
+
const webSocket = new WebSocket(`ws://${DEFAULT_HOSTNAME}:${port}`, { headers: { authorization: `Bearer ${secret}` } });
|
|
1794
|
+
const serving = serveSessionChild(webSocket, createClaudeAgentSdkDriver, writeLine);
|
|
1795
|
+
await once(webSocket, "open");
|
|
1796
|
+
writeLine("This window shows one session as it runs. Type to it in the page.\nTo stop it, close this window.\n");
|
|
1797
|
+
const stop = () => {
|
|
1798
|
+
webSocket.close();
|
|
1799
|
+
};
|
|
1800
|
+
process.once("SIGINT", stop);
|
|
1801
|
+
process.once("SIGHUP", stop);
|
|
1802
|
+
await serving;
|
|
1803
|
+
};
|
|
1804
|
+
//#endregion
|
|
1805
|
+
//#region src/services/installer/checkIsHostInstalled.ts
|
|
1806
|
+
const checkIsHostInstalled = () => relative(getHostInstallDirectory(), dirname(process.execPath)) === "";
|
|
1807
|
+
//#endregion
|
|
1808
|
+
//#region src/services/installer/checkIsSchemeLaunchTampered.ts
|
|
1809
|
+
const checkIsSchemeLaunchTampered = (commandLineArguments) => commandLineArguments.length > 1 && commandLineArguments.some((commandLineArgument) => commandLineArgument.startsWith(`${HOST_SCHEME}:`));
|
|
1810
|
+
//#endregion
|
|
1811
|
+
//#region src/services/installer/getSchemeLaunch.ts
|
|
1812
|
+
const getSchemeLaunch = (argument) => {
|
|
1813
|
+
if (!URL.canParse(argument)) return void 0;
|
|
1814
|
+
const url = new URL(argument);
|
|
1815
|
+
if (url.protocol !== `${HOST_SCHEME}:`) return void 0;
|
|
1816
|
+
return { code: url.searchParams.get("code") ?? "" };
|
|
1817
|
+
};
|
|
1818
|
+
//#endregion
|
|
1819
|
+
//#region src/services/installer/installHost.ts
|
|
1820
|
+
const installHost = () => {
|
|
1821
|
+
const installDirectory = getHostInstallDirectory();
|
|
1822
|
+
const executablePath = join(installDirectory, HOST_EXECUTABLE_FILENAME);
|
|
1823
|
+
mkdirSync(installDirectory, { recursive: true });
|
|
1824
|
+
copyFileSync(process.execPath, executablePath);
|
|
1825
|
+
writeFileSync(join(installDirectory, CLAUDE_CODE_EXECUTABLE_FILENAME), new Uint8Array(getRawAsset(CLAUDE_CODE_EXECUTABLE_FILENAME)));
|
|
1826
|
+
for (const assetKey of getAssetKeys().filter((key) => key.startsWith(`${NODE_PTY_DIRECTORY_NAME}/`))) {
|
|
1827
|
+
const assetPath = join(installDirectory, ...assetKey.split("/"));
|
|
1828
|
+
mkdirSync(dirname(assetPath), { recursive: true });
|
|
1829
|
+
writeFileSync(assetPath, new Uint8Array(getRawAsset(assetKey)));
|
|
1830
|
+
}
|
|
1831
|
+
for (const registryArguments of [
|
|
1832
|
+
[
|
|
1833
|
+
HOST_SCHEME_REGISTRY_KEY,
|
|
1834
|
+
"/ve",
|
|
1835
|
+
"/d",
|
|
1836
|
+
`URL:${SITE_NAME} Host`
|
|
1837
|
+
],
|
|
1838
|
+
[
|
|
1839
|
+
HOST_SCHEME_REGISTRY_KEY,
|
|
1840
|
+
"/v",
|
|
1841
|
+
"URL Protocol",
|
|
1842
|
+
"/d",
|
|
1843
|
+
""
|
|
1844
|
+
],
|
|
1845
|
+
[
|
|
1846
|
+
win32.join(HOST_SCHEME_REGISTRY_KEY, "shell", "open", "command"),
|
|
1847
|
+
"/ve",
|
|
1848
|
+
"/d",
|
|
1849
|
+
`"${executablePath}" "%1"`
|
|
1850
|
+
]
|
|
1851
|
+
]) execFileSync("reg", [
|
|
1852
|
+
"add",
|
|
1853
|
+
...registryArguments,
|
|
1854
|
+
"/f"
|
|
1855
|
+
], { stdio: "ignore" });
|
|
1856
|
+
return installDirectory;
|
|
1857
|
+
};
|
|
1858
|
+
//#endregion
|
|
1859
|
+
//#region src/services/installer/uninstallHost.ts
|
|
1860
|
+
const uninstallHost = () => {
|
|
1861
|
+
execFileSync("reg", [
|
|
1862
|
+
"delete",
|
|
1863
|
+
HOST_SCHEME_REGISTRY_KEY,
|
|
1864
|
+
"/f"
|
|
1865
|
+
], { stdio: "ignore" });
|
|
1866
|
+
spawn("powershell.exe", [
|
|
1867
|
+
"-NoProfile",
|
|
1868
|
+
"-Command",
|
|
1869
|
+
`Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:${HOST_DIRECTORY_ENVIRONMENT_VARIABLE} -Recurse -Force`
|
|
1870
|
+
], {
|
|
1871
|
+
detached: true,
|
|
1872
|
+
env: {
|
|
1873
|
+
...process.env,
|
|
1874
|
+
[HOST_DIRECTORY_ENVIRONMENT_VARIABLE]: getHostInstallDirectory()
|
|
1875
|
+
},
|
|
1876
|
+
stdio: "ignore",
|
|
1877
|
+
windowsHide: true
|
|
1878
|
+
}).unref();
|
|
1879
|
+
};
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/services/device/hashCredential.ts
|
|
1882
|
+
const hashCredential = (credential) => createHash("sha256").update(credential).digest("base64url");
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/services/device/findDevice.ts
|
|
1885
|
+
const findDevice = (devices, credential) => {
|
|
1886
|
+
const credentialHash = hashCredential(credential);
|
|
1887
|
+
return devices.find((device) => checkIsTokenValid(credentialHash, device.credentialHash));
|
|
1888
|
+
};
|
|
1889
|
+
//#endregion
|
|
1890
|
+
//#region src/services/device/getDeviceName.ts
|
|
1891
|
+
const BrowserNames = [
|
|
1892
|
+
[/Edg\//u, "Edge"],
|
|
1893
|
+
[/Firefox\//u, "Firefox"],
|
|
1894
|
+
[/Chrome\//u, "Chrome"],
|
|
1895
|
+
[/Safari\//u, "Safari"]
|
|
1896
|
+
];
|
|
1897
|
+
const SystemNames = [
|
|
1898
|
+
[/Windows/u, "Windows"],
|
|
1899
|
+
[/Android/u, "Android"],
|
|
1900
|
+
[/iPhone|iPad/u, "iOS"],
|
|
1901
|
+
[/Mac OS X/u, "macOS"],
|
|
1902
|
+
[/Linux/u, "Linux"]
|
|
1903
|
+
];
|
|
1904
|
+
const getDeviceName = (userAgent) => {
|
|
1905
|
+
const browserName = BrowserNames.find(([pattern]) => pattern.test(userAgent))?.[1] ?? "A browser";
|
|
1906
|
+
const systemName = SystemNames.find(([pattern]) => pattern.test(userAgent))?.[1];
|
|
1907
|
+
return systemName ? `${browserName} on ${systemName}` : browserName;
|
|
1908
|
+
};
|
|
1909
|
+
//#endregion
|
|
1910
|
+
//#region src/services/handshake/getPublicKeyText.ts
|
|
1911
|
+
const getPublicKeyText = (privateKey) => createPublicKey(privateKey).export({ format: "jwk" }).x ?? "";
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region src/services/server/answerHttpRequest.ts
|
|
1914
|
+
const answerHttpRequest = ({ headers, method }, response) => {
|
|
1915
|
+
if (method !== "OPTIONS") {
|
|
1916
|
+
response.writeHead(405).end();
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
response.setHeader("Access-Control-Allow-Origin", headers.origin ?? "*");
|
|
1920
|
+
response.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
1921
|
+
response.setHeader("Access-Control-Allow-Methods", "GET");
|
|
1922
|
+
response.setHeader("Access-Control-Allow-Headers", headers["access-control-request-headers"] ?? "*");
|
|
1923
|
+
response.setHeader("Vary", "Origin");
|
|
1924
|
+
response.writeHead(204).end();
|
|
1925
|
+
};
|
|
1926
|
+
//#endregion
|
|
1927
|
+
//#region src/services/server/createEventLog.ts
|
|
1928
|
+
const createEventLog = () => {
|
|
1929
|
+
const sessionEventMap = /* @__PURE__ */ new Map();
|
|
1930
|
+
return {
|
|
1931
|
+
append: (sessionId, events) => {
|
|
1932
|
+
const sessionEvents = getOrCreate(sessionEventMap, sessionId, () => ({
|
|
1933
|
+
eventIds: /* @__PURE__ */ new Set(),
|
|
1934
|
+
events: []
|
|
1935
|
+
}));
|
|
1936
|
+
const newEvents = [];
|
|
1937
|
+
for (const event of events) {
|
|
1938
|
+
if (EphemeralAgentEventTypes.includes(event.type)) {
|
|
1939
|
+
newEvents.push(event);
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
if (sessionEvents.eventIds.has(event.id)) continue;
|
|
1943
|
+
sessionEvents.eventIds.add(event.id);
|
|
1944
|
+
sessionEvents.events.push(event);
|
|
1945
|
+
newEvents.push(event);
|
|
1946
|
+
}
|
|
1947
|
+
return newEvents;
|
|
1948
|
+
},
|
|
1949
|
+
entries: () => Array.from(sessionEventMap, ([sessionId, { events }]) => [sessionId, events]),
|
|
1950
|
+
reset: (sessionId) => {
|
|
1951
|
+
sessionEventMap.delete(sessionId);
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
};
|
|
1955
|
+
//#endregion
|
|
1956
|
+
//#region src/services/server/createPairingCodes.ts
|
|
1957
|
+
const createPairingCodes = () => {
|
|
1958
|
+
const codeTimeoutMap = /* @__PURE__ */ new Map();
|
|
1959
|
+
return {
|
|
1960
|
+
add: (code, duration) => {
|
|
1961
|
+
clearTimeout(codeTimeoutMap.get(code));
|
|
1962
|
+
codeTimeoutMap.set(code, setTimeout(() => {
|
|
1963
|
+
codeTimeoutMap.delete(code);
|
|
1964
|
+
}, duration));
|
|
1965
|
+
},
|
|
1966
|
+
clear: () => {
|
|
1967
|
+
for (const timeout of codeTimeoutMap.values()) clearTimeout(timeout);
|
|
1968
|
+
codeTimeoutMap.clear();
|
|
1969
|
+
},
|
|
1970
|
+
take: (code) => {
|
|
1971
|
+
const heldCode = [...codeTimeoutMap.keys()].find((candidate) => checkIsTokenValid(code, candidate));
|
|
1972
|
+
if (heldCode === void 0) return false;
|
|
1973
|
+
clearTimeout(codeTimeoutMap.get(heldCode));
|
|
1974
|
+
codeTimeoutMap.delete(heldCode);
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
};
|
|
1979
|
+
//#endregion
|
|
1980
|
+
//#region src/services/server/handleShellCommand.ts
|
|
1981
|
+
const handleShellCommand = async (shellRegistry, command) => {
|
|
1982
|
+
switch (command.type) {
|
|
1983
|
+
case "CloseShell":
|
|
1984
|
+
shellRegistry.close(command.shellId);
|
|
1985
|
+
return "";
|
|
1986
|
+
case "OpenShell": return await shellRegistry.open(command.sessionId, {
|
|
1987
|
+
cols: command.cols,
|
|
1988
|
+
cwd: command.cwd,
|
|
1989
|
+
rows: command.rows
|
|
1990
|
+
});
|
|
1991
|
+
case "ShellInput":
|
|
1992
|
+
shellRegistry.write(command.shellId, command.data);
|
|
1993
|
+
return "";
|
|
1994
|
+
case "ShellResize":
|
|
1995
|
+
shellRegistry.resize(command.shellId, command.cols, command.rows);
|
|
1996
|
+
return "";
|
|
1233
1997
|
default: return exhaustiveGuard(command);
|
|
1234
1998
|
}
|
|
1235
1999
|
};
|
|
@@ -1239,13 +2003,91 @@ const sendServerMessage = (webSocket, message) => {
|
|
|
1239
2003
|
if (webSocket.readyState === WebSocket.OPEN) webSocket.send(JSON.stringify(message));
|
|
1240
2004
|
};
|
|
1241
2005
|
//#endregion
|
|
2006
|
+
//#region src/services/shell/createShellRegistry.ts
|
|
2007
|
+
const createShellRegistry = ({ onClose, onOutput, spawnShell }) => {
|
|
2008
|
+
const shellMap = /* @__PURE__ */ new Map();
|
|
2009
|
+
const pendingShells = /* @__PURE__ */ new Set();
|
|
2010
|
+
const closedSessionIds = /* @__PURE__ */ new Set();
|
|
2011
|
+
let isStopped = false;
|
|
2012
|
+
const getTerminal = (shellId) => {
|
|
2013
|
+
const shell = shellMap.get(shellId);
|
|
2014
|
+
if (!shell) throw new InvalidOperationError(Operation.Read, shellId, "the shell is not running on this host");
|
|
2015
|
+
return shell.terminal;
|
|
2016
|
+
};
|
|
2017
|
+
return {
|
|
2018
|
+
close: (shellId) => {
|
|
2019
|
+
getTerminal(shellId).kill();
|
|
2020
|
+
},
|
|
2021
|
+
closeAll: () => {
|
|
2022
|
+
isStopped = true;
|
|
2023
|
+
for (const pendingShell of pendingShells) pendingShell.isClosed = true;
|
|
2024
|
+
for (const { terminal } of shellMap.values()) terminal.kill();
|
|
2025
|
+
},
|
|
2026
|
+
closeSession: (sessionId) => {
|
|
2027
|
+
closedSessionIds.add(sessionId);
|
|
2028
|
+
for (const pendingShell of pendingShells) if (pendingShell.sessionId === sessionId) pendingShell.isClosed = true;
|
|
2029
|
+
for (const shell of shellMap.values()) if (shell.sessionId === sessionId) shell.terminal.kill();
|
|
2030
|
+
},
|
|
2031
|
+
entries: () => Array.from(shellMap, ([shellId, { output, sessionId }]) => ({
|
|
2032
|
+
output,
|
|
2033
|
+
sessionId,
|
|
2034
|
+
shellId
|
|
2035
|
+
})),
|
|
2036
|
+
open: async (sessionId, options) => {
|
|
2037
|
+
if (isStopped) throw new InvalidOperationError(Operation.Create, sessionId, "the host is stopping");
|
|
2038
|
+
if (closedSessionIds.has(sessionId)) throw new InvalidOperationError(Operation.Create, sessionId, "the session is closed");
|
|
2039
|
+
const pendingShell = {
|
|
2040
|
+
isClosed: false,
|
|
2041
|
+
sessionId
|
|
2042
|
+
};
|
|
2043
|
+
pendingShells.add(pendingShell);
|
|
2044
|
+
const terminal = await withFinalizerAsync(() => spawnShell(options), () => {
|
|
2045
|
+
pendingShells.delete(pendingShell);
|
|
2046
|
+
});
|
|
2047
|
+
if (pendingShell.isClosed) {
|
|
2048
|
+
terminal.kill();
|
|
2049
|
+
throw new InvalidOperationError(Operation.Create, sessionId, "the shell's session or host closed as it started");
|
|
2050
|
+
}
|
|
2051
|
+
const shellId = crypto.randomUUID();
|
|
2052
|
+
const shell = {
|
|
2053
|
+
output: "",
|
|
2054
|
+
sessionId,
|
|
2055
|
+
terminal
|
|
2056
|
+
};
|
|
2057
|
+
shellMap.set(shellId, shell);
|
|
2058
|
+
terminal.onData((data) => {
|
|
2059
|
+
shell.output = `${shell.output}${data}`.slice(-SHELL_OUTPUT_LENGTH);
|
|
2060
|
+
onOutput(shellId, data);
|
|
2061
|
+
});
|
|
2062
|
+
terminal.onExit(() => {
|
|
2063
|
+
shellMap.delete(shellId);
|
|
2064
|
+
onClose(shellId);
|
|
2065
|
+
});
|
|
2066
|
+
return shellId;
|
|
2067
|
+
},
|
|
2068
|
+
openSession: (sessionId) => {
|
|
2069
|
+
closedSessionIds.delete(sessionId);
|
|
2070
|
+
},
|
|
2071
|
+
resize: (shellId, cols, rows) => {
|
|
2072
|
+
getTerminal(shellId).resize(cols, rows);
|
|
2073
|
+
},
|
|
2074
|
+
write: (shellId, data) => {
|
|
2075
|
+
getTerminal(shellId).write(data);
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
};
|
|
2079
|
+
//#endregion
|
|
1242
2080
|
//#region src/services/server/createAgentConsoleServer.ts
|
|
1243
|
-
const createAgentConsoleServer = async ({ createDriver, hostname, port,
|
|
2081
|
+
const createAgentConsoleServer = async ({ createDriver, hostKey, hostname, origin, port, spawnShell, stateDirectory, writeLine }) => {
|
|
1244
2082
|
const eventLog = createEventLog();
|
|
1245
2083
|
const taskRegistry = createTaskRegistry();
|
|
2084
|
+
const pairingCodes = createPairingCodes();
|
|
2085
|
+
const publicKey = createPublicKey(hostKey);
|
|
2086
|
+
const publicKeyText = getPublicKeyText(hostKey);
|
|
1246
2087
|
const webSocketServer = new WebSocketServer({ noServer: true });
|
|
2088
|
+
const socketDeviceIdMap = /* @__PURE__ */ new Map();
|
|
1247
2089
|
const broadcast = (message) => {
|
|
1248
|
-
for (const webSocket of
|
|
2090
|
+
for (const webSocket of socketDeviceIdMap.keys()) sendServerMessage(webSocket, message);
|
|
1249
2091
|
};
|
|
1250
2092
|
let isRefreshing = false;
|
|
1251
2093
|
let isRefreshStale = false;
|
|
@@ -1268,6 +2110,22 @@ const createAgentConsoleServer = async ({ createDriver, hostname, port, token })
|
|
|
1268
2110
|
isRefreshing = false;
|
|
1269
2111
|
});
|
|
1270
2112
|
};
|
|
2113
|
+
const shellRegistry = createShellRegistry({
|
|
2114
|
+
onClose: (shellId) => {
|
|
2115
|
+
broadcast({
|
|
2116
|
+
shellId,
|
|
2117
|
+
type: "ShellClosed"
|
|
2118
|
+
});
|
|
2119
|
+
},
|
|
2120
|
+
onOutput: (shellId, data) => {
|
|
2121
|
+
broadcast({
|
|
2122
|
+
data,
|
|
2123
|
+
shellId,
|
|
2124
|
+
type: "ShellOutput"
|
|
2125
|
+
});
|
|
2126
|
+
},
|
|
2127
|
+
spawnShell
|
|
2128
|
+
});
|
|
1271
2129
|
const driver = createDriver({
|
|
1272
2130
|
onEvents: (sessionId, events) => {
|
|
1273
2131
|
const newEvents = eventLog.append(sessionId, events);
|
|
@@ -1276,8 +2134,10 @@ const createAgentConsoleServer = async ({ createDriver, hostname, port, token })
|
|
|
1276
2134
|
sessionId,
|
|
1277
2135
|
type: "Events"
|
|
1278
2136
|
});
|
|
2137
|
+
if (events.some((event) => event.type === "SessionState" && event.state === "Closed")) shellRegistry.closeSession(sessionId);
|
|
1279
2138
|
},
|
|
1280
2139
|
onSessionOpen: (sessionId) => {
|
|
2140
|
+
shellRegistry.openSession(sessionId);
|
|
1281
2141
|
eventLog.reset(sessionId);
|
|
1282
2142
|
broadcast({
|
|
1283
2143
|
sessionId,
|
|
@@ -1289,8 +2149,7 @@ const createAgentConsoleServer = async ({ createDriver, hostname, port, token })
|
|
|
1289
2149
|
}
|
|
1290
2150
|
});
|
|
1291
2151
|
const receive = async (webSocket, data) => {
|
|
1292
|
-
const
|
|
1293
|
-
const command = getResult(() => commandSchema.parse(JSON.parse(text))).orTee((error) => {
|
|
2152
|
+
const command = getResult(() => commandSchema.parse(JSON.parse(readMessageText(data)))).orTee((error) => {
|
|
1294
2153
|
sendServerMessage(webSocket, {
|
|
1295
2154
|
commandId: "",
|
|
1296
2155
|
message: error.message,
|
|
@@ -1298,39 +2157,147 @@ const createAgentConsoleServer = async ({ createDriver, hostname, port, token })
|
|
|
1298
2157
|
});
|
|
1299
2158
|
}).unwrapOr(void 0);
|
|
1300
2159
|
if (!command) return;
|
|
1301
|
-
|
|
1302
|
-
if (sessionId) sendServerMessage(webSocket, {
|
|
1303
|
-
commandId: command.id,
|
|
1304
|
-
sessionId,
|
|
1305
|
-
type: "SessionOpened"
|
|
1306
|
-
});
|
|
1307
|
-
else if (command.type === "ListSessions") refreshSessions();
|
|
1308
|
-
}, (error) => {
|
|
2160
|
+
const sendCommandError = (error) => {
|
|
1309
2161
|
sendServerMessage(webSocket, {
|
|
1310
2162
|
commandId: command.id,
|
|
1311
2163
|
message: error.message,
|
|
1312
2164
|
type: "CommandError"
|
|
1313
2165
|
});
|
|
1314
|
-
}
|
|
2166
|
+
};
|
|
2167
|
+
switch (command.type) {
|
|
2168
|
+
case "CloseShell":
|
|
2169
|
+
case "OpenShell":
|
|
2170
|
+
case "ShellInput":
|
|
2171
|
+
case "ShellResize":
|
|
2172
|
+
await getResultAsync(() => handleShellCommand(shellRegistry, command)).match((shellId) => {
|
|
2173
|
+
if (command.type === "OpenShell") sendServerMessage(webSocket, {
|
|
2174
|
+
commandId: command.id,
|
|
2175
|
+
sessionId: command.sessionId,
|
|
2176
|
+
shellId,
|
|
2177
|
+
type: "ShellOpened"
|
|
2178
|
+
});
|
|
2179
|
+
}, sendCommandError);
|
|
2180
|
+
return;
|
|
2181
|
+
default: await getResultAsync(() => handleCommand(driver, command)).match((sessionId) => {
|
|
2182
|
+
if (sessionId) sendServerMessage(webSocket, {
|
|
2183
|
+
commandId: command.id,
|
|
2184
|
+
sessionId,
|
|
2185
|
+
type: "SessionOpened"
|
|
2186
|
+
});
|
|
2187
|
+
else if (command.type === "ListSessions") refreshSessions();
|
|
2188
|
+
}, sendCommandError);
|
|
2189
|
+
}
|
|
1315
2190
|
};
|
|
1316
|
-
|
|
2191
|
+
const admit = (webSocket, deviceId) => {
|
|
2192
|
+
socketDeviceIdMap.set(webSocket, deviceId);
|
|
1317
2193
|
for (const [sessionId, events] of eventLog.entries()) sendServerMessage(webSocket, {
|
|
1318
2194
|
events,
|
|
1319
2195
|
sessionId,
|
|
1320
2196
|
type: "Events"
|
|
1321
2197
|
});
|
|
2198
|
+
for (const { output, sessionId, shellId } of shellRegistry.entries()) {
|
|
2199
|
+
sendServerMessage(webSocket, {
|
|
2200
|
+
commandId: "",
|
|
2201
|
+
sessionId,
|
|
2202
|
+
shellId,
|
|
2203
|
+
type: "ShellOpened"
|
|
2204
|
+
});
|
|
2205
|
+
if (output) sendServerMessage(webSocket, {
|
|
2206
|
+
data: output,
|
|
2207
|
+
shellId,
|
|
2208
|
+
type: "ShellOutput"
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
1322
2211
|
refreshSessions();
|
|
2212
|
+
};
|
|
2213
|
+
const pair = (webSocket, request, code) => {
|
|
2214
|
+
if (request.headers.origin !== origin || !pairingCodes.take(code)) {
|
|
2215
|
+
webSocket.close(4403);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
const credential = randomBytes(32).toString("base64url");
|
|
2219
|
+
const device = {
|
|
2220
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2221
|
+
credentialHash: hashCredential(credential),
|
|
2222
|
+
id: crypto.randomUUID(),
|
|
2223
|
+
name: getDeviceName(request.headers["user-agent"] ?? ""),
|
|
2224
|
+
origin
|
|
2225
|
+
};
|
|
2226
|
+
writeDevices(stateDirectory, [...readDevices(stateDirectory), device]);
|
|
2227
|
+
writeLine(`${device.name} connected. To remove it: devices --revoke ${device.id}`);
|
|
2228
|
+
sendServerMessage(webSocket, {
|
|
2229
|
+
credential,
|
|
2230
|
+
deviceId: device.id,
|
|
2231
|
+
publicKey: publicKeyText,
|
|
2232
|
+
type: "Paired"
|
|
2233
|
+
});
|
|
2234
|
+
admit(webSocket, device.id);
|
|
2235
|
+
};
|
|
2236
|
+
const receiveHandshake = (webSocket, request, hostNonce, handshakeMessage) => {
|
|
2237
|
+
const localPort = request.socket.localPort ?? 0;
|
|
2238
|
+
switch (handshakeMessage.type) {
|
|
2239
|
+
case "Authenticate": {
|
|
2240
|
+
const device = findDevice(readDevices(stateDirectory), handshakeMessage.credential);
|
|
2241
|
+
if (!device) {
|
|
2242
|
+
webSocket.close(4401);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
sendServerMessage(webSocket, { type: "Authenticated" });
|
|
2246
|
+
admit(webSocket, device.id);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
case "Challenge":
|
|
2250
|
+
sendServerMessage(webSocket, {
|
|
2251
|
+
nonce: hostNonce,
|
|
2252
|
+
port: localPort,
|
|
2253
|
+
signature: signNonce(hostKey, "HostProof", localPort, handshakeMessage.nonce),
|
|
2254
|
+
type: "Proof"
|
|
2255
|
+
});
|
|
2256
|
+
return;
|
|
2257
|
+
case "HandOff":
|
|
2258
|
+
case "Revoke":
|
|
2259
|
+
if (!checkIsSignatureValid(publicKey, "OwnerProof", localPort, hostNonce, handshakeMessage.signature)) {
|
|
2260
|
+
webSocket.close(4404);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
if (handshakeMessage.type === "HandOff") pairingCodes.add(handshakeMessage.code, SCHEME_PAIRING_CODE_DURATION);
|
|
2264
|
+
else {
|
|
2265
|
+
writeDevices(stateDirectory, readDevices(stateDirectory).filter(({ id }) => id !== handshakeMessage.deviceId));
|
|
2266
|
+
for (const [deviceWebSocket, deviceId] of socketDeviceIdMap) if (deviceId === handshakeMessage.deviceId) deviceWebSocket.close(4401);
|
|
2267
|
+
writeLine("A device was removed, and its connection closed.");
|
|
2268
|
+
}
|
|
2269
|
+
webSocket.close();
|
|
2270
|
+
return;
|
|
2271
|
+
case "Pair":
|
|
2272
|
+
pair(webSocket, request, handshakeMessage.code);
|
|
2273
|
+
return;
|
|
2274
|
+
default: exhaustiveGuard(handshakeMessage);
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
webSocketServer.on("connection", (webSocket, request) => {
|
|
2278
|
+
const hostNonce = randomBytes(32).toString("base64url");
|
|
1323
2279
|
webSocket.on("message", (data) => {
|
|
1324
|
-
|
|
2280
|
+
if (socketDeviceIdMap.has(webSocket)) {
|
|
2281
|
+
taskRegistry.run(() => receive(webSocket, data));
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
getResult(() => {
|
|
2285
|
+
receiveHandshake(webSocket, request, hostNonce, handshakeMessageSchema.parse(JSON.parse(readMessageText(data))));
|
|
2286
|
+
}).match(noop, (error) => {
|
|
2287
|
+
console.error(error);
|
|
2288
|
+
webSocket.close(4400);
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
webSocket.on("close", () => {
|
|
2292
|
+
socketDeviceIdMap.delete(webSocket);
|
|
1325
2293
|
});
|
|
1326
2294
|
});
|
|
1327
2295
|
const httpServer = createServer((request, response) => {
|
|
1328
2296
|
answerHttpRequest(request, response);
|
|
1329
2297
|
});
|
|
1330
2298
|
httpServer.on("upgrade", (request, socket, head) => {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
socket.end("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
2299
|
+
if (request.headers.origin !== void 0 && request.headers.origin !== origin) {
|
|
2300
|
+
socket.end("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1334
2301
|
return;
|
|
1335
2302
|
}
|
|
1336
2303
|
webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
|
|
@@ -1340,38 +2307,60 @@ const createAgentConsoleServer = async ({ createDriver, hostname, port, token })
|
|
|
1340
2307
|
httpServer.listen(port, hostname);
|
|
1341
2308
|
await once(httpServer, "listening");
|
|
1342
2309
|
const address = httpServer.address();
|
|
2310
|
+
let closing;
|
|
2311
|
+
const close = async () => {
|
|
2312
|
+
broadcast({ type: "HostStopping" });
|
|
2313
|
+
pairingCodes.clear();
|
|
2314
|
+
shellRegistry.closeAll();
|
|
2315
|
+
await driver.close();
|
|
2316
|
+
for (const webSocket of webSocketServer.clients) webSocket.terminate();
|
|
2317
|
+
webSocketServer.close();
|
|
2318
|
+
httpServer.close();
|
|
2319
|
+
await Promise.all([
|
|
2320
|
+
taskRegistry.drain(),
|
|
2321
|
+
once(webSocketServer, "close"),
|
|
2322
|
+
once(httpServer, "close")
|
|
2323
|
+
]);
|
|
2324
|
+
};
|
|
1343
2325
|
return {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
for (const webSocket of webSocketServer.clients) webSocket.terminate();
|
|
1347
|
-
webSocketServer.close();
|
|
1348
|
-
httpServer.close();
|
|
1349
|
-
await Promise.all([
|
|
1350
|
-
taskRegistry.drain(),
|
|
1351
|
-
once(webSocketServer, "close"),
|
|
1352
|
-
once(httpServer, "close")
|
|
1353
|
-
]);
|
|
1354
|
-
},
|
|
2326
|
+
addPairingCode: pairingCodes.add,
|
|
2327
|
+
close: () => closing ??= close(),
|
|
1355
2328
|
port: typeof address === "object" && address ? address.port : port
|
|
1356
2329
|
};
|
|
1357
2330
|
};
|
|
1358
2331
|
//#endregion
|
|
1359
|
-
//#region src/services/
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
return
|
|
2332
|
+
//#region src/services/shell/constants.ts
|
|
2333
|
+
const CONSOLE_LIST_AGENT_NAME = "conpty_console_list_agent";
|
|
2334
|
+
//#endregion
|
|
2335
|
+
//#region src/services/shell/runConsoleListAgent.ts
|
|
2336
|
+
const runConsoleListAgent = (shellPid) => {
|
|
2337
|
+
const installDirectory = getHostInstallDirectory();
|
|
2338
|
+
const { getConsoleProcessList } = createRequire(join(installDirectory, "package.json"))(join(installDirectory, NODE_PTY_DIRECTORY_NAME, "prebuilds", "win32-x64", "conpty_console_list.node"));
|
|
2339
|
+
process.send?.({ consoleProcessList: getConsoleProcessList(shellPid) });
|
|
2340
|
+
};
|
|
2341
|
+
//#endregion
|
|
2342
|
+
//#region src/services/shell/getShellFile.ts
|
|
2343
|
+
const getShellFile = () => process.platform === "win32" ? "powershell.exe" : process.env.SHELL ?? "/bin/sh";
|
|
2344
|
+
//#endregion
|
|
2345
|
+
//#region src/services/shell/loadNodePty.ts
|
|
2346
|
+
const loadNodePty = () => {
|
|
2347
|
+
if (!isSea()) return import("node-pty");
|
|
2348
|
+
const installDirectory = getHostInstallDirectory();
|
|
2349
|
+
return Promise.resolve(createRequire(join(installDirectory, "package.json"))(join(installDirectory, NODE_PTY_DIRECTORY_NAME)));
|
|
2350
|
+
};
|
|
2351
|
+
//#endregion
|
|
2352
|
+
//#region src/services/shell/spawnPtyShell.ts
|
|
2353
|
+
const spawnPtyShell = async ({ cols, cwd, rows }) => {
|
|
2354
|
+
const { spawn } = await getResultAsync(() => loadNodePty()).match((nodePty) => nodePty, () => {
|
|
2355
|
+
throw new InvalidOperationError(Operation.Create, cwd, "this host has no terminal support, so no shell can start");
|
|
2356
|
+
});
|
|
2357
|
+
return spawn(getShellFile(), [], {
|
|
2358
|
+
cols,
|
|
2359
|
+
cwd,
|
|
2360
|
+
env: process.env,
|
|
2361
|
+
name: "xterm-256color",
|
|
2362
|
+
rows
|
|
2363
|
+
});
|
|
1375
2364
|
};
|
|
1376
2365
|
//#endregion
|
|
1377
|
-
export {
|
|
2366
|
+
export { HOST_DIRECTORY_ENVIRONMENT_VARIABLE as $, sendChildCommand as A, IGNORED_SDK_MESSAGE_TYPES as At, SESSION_SECRET_BYTE_LENGTH as B, signNonce as Bt, serveSessionChild as C, createStreamTracker as Ct, createWindowDriver as D, toPermissionRequestEvent as Dt, launchSessionWindow as E, toPermissionResult as Et, childCommandResultMessageSchema as F, getEventId as Ft, createTaskRegistry as G, readHostKey as Gt, SESSION_SUBCOMMAND as H, writeDevices as Ht, childCommandErrorMessageSchema as I, runDevicesCommand as It, listSessionSummaries as J, DEVICES_FILENAME as Jt, toContentBlockParam as K, writeStateFile as Kt, ChildMessageType as L, sendToRunningHost as Lt, childSessionsChangeMessageSchema as M, SETTING_SOURCES as Mt, childSessionOpenMessageSchema as N, closeOpenSession as Nt, checkIsTokenValid as O, createInputQueue as Ot, childEventsMessageSchema as P, toSessionClosedEvents as Pt, CLAUDE_CODE_EXECUTABLE_FILENAME as Q, STATE_DIRECTORY_NAME as Qt, childMessageTypeSchema as R, readMessageText as Rt, runSessionChild as S, TodoToolName as St, formatSessionLogLine as T, createPermissionBridge as Tt, SESSION_WINDOW_CONNECT_TIMEOUT as U, readDevices as Ut, SESSION_SECRET_ENVIRONMENT_VARIABLE as V, checkIsSignatureValid as Vt, createClaudeAgentSdkDriver as W, deviceSchema as Wt, getClaudeCodeExecutablePath as X, PRINTED_PAIRING_CODE_DURATION as Xt, createSessionOpener as Y, HOST_KEY_FILENAME as Yt, getHostInstallDirectory as Z, SECRET_BYTE_LENGTH as Zt, uninstallHost as _, mapTaskMessage as _t, CONSOLE_LIST_AGENT_NAME as a, toContextUsageEvent as at, checkIsSchemeLaunchTampered as b, toUnknownEvent as bt, sendServerMessage as c, readToolUseResultMap as ct, createEventLog as d, toTurnResultEvents as dt, HOST_EXECUTABLE_FILENAME as et, answerHttpRequest as f, toSessionInitEvent as ft, hashCredential as g, mapSystemMessage as gt, findDevice as h, toToolResultText as ht, runConsoleListAgent as i, watchSession as it, childMessageSchema as j, SESSION_LIST_LIMIT as jt, takeChildReply as k, DEFAULT_DENY_MESSAGE as kt, handleShellCommand as l, readSessionHistory as lt, getDeviceName as m, mapUserContent as mt, loadNodePty as n, HOST_SCHEME_REGISTRY_KEY as nt, createAgentConsoleServer as o, toCapabilitiesEvent as ot, getPublicKeyText as p, parseMessageBody as pt, readSessionCwd as q, getStateDirectory as qt, getShellFile as r, NODE_PTY_DIRECTORY_NAME as rt, createShellRegistry as s, getSessionTitle as st, spawnPtyShell as t, HOST_INSTALL_DIRECTORY_SEGMENTS as tt, createPairingCodes as u, createSdkMessageMapper as ut, installHost as v, mapHookMessage as vt, handleCommand as w, createFileOriginTracker as wt, checkIsHostInstalled as x, createTodoTracker as xt, getSchemeLaunch as y, mapAssistantContent as yt, createBaseChildMessageSchema as z, HOST_PROBE_TIMEOUT_MS as zt };
|