@sipher.dev/agents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3498 -0
- package/dist/local-machine-controller-Dtiash-C.js +2129 -0
- package/dist/local-machine.d.ts +177 -0
- package/dist/local-machine.js +2 -0
- package/package.json +47 -0
|
@@ -0,0 +1,2129 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
3
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
6
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
7
|
+
import { access, chmod, mkdir, readFile, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const result = {};
|
|
11
|
+
let index = 0;
|
|
12
|
+
const first = argv[0];
|
|
13
|
+
if (first === "login" || first === "logout" || first === "whoami" || first === "connect") {
|
|
14
|
+
result.command = first;
|
|
15
|
+
index = 1;
|
|
16
|
+
} else if (first === "service") {
|
|
17
|
+
result.serviceCommand = parseServiceCommand(argv[1]);
|
|
18
|
+
index = 2;
|
|
19
|
+
}
|
|
20
|
+
for (; index < argv.length; index += 1) {
|
|
21
|
+
const arg = argv[index];
|
|
22
|
+
if (arg === "--server") {
|
|
23
|
+
result.serverUrl = requiredValue(argv, index, arg);
|
|
24
|
+
index += 1;
|
|
25
|
+
} else if (arg === "--profile" || arg === "-p") {
|
|
26
|
+
result.profileName = requiredValue(argv, index, arg);
|
|
27
|
+
index += 1;
|
|
28
|
+
} else if (arg === "--config") {
|
|
29
|
+
result.configPath = requiredValue(argv, index, arg);
|
|
30
|
+
index += 1;
|
|
31
|
+
} else if (arg === "--name") {
|
|
32
|
+
result.name = requiredValue(argv, index, arg);
|
|
33
|
+
index += 1;
|
|
34
|
+
} else if (arg === "--foreground") result.foreground = true;
|
|
35
|
+
else if (arg === "--insecure" || arg === "--tls-skip-verify") result.allowInsecureTls = true;
|
|
36
|
+
else if (arg === "--token") throw new Error("--token is no longer supported. Run agents login.");
|
|
37
|
+
else throw new Error(`Unknown argument: ${arg}\n${usage()}`);
|
|
38
|
+
}
|
|
39
|
+
if (result.foreground && result.command !== "connect") throw new Error("Use agents connect --foreground for local connector testing.");
|
|
40
|
+
if (result.command === "connect" && !result.foreground) throw new Error("Use agents connect --foreground for local connector testing.");
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function parseServiceCommand(value) {
|
|
44
|
+
if (value === "install" || value === "uninstall" || value === "repair" || value === "start" || value === "stop" || value === "restart" || value === "status" || value === "logs") return value;
|
|
45
|
+
throw new Error("Expected service install, service uninstall, service repair, service start, service stop, service restart, service status, or service logs.");
|
|
46
|
+
}
|
|
47
|
+
function requiredValue(argv, index, flag) {
|
|
48
|
+
const value = argv[index + 1];
|
|
49
|
+
if (!value || value.startsWith("-")) throw new Error(`Missing value for ${flag}.\n${usage()}`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function usage() {
|
|
53
|
+
const command = commandName();
|
|
54
|
+
return [
|
|
55
|
+
"Usage:",
|
|
56
|
+
` ${command} login [--server <url>]`,
|
|
57
|
+
` ${command} logout [--server <url>]`,
|
|
58
|
+
` ${command} whoami [--server <url>]`,
|
|
59
|
+
` ${command} connect --foreground [--server <url>]`,
|
|
60
|
+
` ${command} service install [--name <machine-name>] [--server <url>]`,
|
|
61
|
+
` ${command} service uninstall`,
|
|
62
|
+
` ${command} service repair [--server <url>]`,
|
|
63
|
+
` ${command} service start`,
|
|
64
|
+
` ${command} service stop`,
|
|
65
|
+
` ${command} service restart`,
|
|
66
|
+
` ${command} service status`,
|
|
67
|
+
` ${command} service logs`,
|
|
68
|
+
"",
|
|
69
|
+
"Profiles:",
|
|
70
|
+
` ${command} login --profile local --server http://localhost:3000`,
|
|
71
|
+
` ${command} service status -p local`,
|
|
72
|
+
"",
|
|
73
|
+
"Local testing:",
|
|
74
|
+
` ${command} login --server https://agents.lc --insecure`
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
function commandName() {
|
|
78
|
+
const invoked = process.argv[1]?.split(/[\\/]/).pop();
|
|
79
|
+
if (!invoked || invoked === "index.js") return "agents";
|
|
80
|
+
return invoked;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/tls.ts
|
|
84
|
+
function allowInsecureTlsForLocalTesting(options = {}) {
|
|
85
|
+
if (!options.allowInsecureTls) return;
|
|
86
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
87
|
+
}
|
|
88
|
+
function tlsFailureHint(error) {
|
|
89
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
90
|
+
const code = cause && typeof cause === "object" && "code" in cause ? cause.code : void 0;
|
|
91
|
+
if (code === "SELF_SIGNED_CERT_IN_CHAIN" || code === "DEPTH_ZERO_SELF_SIGNED_CERT" || code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") return "TLS certificate validation failed. For local self-signed HTTPS, rerun with --insecure.";
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/auth-client.ts
|
|
95
|
+
const REQUEST_TIMEOUT_MS$1 = 3e4;
|
|
96
|
+
var CliAuthClient = class {
|
|
97
|
+
serverUrl;
|
|
98
|
+
options;
|
|
99
|
+
constructor(serverUrl, options = {}) {
|
|
100
|
+
this.serverUrl = serverUrl;
|
|
101
|
+
this.options = options;
|
|
102
|
+
}
|
|
103
|
+
async startDeviceAuthorization(deviceLabel) {
|
|
104
|
+
const body = deviceLabel ? { device_label: deviceLabel } : {};
|
|
105
|
+
const response = await postJson(new URL("/api/cli/oauth/device", this.serverUrl), body, this.options);
|
|
106
|
+
return {
|
|
107
|
+
deviceCode: readString(response, "device_code"),
|
|
108
|
+
userCode: readString(response, "user_code"),
|
|
109
|
+
verificationUri: readString(response, "verification_uri"),
|
|
110
|
+
verificationUriComplete: readString(response, "verification_uri_complete"),
|
|
111
|
+
expiresIn: readNumber(response, "expires_in"),
|
|
112
|
+
interval: readNumber(response, "interval")
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async pollDeviceToken(deviceCode) {
|
|
116
|
+
return parseTokenResponse(await postForm(new URL("/api/cli/oauth/token", this.serverUrl), {
|
|
117
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
118
|
+
device_code: deviceCode
|
|
119
|
+
}, this.options));
|
|
120
|
+
}
|
|
121
|
+
async refresh(refreshToken) {
|
|
122
|
+
return parseTokenResponse(await postForm(new URL("/api/cli/oauth/token", this.serverUrl), {
|
|
123
|
+
grant_type: "refresh_token",
|
|
124
|
+
refresh_token: refreshToken
|
|
125
|
+
}, this.options));
|
|
126
|
+
}
|
|
127
|
+
async revoke(refreshToken) {
|
|
128
|
+
await postForm(new URL("/api/cli/oauth/revoke", this.serverUrl), { refresh_token: refreshToken }, this.options);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
async function postJson(url, body, options) {
|
|
132
|
+
allowInsecureTlsForLocalTesting(options);
|
|
133
|
+
const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS$1);
|
|
134
|
+
let response;
|
|
135
|
+
try {
|
|
136
|
+
response = await fetch(url, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
body: JSON.stringify(body),
|
|
140
|
+
signal
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(tlsFailureHint(error) ?? "CLI auth request failed: fetch failed.");
|
|
144
|
+
}
|
|
145
|
+
return readResponse(response);
|
|
146
|
+
}
|
|
147
|
+
async function postForm(url, body, options) {
|
|
148
|
+
allowInsecureTlsForLocalTesting(options);
|
|
149
|
+
const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS$1);
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = await fetch(url, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
155
|
+
body: new URLSearchParams(body),
|
|
156
|
+
signal
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new Error(tlsFailureHint(error) ?? "CLI auth request failed: fetch failed.");
|
|
160
|
+
}
|
|
161
|
+
return readResponse(response);
|
|
162
|
+
}
|
|
163
|
+
async function readResponse(response) {
|
|
164
|
+
const value = await response.json().catch(() => ({}));
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const error = isRecord$3(value) && typeof value.error === "string" ? value.error : "request_failed";
|
|
167
|
+
throw new Error(`CLI auth request failed: ${error}`);
|
|
168
|
+
}
|
|
169
|
+
if (!isRecord$3(value)) throw new Error("CLI auth response was invalid.");
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function parseTokenResponse(value) {
|
|
173
|
+
const tokenType = readString(value, "token_type");
|
|
174
|
+
if (tokenType !== "Bearer") throw new Error("CLI auth token response was invalid.");
|
|
175
|
+
return {
|
|
176
|
+
accessToken: readString(value, "access_token"),
|
|
177
|
+
refreshToken: readString(value, "refresh_token"),
|
|
178
|
+
expiresIn: readNumber(value, "expires_in"),
|
|
179
|
+
tokenType
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function readString(value, key) {
|
|
183
|
+
const field = value[key];
|
|
184
|
+
if (typeof field !== "string" || !field.trim()) throw new Error("CLI auth response was invalid.");
|
|
185
|
+
return field;
|
|
186
|
+
}
|
|
187
|
+
function readNumber(value, key) {
|
|
188
|
+
const field = value[key];
|
|
189
|
+
if (typeof field !== "number" || !Number.isFinite(field)) throw new Error("CLI auth response was invalid.");
|
|
190
|
+
return field;
|
|
191
|
+
}
|
|
192
|
+
function isRecord$3(value) {
|
|
193
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
194
|
+
}
|
|
195
|
+
const MAX_MACHINE_TERMINAL_HISTORY_BYTES = 512 * 1024;
|
|
196
|
+
const utf8$2 = new TextEncoder();
|
|
197
|
+
function isTerminalRpcRequest(value) {
|
|
198
|
+
if (!isRecord$2(value) || value.type !== "rpc/request" || !isNonEmptyString$2(value.id)) return false;
|
|
199
|
+
if (value.method === "terminal/open") {
|
|
200
|
+
const params = value.params;
|
|
201
|
+
return isRecord$2(params) && isMachineTerminalId(params.terminalId) && optionalNonEmptyString$1(params.cwd) && optionalPositiveBoundedSafeInteger$1(params.cols, 1e3) && optionalPositiveBoundedSafeInteger$1(params.rows, 500);
|
|
202
|
+
}
|
|
203
|
+
if (value.method === "terminal/write") {
|
|
204
|
+
const params = value.params;
|
|
205
|
+
return isRecord$2(params) && isMachineTerminalId(params.terminalId) && isNonEmptyRawString(params.data) && utf8$2.encode(params.data).byteLength <= 65536;
|
|
206
|
+
}
|
|
207
|
+
if (value.method === "terminal/resize") {
|
|
208
|
+
const params = value.params;
|
|
209
|
+
return isRecord$2(params) && isMachineTerminalId(params.terminalId) && isPositiveBoundedSafeInteger(params.cols, 1e3) && isPositiveBoundedSafeInteger(params.rows, 500);
|
|
210
|
+
}
|
|
211
|
+
if (value.method === "terminal/close") {
|
|
212
|
+
const params = value.params;
|
|
213
|
+
return isRecord$2(params) && isMachineTerminalId(params.terminalId);
|
|
214
|
+
}
|
|
215
|
+
if (value.method === "terminal/clear") {
|
|
216
|
+
const params = value.params;
|
|
217
|
+
return isRecord$2(params) && isMachineTerminalId(params.terminalId);
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
function optionalNonEmptyString$1(value) {
|
|
222
|
+
return value === void 0 || isNonEmptyString$2(value);
|
|
223
|
+
}
|
|
224
|
+
function optionalPositiveBoundedSafeInteger$1(value, max) {
|
|
225
|
+
return value === void 0 || typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= max;
|
|
226
|
+
}
|
|
227
|
+
function isPositiveBoundedSafeInteger(value, max) {
|
|
228
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= max;
|
|
229
|
+
}
|
|
230
|
+
function isMachineTerminalId(value) {
|
|
231
|
+
return isNonEmptyString$2(value) && value.length <= 128;
|
|
232
|
+
}
|
|
233
|
+
function isNonEmptyString$2(value) {
|
|
234
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
235
|
+
}
|
|
236
|
+
function isNonEmptyRawString(value) {
|
|
237
|
+
return typeof value === "string" && value.length > 0;
|
|
238
|
+
}
|
|
239
|
+
function isRecord$2(value) {
|
|
240
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region ../machine-protocol/src/mcp.ts
|
|
244
|
+
const utf8$1 = new TextEncoder();
|
|
245
|
+
const MACHINE_MCP_TAG = "mcp:";
|
|
246
|
+
const MACHINE_MCP_MAX_FRAME_BYTES = 6 * 1024 * 1024;
|
|
247
|
+
const MACHINE_MCP_MAX_STDERR_BYTES = 64 * 1024;
|
|
248
|
+
const MACHINE_MCP_MAX_ERROR_MESSAGE_BYTES = 4 * 1024;
|
|
249
|
+
const CONNECTOR_MAX_WEBSOCKET_PAYLOAD_BYTES = 32 * 1024 * 1024;
|
|
250
|
+
var MachineMcpValidationError = class extends Error {
|
|
251
|
+
code;
|
|
252
|
+
constructor(code, message) {
|
|
253
|
+
super(message);
|
|
254
|
+
this.code = code;
|
|
255
|
+
this.name = "MachineMcpValidationError";
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
function utf8Bytes(value) {
|
|
259
|
+
return utf8$1.encode(value).byteLength;
|
|
260
|
+
}
|
|
261
|
+
function serializedBytes(value) {
|
|
262
|
+
return utf8Bytes(JSON.stringify(value));
|
|
263
|
+
}
|
|
264
|
+
function validateMachineMcpLaunchSpec(value) {
|
|
265
|
+
if (!isRecord$1(value)) invalid("Invalid MCP launch specification.");
|
|
266
|
+
const launch = value;
|
|
267
|
+
if (!hasOnlyKeys(launch, [
|
|
268
|
+
"definitionId",
|
|
269
|
+
"configVersion",
|
|
270
|
+
"catalogRevision",
|
|
271
|
+
"catalogFingerprint",
|
|
272
|
+
"command",
|
|
273
|
+
"args",
|
|
274
|
+
"cwdMode",
|
|
275
|
+
"projectRoot"
|
|
276
|
+
]) || !isBoundedString(launch.definitionId, 256, true) || !isPositiveInteger(launch.configVersion) || !isNonNegativeInteger(launch.catalogRevision) || launch.catalogFingerprint !== void 0 && (typeof launch.catalogFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(launch.catalogFingerprint)) || !isBoundedString(launch.command, 4096, true) || !Array.isArray(launch.args) || launch.args.length > 64 || !launch.args.every((arg) => typeof arg === "string" && utf8Bytes(arg) <= 4096) || launch.args.reduce((total, arg) => total + (typeof arg === "string" ? utf8Bytes(arg) : 0), 0) > 65536 || launch.cwdMode !== "project_root" && launch.cwdMode !== "home" || !isAbsoluteHostPath$1(launch.projectRoot)) invalid("Invalid MCP launch specification.");
|
|
277
|
+
if (utf8Bytes(launch.projectRoot) > 32768) throw new MachineMcpValidationError("project_root_too_large", "Project root is too large.");
|
|
278
|
+
if (serializedBytes(value) > 131072) throw new MachineMcpValidationError("launch_request_too_large", "MCP launch request is too large.");
|
|
279
|
+
if (serializedBytes({
|
|
280
|
+
...launch,
|
|
281
|
+
args: launch.args.map((arg) => arg === "{projectRoot}" ? launch.projectRoot : arg),
|
|
282
|
+
cwd: launch.projectRoot
|
|
283
|
+
}) > 131072) throw new MachineMcpValidationError("launch_request_too_large", "Expanded MCP launch request is too large.");
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
function validateMachineMcpRequest(value) {
|
|
287
|
+
if (!isRecord$1(value) || value.type !== "mcp/request" || !isBoundedString(value.id, 256, true)) invalid("Invalid MCP request.");
|
|
288
|
+
if (!isRecord$1(value.params)) invalid("Invalid MCP request parameters.");
|
|
289
|
+
if (!hasOnlyKeys(value, [
|
|
290
|
+
"type",
|
|
291
|
+
"id",
|
|
292
|
+
"method",
|
|
293
|
+
"params"
|
|
294
|
+
])) invalid("Invalid MCP request.");
|
|
295
|
+
if (value.method === "mcp/probe") {
|
|
296
|
+
if (!hasOnlyKeys(value.params, ["launch", "timeoutMs"])) invalid("Invalid MCP probe parameters.");
|
|
297
|
+
validateMachineMcpLaunchSpec(value.params.launch);
|
|
298
|
+
if (!isBoundedTimeout(value.params.timeoutMs)) invalid("Invalid MCP timeout.");
|
|
299
|
+
} else if (value.method === "mcp/call") {
|
|
300
|
+
if (!hasOnlyKeys(value.params, [
|
|
301
|
+
"launch",
|
|
302
|
+
"toolName",
|
|
303
|
+
"arguments",
|
|
304
|
+
"timeoutMs"
|
|
305
|
+
])) invalid("Invalid MCP call parameters.");
|
|
306
|
+
validateMachineMcpCallRequestSize(value);
|
|
307
|
+
if (!validateMachineMcpLaunchSpec(value.params.launch).catalogFingerprint) invalid("MCP calls require an accepted catalog fingerprint.");
|
|
308
|
+
if (!isBoundedString(value.params.toolName, 256, true)) invalid("Invalid MCP tool name.");
|
|
309
|
+
if (!isRecord$1(value.params.arguments)) invalid("Invalid MCP tool arguments.");
|
|
310
|
+
if (serializedBytes(value.params.arguments) > 262144) throw new MachineMcpValidationError("arguments_too_large", "MCP tool arguments are too large.");
|
|
311
|
+
if (!isBoundedTimeout(value.params.timeoutMs)) invalid("Invalid MCP timeout.");
|
|
312
|
+
} else if (value.method === "mcp/stop") {
|
|
313
|
+
if (!hasOnlyKeys(value.params, [
|
|
314
|
+
"definitionId",
|
|
315
|
+
"configVersion",
|
|
316
|
+
"projectRoot"
|
|
317
|
+
]) || !isBoundedString(value.params.definitionId, 256, true) || value.params.configVersion !== void 0 && !isPositiveInteger(value.params.configVersion) || value.params.projectRoot !== void 0 && (!isAbsoluteHostPath$1(value.params.projectRoot) || utf8Bytes(value.params.projectRoot) > 32768)) invalid("Invalid MCP stop request.");
|
|
318
|
+
} else if (value.method === "mcp/cancel") {
|
|
319
|
+
if (!hasOnlyKeys(value.params, ["requestId"]) || !isBoundedString(value.params.requestId, 256, true)) invalid("Invalid MCP cancel request.");
|
|
320
|
+
} else invalid("Unknown MCP request method.");
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
function validateMachineMcpCallRequestSize(value) {
|
|
324
|
+
if (serializedBytes(value) > 524288) throw new MachineMcpValidationError("launch_request_too_large", "MCP call request is too large.");
|
|
325
|
+
}
|
|
326
|
+
function encodeMachineMcpFrame(message) {
|
|
327
|
+
const frame = `${MACHINE_MCP_TAG}${JSON.stringify(message)}`;
|
|
328
|
+
if (utf8Bytes(frame) > 6291456) throw new MachineMcpValidationError("frame_too_large", "MCP connector frame is too large.");
|
|
329
|
+
return frame;
|
|
330
|
+
}
|
|
331
|
+
function validateMachineMcpResult(result) {
|
|
332
|
+
if (serializedBytes(result) > 5242880) throw new MachineMcpValidationError("result_too_large", "MCP result is too large.");
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
function sanitizeMachineMcpCatalog(input) {
|
|
336
|
+
const serverInfo = sanitizeServerInfo(input.serverInfo);
|
|
337
|
+
if (!Array.isArray(input.tools) || input.tools.length > 500) throw new MachineMcpValidationError("catalog_overflow", "MCP catalog has too many tools.");
|
|
338
|
+
let catalogValues = 0;
|
|
339
|
+
const tools = input.tools.map((tool) => {
|
|
340
|
+
if (!isRecord$1(tool) || typeof tool.name !== "string") throw new MachineMcpValidationError("catalog_structure_overflow", "Invalid MCP tool name.");
|
|
341
|
+
const name = tool.name.trim();
|
|
342
|
+
if (!isBoundedString(name, 256, true)) throw new MachineMcpValidationError("catalog_structure_overflow", "Invalid MCP tool name.");
|
|
343
|
+
let toolValues = 0;
|
|
344
|
+
const countValues = (count) => {
|
|
345
|
+
toolValues += count;
|
|
346
|
+
catalogValues += count;
|
|
347
|
+
if (toolValues > 1e4) structureError();
|
|
348
|
+
if (catalogValues > 1e5) structureError();
|
|
349
|
+
};
|
|
350
|
+
const sanitized = {
|
|
351
|
+
name,
|
|
352
|
+
inputSchema: isRecord$1(tool.inputSchema) ? sanitizeObject(tool.inputSchema, countValues) : { type: "object" }
|
|
353
|
+
};
|
|
354
|
+
if (typeof tool.title === "string") {
|
|
355
|
+
if (utf8Bytes(tool.title) > 16384) structureError();
|
|
356
|
+
sanitized.title = tool.title;
|
|
357
|
+
}
|
|
358
|
+
if (typeof tool.description === "string") {
|
|
359
|
+
if (utf8Bytes(tool.description) > 16384) structureError();
|
|
360
|
+
sanitized.description = tool.description.trim();
|
|
361
|
+
}
|
|
362
|
+
if (isRecord$1(tool.outputSchema)) sanitized.outputSchema = sanitizeObject(tool.outputSchema, countValues);
|
|
363
|
+
if (isRecord$1(tool.annotations)) sanitized.annotations = sanitizeObject(tool.annotations, countValues);
|
|
364
|
+
if (catalogValues > 1e5) structureError();
|
|
365
|
+
if (serializedBytes(sanitized) > 262144) throw new MachineMcpValidationError("catalog_overflow", "MCP tool is too large.");
|
|
366
|
+
return sanitized;
|
|
367
|
+
}).sort((a, b) => compareCodeUnits(a.name, b.name));
|
|
368
|
+
if (new Set(tools.map((tool) => tool.name)).size !== tools.length) throw new MachineMcpValidationError("catalog_structure_overflow", "MCP tool names must be unique.");
|
|
369
|
+
const catalog = {
|
|
370
|
+
serverInfo,
|
|
371
|
+
tools
|
|
372
|
+
};
|
|
373
|
+
if (serializedBytes(catalog) > 1048576) throw new MachineMcpValidationError("catalog_overflow", "MCP catalog is too large.");
|
|
374
|
+
return catalog;
|
|
375
|
+
}
|
|
376
|
+
function sanitizeAndCanonicalizeMachineMcpCatalog(input) {
|
|
377
|
+
const catalog = sanitizeMachineMcpCatalog(input);
|
|
378
|
+
return {
|
|
379
|
+
catalog,
|
|
380
|
+
canonicalJson: canonicalizeSanitizedMachineMcpCatalog(catalog)
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function canonicalizeSanitizedMachineMcpCatalog(catalog) {
|
|
384
|
+
return canonicalJson(catalog.tools);
|
|
385
|
+
}
|
|
386
|
+
function substituteMachineMcpLaunch(launch, platform = processPlatform(), homeRoot = launch.projectRoot) {
|
|
387
|
+
const root = platform === "windows" ? launch.projectRoot.replaceAll("/", "\\") : launch.projectRoot.replaceAll("\\", "/");
|
|
388
|
+
const command = launch.command;
|
|
389
|
+
const args = launch.args.map((argument) => argument === "{projectRoot}" ? root : argument);
|
|
390
|
+
const cwd = launch.cwdMode === "project_root" ? root : homeRoot;
|
|
391
|
+
const resolved = {
|
|
392
|
+
command,
|
|
393
|
+
args,
|
|
394
|
+
cwd
|
|
395
|
+
};
|
|
396
|
+
if (serializedBytes({
|
|
397
|
+
...launch,
|
|
398
|
+
command,
|
|
399
|
+
args,
|
|
400
|
+
...cwd ? { cwd } : {}
|
|
401
|
+
}) > 131072) throw new MachineMcpValidationError("launch_request_too_large", "Resolved MCP launch request is too large.");
|
|
402
|
+
return resolved;
|
|
403
|
+
}
|
|
404
|
+
function sanitizeServerInfo(value) {
|
|
405
|
+
if (!isRecord$1(value) || !isBoundedString(value.name, 256, true)) structureError("Invalid MCP server info.");
|
|
406
|
+
if (value.version !== void 0 && !isBoundedString(value.version, 256, true)) structureError("Invalid MCP server version.");
|
|
407
|
+
return {
|
|
408
|
+
name: value.name,
|
|
409
|
+
...value.version ? { version: value.version } : {}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function sanitizeObject(value, addToCatalog) {
|
|
413
|
+
if (!isRecord$1(value)) structureError();
|
|
414
|
+
const output = {};
|
|
415
|
+
const stack = [{
|
|
416
|
+
source: value,
|
|
417
|
+
target: output,
|
|
418
|
+
depth: 1
|
|
419
|
+
}];
|
|
420
|
+
let values = 0;
|
|
421
|
+
while (stack.length > 0) {
|
|
422
|
+
const item = stack.pop();
|
|
423
|
+
if (item.depth > 32) structureError();
|
|
424
|
+
const entries = Array.isArray(item.source) ? item.source.map((entry, index) => [String(index), entry]) : Object.entries(item.source).sort(([a], [b]) => compareCodeUnits(a, b));
|
|
425
|
+
if (Array.isArray(item.source) && entries.length > 4096) structureError();
|
|
426
|
+
if (!Array.isArray(item.source) && entries.length > 1024) structureError();
|
|
427
|
+
for (const [key, child] of entries) {
|
|
428
|
+
values += 1;
|
|
429
|
+
if (values > 1e4) structureError();
|
|
430
|
+
if (utf8Bytes(key) > 65536) structureError();
|
|
431
|
+
let next;
|
|
432
|
+
if (typeof child === "string") {
|
|
433
|
+
if (utf8Bytes(child) > 65536) structureError();
|
|
434
|
+
next = child;
|
|
435
|
+
} else if (child === null || typeof child === "boolean" || typeof child === "number") {
|
|
436
|
+
if (typeof child === "number" && !Number.isFinite(child)) structureError();
|
|
437
|
+
next = child;
|
|
438
|
+
} else if (Array.isArray(child)) {
|
|
439
|
+
next = [];
|
|
440
|
+
stack.push({
|
|
441
|
+
source: child,
|
|
442
|
+
target: next,
|
|
443
|
+
depth: item.depth + 1
|
|
444
|
+
});
|
|
445
|
+
} else if (isRecord$1(child)) {
|
|
446
|
+
next = {};
|
|
447
|
+
stack.push({
|
|
448
|
+
source: child,
|
|
449
|
+
target: next,
|
|
450
|
+
depth: item.depth + 1
|
|
451
|
+
});
|
|
452
|
+
} else structureError();
|
|
453
|
+
if (Array.isArray(item.target)) item.target.push(next);
|
|
454
|
+
else Object.defineProperty(item.target, key, {
|
|
455
|
+
configurable: true,
|
|
456
|
+
enumerable: true,
|
|
457
|
+
value: next,
|
|
458
|
+
writable: true
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
addToCatalog(values);
|
|
463
|
+
return output;
|
|
464
|
+
}
|
|
465
|
+
function canonicalJson(value) {
|
|
466
|
+
const output = [];
|
|
467
|
+
const stack = [{
|
|
468
|
+
kind: "value",
|
|
469
|
+
value
|
|
470
|
+
}];
|
|
471
|
+
while (stack.length > 0) {
|
|
472
|
+
const task = stack.pop();
|
|
473
|
+
if (task.kind === "text") {
|
|
474
|
+
output.push(task.value);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const item = task.value;
|
|
478
|
+
if (Array.isArray(item)) {
|
|
479
|
+
stack.push({
|
|
480
|
+
kind: "text",
|
|
481
|
+
value: "]"
|
|
482
|
+
});
|
|
483
|
+
for (let index = item.length - 1; index >= 0; index -= 1) {
|
|
484
|
+
stack.push({
|
|
485
|
+
kind: "value",
|
|
486
|
+
value: item[index]
|
|
487
|
+
});
|
|
488
|
+
if (index > 0) stack.push({
|
|
489
|
+
kind: "text",
|
|
490
|
+
value: ","
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
stack.push({
|
|
494
|
+
kind: "text",
|
|
495
|
+
value: "["
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (isRecord$1(item)) {
|
|
500
|
+
const keys = Object.keys(item).sort(compareCodeUnits);
|
|
501
|
+
stack.push({
|
|
502
|
+
kind: "text",
|
|
503
|
+
value: "}"
|
|
504
|
+
});
|
|
505
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
506
|
+
const key = keys[index];
|
|
507
|
+
stack.push({
|
|
508
|
+
kind: "value",
|
|
509
|
+
value: item[key]
|
|
510
|
+
});
|
|
511
|
+
stack.push({
|
|
512
|
+
kind: "text",
|
|
513
|
+
value: `${JSON.stringify(key)}:`
|
|
514
|
+
});
|
|
515
|
+
if (index > 0) stack.push({
|
|
516
|
+
kind: "text",
|
|
517
|
+
value: ","
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
stack.push({
|
|
521
|
+
kind: "text",
|
|
522
|
+
value: "{"
|
|
523
|
+
});
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const encoded = JSON.stringify(item);
|
|
527
|
+
if (encoded === void 0) structureError();
|
|
528
|
+
output.push(encoded);
|
|
529
|
+
}
|
|
530
|
+
return output.join("");
|
|
531
|
+
}
|
|
532
|
+
function compareCodeUnits(left, right) {
|
|
533
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
534
|
+
}
|
|
535
|
+
function isBoundedString(value, maxBytes, nonBlank) {
|
|
536
|
+
return typeof value === "string" && (!nonBlank || value.trim().length > 0) && utf8Bytes(value) <= maxBytes;
|
|
537
|
+
}
|
|
538
|
+
function isAbsoluteHostPath$1(value) {
|
|
539
|
+
return isNonEmptyString$1(value) && !value.includes("\0") && (value.startsWith("/") || value.startsWith("\\\\") || /^[A-Za-z]:[\\/]/.test(value));
|
|
540
|
+
}
|
|
541
|
+
function isNonEmptyString$1(value) {
|
|
542
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
543
|
+
}
|
|
544
|
+
function isRecord$1(value) {
|
|
545
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
546
|
+
}
|
|
547
|
+
function isPositiveInteger(value) {
|
|
548
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
549
|
+
}
|
|
550
|
+
function isNonNegativeInteger(value) {
|
|
551
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
552
|
+
}
|
|
553
|
+
function isBoundedTimeout(value) {
|
|
554
|
+
return isPositiveInteger(value) && value <= 12e4;
|
|
555
|
+
}
|
|
556
|
+
function hasOnlyKeys(value, allowed) {
|
|
557
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
558
|
+
}
|
|
559
|
+
function invalid(message) {
|
|
560
|
+
throw new MachineMcpValidationError("invalid_request", message);
|
|
561
|
+
}
|
|
562
|
+
function structureError(message = "MCP catalog structure is too large or invalid.") {
|
|
563
|
+
throw new MachineMcpValidationError("catalog_structure_overflow", message);
|
|
564
|
+
}
|
|
565
|
+
function processPlatform() {
|
|
566
|
+
return typeof process !== "undefined" && process.platform === "win32" ? "windows" : "posix";
|
|
567
|
+
}
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region ../machine-protocol/src/index.ts
|
|
570
|
+
const DEFAULT_MACHINE_EXEC_TIMEOUT_MS = 6e4;
|
|
571
|
+
const MAX_MACHINE_EXEC_TIMEOUT_MS = 12e4;
|
|
572
|
+
const DEFAULT_MACHINE_EXEC_OUTPUT_BYTES = 512e3;
|
|
573
|
+
const MAX_MACHINE_EXEC_OUTPUT_BYTES = 1048576;
|
|
574
|
+
const DEFAULT_MACHINE_FILE_READ_BYTES = 20 * 1024 * 1024;
|
|
575
|
+
const MAX_MACHINE_FILE_READ_BYTES = 20 * 1024 * 1024;
|
|
576
|
+
const DEFAULT_TEXT_FILE_READ_BYTES = 50 * 1024;
|
|
577
|
+
const MAX_TEXT_FILE_READ_BYTES = 50 * 1024;
|
|
578
|
+
const DEFAULT_TEXT_FILE_READ_LINES = 2e3;
|
|
579
|
+
const MAX_TEXT_FILE_READ_LINES = 2e3;
|
|
580
|
+
const MAX_MACHINE_JOB_OUTPUT_BYTES = 512 * 1024;
|
|
581
|
+
const MAX_PATCH_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
|
|
582
|
+
const MAX_TEXT_FILE_WRITE_BYTES = 5 * 1024 * 1024;
|
|
583
|
+
const MAX_MACHINE_FILE_SEARCH_PATH_LENGTH = 4096;
|
|
584
|
+
const utf8 = new TextEncoder();
|
|
585
|
+
function isServerRpcRequest(value) {
|
|
586
|
+
if (!isRecord(value)) return false;
|
|
587
|
+
if (value.type !== "rpc/request" || !isNonEmptyString(value.id)) return false;
|
|
588
|
+
if (isTerminalRpcRequest(value)) return true;
|
|
589
|
+
if (value.method === "shell/exec") {
|
|
590
|
+
const params = value.params;
|
|
591
|
+
return isRecord(params) && isNonEmptyString(params.shellId) && isNonEmptyString(params.command) && optionalString(params.cwd) && optionalNumber(params.timeoutMs) && optionalNumber(params.maxOutputBytes);
|
|
592
|
+
}
|
|
593
|
+
if (value.method === "shell/close") {
|
|
594
|
+
const params = value.params;
|
|
595
|
+
return isRecord(params) && isNonEmptyString(params.shellId);
|
|
596
|
+
}
|
|
597
|
+
if (value.method === "shell/job_start") {
|
|
598
|
+
const params = value.params;
|
|
599
|
+
return isRecord(params) && isNonEmptyString(params.command) && optionalNonEmptyString(params.cwd);
|
|
600
|
+
}
|
|
601
|
+
if (value.method === "shell/job_get") {
|
|
602
|
+
const params = value.params;
|
|
603
|
+
return isRecord(params) && isBoundedNonEmptyString(params.jobId, 128) && optionalNonNegativeSafeInteger(params.cursor);
|
|
604
|
+
}
|
|
605
|
+
if (value.method === "shell/job_kill") {
|
|
606
|
+
const params = value.params;
|
|
607
|
+
return isRecord(params) && isBoundedNonEmptyString(params.jobId, 128);
|
|
608
|
+
}
|
|
609
|
+
if (value.method === "file/read") {
|
|
610
|
+
const params = value.params;
|
|
611
|
+
return isRecord(params) && isNonEmptyString(params.path) && optionalPositiveBoundedSafeInteger(params.maxBytes, 20971520);
|
|
612
|
+
}
|
|
613
|
+
if (value.method === "file/read_text") {
|
|
614
|
+
const params = value.params;
|
|
615
|
+
return isRecord(params) && isNonEmptyString(params.path) && (params.mode === void 0 || params.mode === "offset" || params.mode === "tail") && !(params.mode === "tail" && params.offset !== void 0) && optionalPositiveSafeInteger(params.offset) && optionalPositiveBoundedSafeInteger(params.limit, 2e3) && optionalPositiveBoundedSafeInteger(params.maxBytes, 51200);
|
|
616
|
+
}
|
|
617
|
+
if (value.method === "file/read_patch_source") {
|
|
618
|
+
const params = value.params;
|
|
619
|
+
return isRecord(params) && isNonEmptyString(params.path) && optionalPositiveBoundedSafeInteger(params.maxBytes, 5242880);
|
|
620
|
+
}
|
|
621
|
+
if (value.method === "file/write") {
|
|
622
|
+
const params = value.params;
|
|
623
|
+
return isRecord(params) && isNonEmptyString(params.path) && typeof params.content === "string" && utf8.encode(params.content).byteLength <= 5242880;
|
|
624
|
+
}
|
|
625
|
+
if (value.method === "file/delete") {
|
|
626
|
+
const params = value.params;
|
|
627
|
+
return isRecord(params) && isNonEmptyString(params.path);
|
|
628
|
+
}
|
|
629
|
+
if (value.method === "file/move") {
|
|
630
|
+
const params = value.params;
|
|
631
|
+
return isRecord(params) && isNonEmptyString(params.sourcePath) && isNonEmptyString(params.targetPath);
|
|
632
|
+
}
|
|
633
|
+
if (value.method === "file/search_entries") {
|
|
634
|
+
const params = value.params;
|
|
635
|
+
return isRecord(params) && isAbsoluteHostPath(params.rootPath) && typeof params.query === "string" && optionalPositiveBoundedSafeInteger(params.limit, 100);
|
|
636
|
+
}
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
function optionalString(value) {
|
|
640
|
+
return value === void 0 || typeof value === "string";
|
|
641
|
+
}
|
|
642
|
+
function optionalNonEmptyString(value) {
|
|
643
|
+
return value === void 0 || isNonEmptyString(value);
|
|
644
|
+
}
|
|
645
|
+
function isBoundedNonEmptyString(value, maxLength) {
|
|
646
|
+
return isNonEmptyString(value) && value.length <= maxLength;
|
|
647
|
+
}
|
|
648
|
+
function optionalNumber(value) {
|
|
649
|
+
return value === void 0 || typeof value === "number" && Number.isFinite(value);
|
|
650
|
+
}
|
|
651
|
+
function optionalPositiveBoundedSafeInteger(value, max) {
|
|
652
|
+
return value === void 0 || typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= max;
|
|
653
|
+
}
|
|
654
|
+
function optionalPositiveSafeInteger(value) {
|
|
655
|
+
return value === void 0 || isPositiveSafeInteger(value);
|
|
656
|
+
}
|
|
657
|
+
function optionalNonNegativeSafeInteger(value) {
|
|
658
|
+
return value === void 0 || isNonNegativeSafeInteger(value);
|
|
659
|
+
}
|
|
660
|
+
function isPositiveSafeInteger(value) {
|
|
661
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
662
|
+
}
|
|
663
|
+
function isNonNegativeSafeInteger(value) {
|
|
664
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
665
|
+
}
|
|
666
|
+
function isAbsoluteHostPath(value) {
|
|
667
|
+
if (!isNonEmptyString(value) || value.includes("\0")) return false;
|
|
668
|
+
return value.startsWith("/") || value.startsWith("\\\\") || /^[A-Za-z]:[\\/]/.test(value);
|
|
669
|
+
}
|
|
670
|
+
function isNonEmptyString(value) {
|
|
671
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
672
|
+
}
|
|
673
|
+
function isRecord(value) {
|
|
674
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
675
|
+
}
|
|
676
|
+
//#endregion
|
|
677
|
+
//#region src/shell-detect.ts
|
|
678
|
+
function detectShell() {
|
|
679
|
+
if (process.platform === "win32") return {
|
|
680
|
+
os: "windows",
|
|
681
|
+
name: "powershell",
|
|
682
|
+
path: "powershell.exe",
|
|
683
|
+
args: [
|
|
684
|
+
"-NoLogo",
|
|
685
|
+
"-NoProfile",
|
|
686
|
+
"-Command",
|
|
687
|
+
"-"
|
|
688
|
+
]
|
|
689
|
+
};
|
|
690
|
+
const path = findExecutable([
|
|
691
|
+
process.env.SHELL,
|
|
692
|
+
loginShellPath(),
|
|
693
|
+
"/bin/bash",
|
|
694
|
+
"/usr/bin/bash",
|
|
695
|
+
"/opt/homebrew/bin/bash",
|
|
696
|
+
"/bin/sh"
|
|
697
|
+
]);
|
|
698
|
+
const name = shellName(path);
|
|
699
|
+
return {
|
|
700
|
+
os: toMachineOs(platform()),
|
|
701
|
+
name,
|
|
702
|
+
path,
|
|
703
|
+
args: name === "bash" ? ["--noprofile", "--norc"] : []
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function loginShellPath() {
|
|
707
|
+
try {
|
|
708
|
+
return userInfo().shell ?? void 0;
|
|
709
|
+
} catch {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function findExecutable(paths) {
|
|
714
|
+
return paths.find(isExecutable) ?? "/bin/sh";
|
|
715
|
+
}
|
|
716
|
+
function isExecutable(path) {
|
|
717
|
+
if (!path || !isAbsolute(path)) return false;
|
|
718
|
+
try {
|
|
719
|
+
accessSync(path, constants.X_OK);
|
|
720
|
+
return statSync(path).isFile();
|
|
721
|
+
} catch {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function shellName(path) {
|
|
726
|
+
const name = basename(path);
|
|
727
|
+
if (name === "bash" || name === "sh" || name === "zsh") return name;
|
|
728
|
+
return "unknown";
|
|
729
|
+
}
|
|
730
|
+
function toMachineOs(value) {
|
|
731
|
+
if (value === "darwin" || value === "linux") return value;
|
|
732
|
+
if (value === "win32") return "windows";
|
|
733
|
+
return "unknown";
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/shell-manager.ts
|
|
737
|
+
const PARSE_TAIL_BYTES = 16384;
|
|
738
|
+
var ShellManager = class {
|
|
739
|
+
shells = /* @__PURE__ */ new Map();
|
|
740
|
+
shell = detectShell();
|
|
741
|
+
async exec(input) {
|
|
742
|
+
return this.getOrCreate(input.shellId, input.cwd).exec(input.command, {
|
|
743
|
+
timeoutMs: clampPositiveInteger(input.timeoutMs, DEFAULT_MACHINE_EXEC_TIMEOUT_MS, MAX_MACHINE_EXEC_TIMEOUT_MS),
|
|
744
|
+
maxOutputBytes: clampPositiveInteger(input.maxOutputBytes, DEFAULT_MACHINE_EXEC_OUTPUT_BYTES, MAX_MACHINE_EXEC_OUTPUT_BYTES)
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
close(shellId) {
|
|
748
|
+
this.shells.get(shellId)?.close();
|
|
749
|
+
this.shells.delete(shellId);
|
|
750
|
+
}
|
|
751
|
+
closeAll() {
|
|
752
|
+
for (const shell of this.shells.values()) shell.close();
|
|
753
|
+
this.shells.clear();
|
|
754
|
+
}
|
|
755
|
+
shellInfo() {
|
|
756
|
+
return this.shell;
|
|
757
|
+
}
|
|
758
|
+
getOrCreate(shellId, cwd) {
|
|
759
|
+
const existing = this.shells.get(shellId);
|
|
760
|
+
if (existing && !existing.closed) return existing;
|
|
761
|
+
const session = new ShellSession(this.shell, resolveShellCwd(cwd));
|
|
762
|
+
this.shells.set(shellId, session);
|
|
763
|
+
return session;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
function resolveShellCwd(cwd) {
|
|
767
|
+
const home = homedir();
|
|
768
|
+
if (!cwd) return home;
|
|
769
|
+
return isAbsolute(cwd) ? cwd : resolve(home, cwd);
|
|
770
|
+
}
|
|
771
|
+
var ShellSession = class {
|
|
772
|
+
shell;
|
|
773
|
+
child;
|
|
774
|
+
active;
|
|
775
|
+
queue = Promise.resolve();
|
|
776
|
+
closed = false;
|
|
777
|
+
constructor(shell, cwd) {
|
|
778
|
+
this.shell = shell;
|
|
779
|
+
this.child = spawn(shell.path, shell.args, {
|
|
780
|
+
cwd,
|
|
781
|
+
stdio: "pipe",
|
|
782
|
+
env: process.env
|
|
783
|
+
});
|
|
784
|
+
this.child.stdout.setEncoding("utf8");
|
|
785
|
+
this.child.stderr.setEncoding("utf8");
|
|
786
|
+
this.child.stdout.on("data", (chunk) => this.handleStdout(chunk));
|
|
787
|
+
this.child.stderr.on("data", (chunk) => this.handleStderr(chunk));
|
|
788
|
+
this.child.once("error", (error) => {
|
|
789
|
+
this.closed = true;
|
|
790
|
+
this.active?.resolve({
|
|
791
|
+
ok: false,
|
|
792
|
+
errorCode: "shell_start_failed",
|
|
793
|
+
message: error.message
|
|
794
|
+
});
|
|
795
|
+
this.active = void 0;
|
|
796
|
+
});
|
|
797
|
+
this.child.once("exit", () => {
|
|
798
|
+
this.closed = true;
|
|
799
|
+
this.active?.resolve({
|
|
800
|
+
ok: false,
|
|
801
|
+
errorCode: "execution_failed",
|
|
802
|
+
message: "Shell exited before the command completed."
|
|
803
|
+
});
|
|
804
|
+
this.active = void 0;
|
|
805
|
+
});
|
|
806
|
+
this.child.stdin.on("error", (error) => {
|
|
807
|
+
this.failActive("execution_failed", error.message);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
async exec(command, options) {
|
|
811
|
+
const run = () => this.execNow(command, options);
|
|
812
|
+
const result = this.queue.then(run, run);
|
|
813
|
+
this.queue = result.then(() => void 0, () => void 0);
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
close() {
|
|
817
|
+
this.closed = true;
|
|
818
|
+
this.child.kill();
|
|
819
|
+
}
|
|
820
|
+
execNow(command, options) {
|
|
821
|
+
if (this.closed) return Promise.resolve({
|
|
822
|
+
ok: false,
|
|
823
|
+
errorCode: "shell_start_failed",
|
|
824
|
+
message: "Shell is closed."
|
|
825
|
+
});
|
|
826
|
+
const marker = randomUUID().replaceAll("-", "");
|
|
827
|
+
const wrapped = wrapCommand(this.shell.name, command, marker);
|
|
828
|
+
return new Promise((resolve) => {
|
|
829
|
+
const timeout = setTimeout(() => {
|
|
830
|
+
this.active = void 0;
|
|
831
|
+
this.close();
|
|
832
|
+
resolve({
|
|
833
|
+
ok: false,
|
|
834
|
+
errorCode: "command_timeout",
|
|
835
|
+
message: `Command timed out after ${options.timeoutMs}ms. The persistent shell was reset.`
|
|
836
|
+
});
|
|
837
|
+
}, options.timeoutMs);
|
|
838
|
+
this.active = {
|
|
839
|
+
marker,
|
|
840
|
+
started: false,
|
|
841
|
+
stdout: "",
|
|
842
|
+
stdoutBytes: 0,
|
|
843
|
+
stdoutTruncated: false,
|
|
844
|
+
stdoutTail: "",
|
|
845
|
+
stderr: "",
|
|
846
|
+
stderrBytes: 0,
|
|
847
|
+
stderrTruncated: false,
|
|
848
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
849
|
+
resolve: (response) => {
|
|
850
|
+
clearTimeout(timeout);
|
|
851
|
+
this.active = void 0;
|
|
852
|
+
resolve(response);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
if (this.child.stdin.destroyed || this.child.stdin.writableEnded || !this.child.stdin.writable) {
|
|
856
|
+
this.failActive("execution_failed", "Shell stdin is not writable.");
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
this.child.stdin.write(wrapped, (error) => {
|
|
860
|
+
if (error && this.active?.marker === marker) this.failActive("execution_failed", error.message);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
failActive(errorCode, message) {
|
|
865
|
+
this.closed = true;
|
|
866
|
+
this.child.kill();
|
|
867
|
+
this.active?.resolve({
|
|
868
|
+
ok: false,
|
|
869
|
+
errorCode,
|
|
870
|
+
message
|
|
871
|
+
});
|
|
872
|
+
this.active = void 0;
|
|
873
|
+
}
|
|
874
|
+
handleStdout(chunk) {
|
|
875
|
+
const active = this.active;
|
|
876
|
+
if (!active) return;
|
|
877
|
+
appendStdout(active, chunk);
|
|
878
|
+
const parsed = parseCompletedStdout(active.stdoutTail, active.marker);
|
|
879
|
+
if (!parsed) return;
|
|
880
|
+
active.resolve({
|
|
881
|
+
ok: true,
|
|
882
|
+
stdout: finishStdout(active, parsed.stdout),
|
|
883
|
+
stderr: finishCapturedOutput(active.stderr, active.stderrTruncated, active.maxOutputBytes),
|
|
884
|
+
exitCode: parsed.exitCode,
|
|
885
|
+
timedOut: false
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
handleStderr(chunk) {
|
|
889
|
+
if (this.active) {
|
|
890
|
+
const next = appendCappedChunk(this.active.stderr, this.active.stderrBytes, this.active.stderrTruncated, chunk, this.active.maxOutputBytes);
|
|
891
|
+
this.active.stderr = next.value;
|
|
892
|
+
this.active.stderrBytes = next.bytes;
|
|
893
|
+
this.active.stderrTruncated = next.truncated;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
function wrapCommand(shellName, command, marker) {
|
|
898
|
+
if (shellName === "powershell") return [
|
|
899
|
+
`Write-Output "__APP_START_${marker}__"`,
|
|
900
|
+
"& {",
|
|
901
|
+
command,
|
|
902
|
+
"}",
|
|
903
|
+
"$appEc = if ($LASTEXITCODE -ne $null) { $LASTEXITCODE } else { 0 }",
|
|
904
|
+
`Write-Output "__APP_DONE_${marker}__:$appEc"`,
|
|
905
|
+
""
|
|
906
|
+
].join("\n");
|
|
907
|
+
return [
|
|
908
|
+
`printf '\\n__APP_START_${marker}__\\n'`,
|
|
909
|
+
"{",
|
|
910
|
+
command,
|
|
911
|
+
"}",
|
|
912
|
+
"__app_ec=$?",
|
|
913
|
+
`printf '\\n__APP_DONE_${marker}__:%s\\n' "$__app_ec"`,
|
|
914
|
+
""
|
|
915
|
+
].join("\n");
|
|
916
|
+
}
|
|
917
|
+
function parseCompletedStdout(stdout, marker) {
|
|
918
|
+
const donePattern = new RegExp(`(^|\\r?\\n)__APP_DONE_${marker}__:(-?\\d+)(\\r?\\n|$)`);
|
|
919
|
+
const doneMatch = stdout.match(donePattern);
|
|
920
|
+
if (doneMatch?.index === void 0) return void 0;
|
|
921
|
+
const beforeDone = stdout.slice(0, doneMatch.index);
|
|
922
|
+
const startPattern = new RegExp(`(^|\\r?\\n)__APP_START_${marker}__(\\r?\\n|$)`);
|
|
923
|
+
return {
|
|
924
|
+
stdout: beforeDone.replace(startPattern, "").replace(/^\r?\n/, ""),
|
|
925
|
+
exitCode: Number(doneMatch[2])
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function appendStdout(active, chunk) {
|
|
929
|
+
const outputChunk = stdoutOutputChunk(active, chunk);
|
|
930
|
+
if (!outputChunk) {
|
|
931
|
+
active.stdoutTail = capTail(`${active.stdoutTail}${chunk}`, PARSE_TAIL_BYTES);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const next = appendCappedChunk(active.stdout, active.stdoutBytes, active.stdoutTruncated, outputChunk, active.maxOutputBytes);
|
|
935
|
+
active.stdout = next.value;
|
|
936
|
+
active.stdoutBytes = next.bytes;
|
|
937
|
+
active.stdoutTruncated = next.truncated;
|
|
938
|
+
active.stdoutTail = capTail(`${active.stdoutTail}${chunk}`, PARSE_TAIL_BYTES);
|
|
939
|
+
}
|
|
940
|
+
function appendCappedChunk(current, currentBytes, truncated, chunk, capBytes) {
|
|
941
|
+
if (currentBytes >= capBytes) return {
|
|
942
|
+
value: current,
|
|
943
|
+
bytes: currentBytes,
|
|
944
|
+
truncated: truncated || chunk.length > 0
|
|
945
|
+
};
|
|
946
|
+
const remaining = capBytes - currentBytes;
|
|
947
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf8");
|
|
948
|
+
if (chunkBytes <= remaining) return {
|
|
949
|
+
value: `${current}${chunk}`,
|
|
950
|
+
bytes: currentBytes + chunkBytes,
|
|
951
|
+
truncated
|
|
952
|
+
};
|
|
953
|
+
return {
|
|
954
|
+
value: `${current}${sliceUtf8Bytes(chunk, remaining)}`,
|
|
955
|
+
bytes: capBytes,
|
|
956
|
+
truncated: true
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function finishStdout(active, parsedTailStdout) {
|
|
960
|
+
return finishCapturedOutput((parseCompletedStdout(active.stdout, active.marker)?.stdout ?? active.stdout) || parsedTailStdout, active.stdoutTruncated, active.maxOutputBytes);
|
|
961
|
+
}
|
|
962
|
+
function stdoutOutputChunk(active, chunk) {
|
|
963
|
+
if (active.started) return chunk;
|
|
964
|
+
const nextTail = `${active.stdoutTail}${chunk}`;
|
|
965
|
+
const startPattern = new RegExp(`(^|\\r?\\n)__APP_START_${active.marker}__(\\r?\\n|$)`);
|
|
966
|
+
const match = nextTail.match(startPattern);
|
|
967
|
+
if (match?.index === void 0) return "";
|
|
968
|
+
active.started = true;
|
|
969
|
+
const afterStartIndex = match.index + match[0].length;
|
|
970
|
+
return nextTail.slice(afterStartIndex);
|
|
971
|
+
}
|
|
972
|
+
function finishCapturedOutput(value, truncated, capBytes) {
|
|
973
|
+
return truncated ? `${value}\n[output truncated at ${capBytes} bytes]` : value;
|
|
974
|
+
}
|
|
975
|
+
function capTail(value, capBytes) {
|
|
976
|
+
if (Buffer.byteLength(value, "utf8") <= capBytes) return value;
|
|
977
|
+
return sliceUtf8BytesFromEnd(value, capBytes);
|
|
978
|
+
}
|
|
979
|
+
function sliceUtf8Bytes(value, maxBytes) {
|
|
980
|
+
let bytes = 0;
|
|
981
|
+
let result = "";
|
|
982
|
+
for (const char of value) {
|
|
983
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
984
|
+
if (bytes + charBytes > maxBytes) break;
|
|
985
|
+
bytes += charBytes;
|
|
986
|
+
result += char;
|
|
987
|
+
}
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
function sliceUtf8BytesFromEnd(value, maxBytes) {
|
|
991
|
+
let bytes = 0;
|
|
992
|
+
let result = "";
|
|
993
|
+
const chars = Array.from(value);
|
|
994
|
+
for (let index = chars.length - 1; index >= 0; index -= 1) {
|
|
995
|
+
const char = chars[index];
|
|
996
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
997
|
+
if (bytes + charBytes > maxBytes) break;
|
|
998
|
+
bytes += charBytes;
|
|
999
|
+
result = `${char}${result}`;
|
|
1000
|
+
}
|
|
1001
|
+
return result;
|
|
1002
|
+
}
|
|
1003
|
+
function clampPositiveInteger(value, fallback, maximum) {
|
|
1004
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
1005
|
+
return Math.min(Math.max(Math.trunc(value), 1), maximum);
|
|
1006
|
+
}
|
|
1007
|
+
//#endregion
|
|
1008
|
+
//#region src/machine-client.ts
|
|
1009
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
1010
|
+
var MachineClaimError = class extends Error {
|
|
1011
|
+
code;
|
|
1012
|
+
status;
|
|
1013
|
+
constructor(message, code, status) {
|
|
1014
|
+
super(message);
|
|
1015
|
+
this.code = code;
|
|
1016
|
+
this.status = status;
|
|
1017
|
+
this.name = "MachineClaimError";
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
var MachineClient = class {
|
|
1021
|
+
serverUrl;
|
|
1022
|
+
options;
|
|
1023
|
+
constructor(serverUrl, options = {}) {
|
|
1024
|
+
this.serverUrl = serverUrl;
|
|
1025
|
+
this.options = options;
|
|
1026
|
+
}
|
|
1027
|
+
async claim(input) {
|
|
1028
|
+
allowInsecureTlsForLocalTesting(this.options);
|
|
1029
|
+
const shell = new ShellManager().shellInfo();
|
|
1030
|
+
const response = await fetch(new URL("/api/cli/machines/claim", this.serverUrl), {
|
|
1031
|
+
method: "POST",
|
|
1032
|
+
headers: {
|
|
1033
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
1034
|
+
"content-type": "application/json"
|
|
1035
|
+
},
|
|
1036
|
+
body: JSON.stringify({
|
|
1037
|
+
connector_install_id: input.connectorInstallId,
|
|
1038
|
+
host_label: input.name ?? hostname(),
|
|
1039
|
+
os: shell.os,
|
|
1040
|
+
connector_version: "0.1.0",
|
|
1041
|
+
shell: {
|
|
1042
|
+
defaultName: shell.name,
|
|
1043
|
+
...shell.path ? { defaultPath: shell.path } : {}
|
|
1044
|
+
}
|
|
1045
|
+
}),
|
|
1046
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1047
|
+
});
|
|
1048
|
+
const body = await response.json().catch(() => ({}));
|
|
1049
|
+
if (!response.ok) {
|
|
1050
|
+
const error = readError(body);
|
|
1051
|
+
throw new MachineClaimError(`Machine claim failed: ${error}`, error, response.status);
|
|
1052
|
+
}
|
|
1053
|
+
if (!body || typeof body !== "object" || !("machine" in body)) throw new Error("Machine claim response was invalid.");
|
|
1054
|
+
const machine = body.machine;
|
|
1055
|
+
if (!machine || typeof machine !== "object") throw new Error("Machine claim response was invalid.");
|
|
1056
|
+
const raw = machine;
|
|
1057
|
+
if (typeof raw.machineId !== "string" || typeof raw.name !== "string") throw new Error("Machine claim response was invalid.");
|
|
1058
|
+
return {
|
|
1059
|
+
machineId: raw.machineId,
|
|
1060
|
+
name: raw.name
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
function readError(value) {
|
|
1065
|
+
if (value && typeof value === "object" && typeof value.error === "string") return value.error;
|
|
1066
|
+
return "request_failed";
|
|
1067
|
+
}
|
|
1068
|
+
//#endregion
|
|
1069
|
+
//#region src/connector-profile.ts
|
|
1070
|
+
const CONNECTOR_SERVICE_BASE_NAME = "agents.connector";
|
|
1071
|
+
function resolveConnectorProfileContext(profileName, servicePlatform = platform()) {
|
|
1072
|
+
const parsedProfileName = parseConnectorProfileName(profileName);
|
|
1073
|
+
const serviceName = connectorServiceName(parsedProfileName, servicePlatform);
|
|
1074
|
+
const paths = connectorProfilePaths(parsedProfileName, servicePlatform);
|
|
1075
|
+
return {
|
|
1076
|
+
...parsedProfileName ? { profileName: parsedProfileName } : {},
|
|
1077
|
+
serviceName,
|
|
1078
|
+
serviceUnitName: `${serviceName}.service`,
|
|
1079
|
+
...paths,
|
|
1080
|
+
launchAgentPath: join(homedir(), "Library", "LaunchAgents", `${serviceName}.plist`),
|
|
1081
|
+
systemdUnitPath: join(homedir(), ".config", "systemd", "user", `${serviceName}.service`)
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function parseConnectorProfileName(value) {
|
|
1085
|
+
if (value === void 0) return void 0;
|
|
1086
|
+
const name = value.trim();
|
|
1087
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name)) throw new Error("Connector profile names must use lowercase letters, numbers, and hyphens, and start with a letter or number.");
|
|
1088
|
+
return name;
|
|
1089
|
+
}
|
|
1090
|
+
function defaultConnectorAuthStorePath() {
|
|
1091
|
+
return resolveConnectorProfileContext().authStorePath;
|
|
1092
|
+
}
|
|
1093
|
+
function connectorServiceName(profileName, servicePlatform) {
|
|
1094
|
+
if (!profileName) return CONNECTOR_SERVICE_BASE_NAME;
|
|
1095
|
+
if (servicePlatform === "linux") return `${CONNECTOR_SERVICE_BASE_NAME}@${profileName}`;
|
|
1096
|
+
return `${CONNECTOR_SERVICE_BASE_NAME}.${profileName}`;
|
|
1097
|
+
}
|
|
1098
|
+
function connectorProfilePaths(profileName, servicePlatform) {
|
|
1099
|
+
const configRoot = configDir(servicePlatform);
|
|
1100
|
+
const stateRoot = stateDir(servicePlatform);
|
|
1101
|
+
if (!profileName) return {
|
|
1102
|
+
configPath: join(configRoot, "config.json"),
|
|
1103
|
+
authStorePath: legacyAuthStorePath(servicePlatform),
|
|
1104
|
+
logPath: join(stateRoot, "connector.log"),
|
|
1105
|
+
errorLogPath: join(stateRoot, "connector.err.log")
|
|
1106
|
+
};
|
|
1107
|
+
return {
|
|
1108
|
+
configPath: join(configRoot, "connectors", profileName, "config.json"),
|
|
1109
|
+
authStorePath: join(configRoot, "connectors", profileName, "auth.json"),
|
|
1110
|
+
logPath: join(stateRoot, "connectors", profileName, "connector.log"),
|
|
1111
|
+
errorLogPath: join(stateRoot, "connectors", profileName, "connector.err.log")
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function configDir(servicePlatform) {
|
|
1115
|
+
if (servicePlatform === "darwin") return join(homedir(), "Library", "Application Support", "agents");
|
|
1116
|
+
if (servicePlatform === "win32") return join(process.env.APPDATA ?? homedir(), "agents");
|
|
1117
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "agents");
|
|
1118
|
+
}
|
|
1119
|
+
function legacyAuthStorePath(servicePlatform) {
|
|
1120
|
+
if (servicePlatform === "darwin") return join(homedir(), "Library", "Application Support", "agents", "auth.json");
|
|
1121
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "agents", "auth.json");
|
|
1122
|
+
}
|
|
1123
|
+
function stateDir(servicePlatform) {
|
|
1124
|
+
if (servicePlatform === "darwin") return join(homedir(), "Library", "Logs", "agents");
|
|
1125
|
+
if (servicePlatform === "win32") return join(process.env.LOCALAPPDATA ?? homedir(), "agents");
|
|
1126
|
+
return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "agents");
|
|
1127
|
+
}
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/service-manager.ts
|
|
1130
|
+
var ConnectorServiceUnsupportedError = class extends Error {
|
|
1131
|
+
code = "connector_service_unsupported";
|
|
1132
|
+
constructor() {
|
|
1133
|
+
super("Service commands support macOS LaunchAgent, Linux systemd user services, and Windows Scheduled Tasks.");
|
|
1134
|
+
this.name = "ConnectorServiceUnsupportedError";
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
function isConnectorServiceUnsupportedError(error) {
|
|
1138
|
+
return error instanceof ConnectorServiceUnsupportedError || Boolean(error && typeof error === "object" && error.code === "connector_service_unsupported");
|
|
1139
|
+
}
|
|
1140
|
+
let runCommand = run;
|
|
1141
|
+
async function runServiceCommand(command, config) {
|
|
1142
|
+
await runServiceCommandForPlatform(command, resolveServicePlatform(), config);
|
|
1143
|
+
}
|
|
1144
|
+
async function runServiceCommandForPlatform(command, servicePlatform, config) {
|
|
1145
|
+
const context = resolveConnectorProfileContext(config?.profileName, servicePlatform);
|
|
1146
|
+
if (command === "install") {
|
|
1147
|
+
if (!config?.serverUrl) throw new Error("A server profile is required for service install.");
|
|
1148
|
+
const installed = await installConnectorService({
|
|
1149
|
+
serverUrl: config.serverUrl,
|
|
1150
|
+
...config.allowInsecureTls ? { allowInsecureTls: true } : {},
|
|
1151
|
+
...config.profileName ? { profileName: config.profileName } : {}
|
|
1152
|
+
});
|
|
1153
|
+
console.log(`Installed ${context.serviceName} ${serviceLabel(installed.service)}.`);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (command === "repair") {
|
|
1157
|
+
if (!config?.serverUrl) throw new Error("A server profile is required for service repair.");
|
|
1158
|
+
await repairService(servicePlatform, {
|
|
1159
|
+
serverUrl: config.serverUrl,
|
|
1160
|
+
...config.allowInsecureTls ? { allowInsecureTls: true } : {},
|
|
1161
|
+
authStorePath: context.authStorePath
|
|
1162
|
+
}, {}, context);
|
|
1163
|
+
console.log(`Repaired ${context.serviceName} ${serviceLabel(serviceKind(servicePlatform))}.`);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (command === "uninstall") {
|
|
1167
|
+
await uninstallService(servicePlatform, context);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (command === "start") {
|
|
1171
|
+
await startService(servicePlatform, context);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (command === "stop") {
|
|
1175
|
+
await stopService(servicePlatform, context);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (command === "restart") {
|
|
1179
|
+
await restartService(servicePlatform, context);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (command === "status") {
|
|
1183
|
+
await printServiceStatus(servicePlatform, context);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
await printServiceLogs(servicePlatform, {}, context);
|
|
1187
|
+
}
|
|
1188
|
+
async function installConnectorService(config) {
|
|
1189
|
+
if (!config.serverUrl) throw new Error("A server profile is required for service install.");
|
|
1190
|
+
const servicePlatform = resolveServicePlatform();
|
|
1191
|
+
const context = resolveConnectorProfileContext(config.profileName, servicePlatform);
|
|
1192
|
+
await installService(servicePlatform, {
|
|
1193
|
+
serverUrl: config.serverUrl,
|
|
1194
|
+
...config.allowInsecureTls ? { allowInsecureTls: true } : {},
|
|
1195
|
+
authStorePath: context.authStorePath
|
|
1196
|
+
}, context, config.connectorEntryPath);
|
|
1197
|
+
return {
|
|
1198
|
+
platform: servicePlatform,
|
|
1199
|
+
service: serviceKind(servicePlatform)
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
async function loadServiceConfig(path = resolveConnectorProfileContext().configPath, options = {}) {
|
|
1203
|
+
const raw = await readFile(path, "utf8");
|
|
1204
|
+
const value = JSON.parse(raw);
|
|
1205
|
+
if (typeof value.serverUrl !== "string") throw new Error(`Invalid connector service config at ${path}.`);
|
|
1206
|
+
const defaultContext = resolveConnectorProfileContext();
|
|
1207
|
+
const authStorePath = typeof value.authStorePath === "string" && value.authStorePath.trim() ? value.authStorePath : path === (options.legacyDefaultConfigPath ?? defaultContext.configPath) ? options.defaultAuthStorePath ?? defaultContext.authStorePath : void 0;
|
|
1208
|
+
if (!authStorePath) throw new Error(`Invalid connector service config at ${path}.`);
|
|
1209
|
+
return {
|
|
1210
|
+
serverUrl: value.serverUrl,
|
|
1211
|
+
...value.allowInsecureTls === true ? { allowInsecureTls: true } : {},
|
|
1212
|
+
authStorePath
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async function installService(servicePlatform, config, context = resolveConnectorProfileContext(void 0, servicePlatform), connectorEntryPath) {
|
|
1216
|
+
const command = await resolveConnectorCommand(connectorEntryPath);
|
|
1217
|
+
if (servicePlatform === "darwin") {
|
|
1218
|
+
await installLaunchAgent(command, config, context);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (servicePlatform === "win32") {
|
|
1222
|
+
await installWindowsScheduledTaskService(command, config, {}, context);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
await installSystemdUserService(command, config, {}, context);
|
|
1226
|
+
}
|
|
1227
|
+
async function uninstallService(servicePlatform, context = resolveConnectorProfileContext(void 0, servicePlatform)) {
|
|
1228
|
+
if (servicePlatform === "darwin") {
|
|
1229
|
+
await cleanupLaunchAgent(context.launchAgentPath, context.configPath, context);
|
|
1230
|
+
console.log(`Uninstalled ${context.serviceName} LaunchAgent.`);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (servicePlatform === "win32") {
|
|
1234
|
+
await cleanupWindowsScheduledTask(context.configPath, context);
|
|
1235
|
+
console.log(`Uninstalled ${context.serviceName} Scheduled Task.`);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
await cleanupSystemdUserService(context.systemdUnitPath, context.configPath, context);
|
|
1239
|
+
console.log(`Uninstalled ${context.serviceName} systemd user service.`);
|
|
1240
|
+
}
|
|
1241
|
+
async function repairService(servicePlatform, config, paths = {}, context = resolveConnectorProfileContext(void 0, servicePlatform)) {
|
|
1242
|
+
await writeServiceConfig(withAuthStorePath(config, context), paths.configPath ?? context.configPath);
|
|
1243
|
+
await restartService(servicePlatform, context);
|
|
1244
|
+
}
|
|
1245
|
+
async function startService(servicePlatform, context = resolveConnectorProfileContext(void 0, servicePlatform)) {
|
|
1246
|
+
if (servicePlatform === "darwin") {
|
|
1247
|
+
await runCommand("launchctl", ["enable", `gui/${currentUid()}/${context.serviceName}`]);
|
|
1248
|
+
await runCommand("launchctl", ["kickstart", `gui/${currentUid()}/${context.serviceName}`]);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (servicePlatform === "win32") {
|
|
1252
|
+
await runCommand("schtasks", [
|
|
1253
|
+
"/Run",
|
|
1254
|
+
"/TN",
|
|
1255
|
+
context.serviceName
|
|
1256
|
+
]);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
await runCommand("systemctl", [
|
|
1260
|
+
"--user",
|
|
1261
|
+
"start",
|
|
1262
|
+
context.serviceUnitName
|
|
1263
|
+
]);
|
|
1264
|
+
}
|
|
1265
|
+
async function stopService(servicePlatform, context = resolveConnectorProfileContext(void 0, servicePlatform)) {
|
|
1266
|
+
if (servicePlatform === "darwin") {
|
|
1267
|
+
await runCommand("launchctl", ["disable", `gui/${currentUid()}/${context.serviceName}`]);
|
|
1268
|
+
await runCommand("launchctl", ["stop", `gui/${currentUid()}/${context.serviceName}`]);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (servicePlatform === "win32") {
|
|
1272
|
+
await runCommand("schtasks", [
|
|
1273
|
+
"/End",
|
|
1274
|
+
"/TN",
|
|
1275
|
+
context.serviceName
|
|
1276
|
+
]);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
await runCommand("systemctl", [
|
|
1280
|
+
"--user",
|
|
1281
|
+
"stop",
|
|
1282
|
+
context.serviceUnitName
|
|
1283
|
+
]);
|
|
1284
|
+
}
|
|
1285
|
+
async function restartService(servicePlatform, context = resolveConnectorProfileContext(void 0, servicePlatform)) {
|
|
1286
|
+
if (servicePlatform === "darwin") {
|
|
1287
|
+
await runCommand("launchctl", ["enable", `gui/${currentUid()}/${context.serviceName}`]);
|
|
1288
|
+
await runCommand("launchctl", [
|
|
1289
|
+
"kickstart",
|
|
1290
|
+
"-k",
|
|
1291
|
+
`gui/${currentUid()}/${context.serviceName}`
|
|
1292
|
+
]);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (servicePlatform === "win32") {
|
|
1296
|
+
await runCommand("schtasks", [
|
|
1297
|
+
"/End",
|
|
1298
|
+
"/TN",
|
|
1299
|
+
context.serviceName
|
|
1300
|
+
], { allowFailure: true });
|
|
1301
|
+
await runCommand("schtasks", [
|
|
1302
|
+
"/Run",
|
|
1303
|
+
"/TN",
|
|
1304
|
+
context.serviceName
|
|
1305
|
+
]);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
await runCommand("systemctl", [
|
|
1309
|
+
"--user",
|
|
1310
|
+
"restart",
|
|
1311
|
+
context.serviceUnitName
|
|
1312
|
+
]);
|
|
1313
|
+
}
|
|
1314
|
+
async function printServiceStatus(servicePlatform, context = resolveConnectorProfileContext(void 0, servicePlatform)) {
|
|
1315
|
+
if (servicePlatform === "darwin") {
|
|
1316
|
+
await runCommand("launchctl", ["print", `gui/${currentUid()}/${context.serviceName}`], { allowFailure: true });
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (servicePlatform === "win32") {
|
|
1320
|
+
await runCommand("schtasks", [
|
|
1321
|
+
"/Query",
|
|
1322
|
+
"/TN",
|
|
1323
|
+
context.serviceName,
|
|
1324
|
+
"/V",
|
|
1325
|
+
"/FO",
|
|
1326
|
+
"LIST"
|
|
1327
|
+
], { allowFailure: true });
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
await runCommand("systemctl", [
|
|
1331
|
+
"--user",
|
|
1332
|
+
"status",
|
|
1333
|
+
context.serviceUnitName
|
|
1334
|
+
], { allowFailure: true });
|
|
1335
|
+
}
|
|
1336
|
+
async function printServiceLogs(servicePlatform, paths = {}, context = resolveConnectorProfileContext(void 0, servicePlatform)) {
|
|
1337
|
+
if (servicePlatform === "darwin") {
|
|
1338
|
+
await printServiceLogFiles(context, paths);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (servicePlatform === "win32") {
|
|
1342
|
+
await printServiceLogFiles(context, paths);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (servicePlatform === "linux") {
|
|
1346
|
+
await runCommand("journalctl", [
|
|
1347
|
+
"--user",
|
|
1348
|
+
"-u",
|
|
1349
|
+
context.serviceUnitName,
|
|
1350
|
+
"-n",
|
|
1351
|
+
"100",
|
|
1352
|
+
"--no-pager"
|
|
1353
|
+
], { allowFailure: true });
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
async function printServiceLogFiles(context = resolveConnectorProfileContext(), paths = {}) {
|
|
1358
|
+
await printFile(paths.logPath ?? context.logPath);
|
|
1359
|
+
await printFile(paths.errorLogPath ?? context.errorLogPath);
|
|
1360
|
+
}
|
|
1361
|
+
async function writeServiceConfig(config, path = resolveConnectorProfileContext().configPath) {
|
|
1362
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1363
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
1364
|
+
await chmod(path, 384).catch(() => void 0);
|
|
1365
|
+
}
|
|
1366
|
+
async function removeServiceConfig(path = resolveConnectorProfileContext().configPath) {
|
|
1367
|
+
await rm(path, { force: true });
|
|
1368
|
+
}
|
|
1369
|
+
function withAuthStorePath(config, context) {
|
|
1370
|
+
return {
|
|
1371
|
+
...config,
|
|
1372
|
+
authStorePath: config.authStorePath ?? context.authStorePath
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
async function removeInstallArtifacts(unitPath, configPath = resolveConnectorProfileContext().configPath) {
|
|
1376
|
+
await Promise.all([rm(unitPath, { force: true }), removeServiceConfig(configPath)]);
|
|
1377
|
+
}
|
|
1378
|
+
async function installLaunchAgent(command, config, context = resolveConnectorProfileContext(void 0, "darwin")) {
|
|
1379
|
+
const plistPath = context.launchAgentPath;
|
|
1380
|
+
try {
|
|
1381
|
+
await mkdir(dirname(context.logPath), { recursive: true });
|
|
1382
|
+
await mkdir(dirname(plistPath), { recursive: true });
|
|
1383
|
+
await writeFile(plistPath, launchAgentPlist(command, {}, context), { mode: 420 });
|
|
1384
|
+
await writeServiceConfig(withAuthStorePath(config, context), context.configPath);
|
|
1385
|
+
await runCommand("launchctl", [
|
|
1386
|
+
"bootout",
|
|
1387
|
+
`gui/${currentUid()}`,
|
|
1388
|
+
plistPath
|
|
1389
|
+
], {
|
|
1390
|
+
allowFailure: true,
|
|
1391
|
+
quietFailure: true
|
|
1392
|
+
});
|
|
1393
|
+
await runCommand("launchctl", [
|
|
1394
|
+
"bootstrap",
|
|
1395
|
+
`gui/${currentUid()}`,
|
|
1396
|
+
plistPath
|
|
1397
|
+
]);
|
|
1398
|
+
await runCommand("launchctl", ["enable", `gui/${currentUid()}/${context.serviceName}`], { allowFailure: true });
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
await cleanupLaunchAgent(plistPath, context.configPath, context);
|
|
1401
|
+
throw error;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function installSystemdUserService(command, config, paths = {}, context = resolveConnectorProfileContext(void 0, "linux")) {
|
|
1405
|
+
const unitPath = paths.unitPath ?? context.systemdUnitPath;
|
|
1406
|
+
const configPath = paths.configPath ?? context.configPath;
|
|
1407
|
+
try {
|
|
1408
|
+
await mkdir(dirname(unitPath), { recursive: true });
|
|
1409
|
+
await writeFile(unitPath, systemdUnit(command, { configPath }), { mode: 420 });
|
|
1410
|
+
await writeServiceConfig(withAuthStorePath(config, context), configPath);
|
|
1411
|
+
await runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
1412
|
+
await runCommand("systemctl", [
|
|
1413
|
+
"--user",
|
|
1414
|
+
"enable",
|
|
1415
|
+
context.serviceUnitName
|
|
1416
|
+
]);
|
|
1417
|
+
await runCommand("systemctl", [
|
|
1418
|
+
"--user",
|
|
1419
|
+
"restart",
|
|
1420
|
+
context.serviceUnitName
|
|
1421
|
+
]);
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
await cleanupSystemdUserService(unitPath, configPath, context);
|
|
1424
|
+
throw error;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
async function installWindowsScheduledTaskService(command, config, paths = {}, context = resolveConnectorProfileContext(void 0, "win32")) {
|
|
1428
|
+
const configPath = paths.configPath ?? context.configPath;
|
|
1429
|
+
const logPath = paths.logPath ?? context.logPath;
|
|
1430
|
+
const errorLogPath = paths.errorLogPath ?? context.errorLogPath;
|
|
1431
|
+
try {
|
|
1432
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
1433
|
+
await mkdir(dirname(errorLogPath), { recursive: true });
|
|
1434
|
+
await writeServiceConfig(withAuthStorePath(config, context), configPath);
|
|
1435
|
+
await runCommand("schtasks", [
|
|
1436
|
+
"/Create",
|
|
1437
|
+
"/F",
|
|
1438
|
+
"/SC",
|
|
1439
|
+
"ONLOGON",
|
|
1440
|
+
"/TN",
|
|
1441
|
+
context.serviceName,
|
|
1442
|
+
"/TR",
|
|
1443
|
+
windowsScheduledTaskAction(command, configPath, logPath, errorLogPath)
|
|
1444
|
+
]);
|
|
1445
|
+
await runCommand("schtasks", [
|
|
1446
|
+
"/Run",
|
|
1447
|
+
"/TN",
|
|
1448
|
+
context.serviceName
|
|
1449
|
+
]);
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
await cleanupWindowsScheduledTask(configPath, context);
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function cleanupLaunchAgent(plistPath = resolveConnectorProfileContext(void 0, "darwin").launchAgentPath, configPath = resolveConnectorProfileContext(void 0, "darwin").configPath, context = resolveConnectorProfileContext(void 0, "darwin")) {
|
|
1456
|
+
await runCommand("launchctl", [
|
|
1457
|
+
"bootout",
|
|
1458
|
+
`gui/${currentUid()}`,
|
|
1459
|
+
plistPath
|
|
1460
|
+
], {
|
|
1461
|
+
allowFailure: true,
|
|
1462
|
+
quietFailure: true
|
|
1463
|
+
});
|
|
1464
|
+
await removeInstallArtifacts(plistPath, configPath);
|
|
1465
|
+
}
|
|
1466
|
+
async function cleanupSystemdUserService(unitPath = resolveConnectorProfileContext(void 0, "linux").systemdUnitPath, configPath = resolveConnectorProfileContext(void 0, "linux").configPath, context = resolveConnectorProfileContext(void 0, "linux")) {
|
|
1467
|
+
await runCommand("systemctl", [
|
|
1468
|
+
"--user",
|
|
1469
|
+
"disable",
|
|
1470
|
+
"--now",
|
|
1471
|
+
context.serviceUnitName
|
|
1472
|
+
], { allowFailure: true });
|
|
1473
|
+
await removeInstallArtifacts(unitPath, configPath);
|
|
1474
|
+
await runCommand("systemctl", ["--user", "daemon-reload"], { allowFailure: true });
|
|
1475
|
+
}
|
|
1476
|
+
async function cleanupWindowsScheduledTask(configPath = resolveConnectorProfileContext(void 0, "win32").configPath, context = resolveConnectorProfileContext(void 0, "win32")) {
|
|
1477
|
+
await runCommand("schtasks", [
|
|
1478
|
+
"/End",
|
|
1479
|
+
"/TN",
|
|
1480
|
+
context.serviceName
|
|
1481
|
+
], {
|
|
1482
|
+
allowFailure: true,
|
|
1483
|
+
quietFailure: true
|
|
1484
|
+
});
|
|
1485
|
+
await runCommand("schtasks", [
|
|
1486
|
+
"/Delete",
|
|
1487
|
+
"/TN",
|
|
1488
|
+
context.serviceName,
|
|
1489
|
+
"/F"
|
|
1490
|
+
], {
|
|
1491
|
+
allowFailure: true,
|
|
1492
|
+
quietFailure: true
|
|
1493
|
+
});
|
|
1494
|
+
await removeServiceConfig(configPath);
|
|
1495
|
+
}
|
|
1496
|
+
function launchAgentPlist(command, paths = {}, context = resolveConnectorProfileContext(void 0, "darwin")) {
|
|
1497
|
+
const configPath = paths.configPath ?? context.configPath;
|
|
1498
|
+
const logPath = paths.logPath ?? context.logPath;
|
|
1499
|
+
const errorLogPath = paths.errorLogPath ?? context.errorLogPath;
|
|
1500
|
+
const programArguments = [
|
|
1501
|
+
command.executable,
|
|
1502
|
+
...command.args,
|
|
1503
|
+
"--config",
|
|
1504
|
+
configPath
|
|
1505
|
+
].map((value) => ` <string>${xmlEscape(value)}</string>`).join("\n");
|
|
1506
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1507
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1508
|
+
<plist version="1.0">
|
|
1509
|
+
<dict>
|
|
1510
|
+
<key>Label</key><string>${context.serviceName}</string>
|
|
1511
|
+
<key>ProgramArguments</key>
|
|
1512
|
+
<array>
|
|
1513
|
+
${programArguments}
|
|
1514
|
+
</array>
|
|
1515
|
+
<key>RunAtLoad</key><true/>
|
|
1516
|
+
<key>KeepAlive</key><true/>
|
|
1517
|
+
<key>StandardOutPath</key><string>${xmlEscape(logPath)}</string>
|
|
1518
|
+
<key>StandardErrorPath</key><string>${xmlEscape(errorLogPath)}</string>
|
|
1519
|
+
</dict>
|
|
1520
|
+
</plist>
|
|
1521
|
+
`;
|
|
1522
|
+
}
|
|
1523
|
+
function systemdUnit(command, paths = {}) {
|
|
1524
|
+
const configPath = paths.configPath ?? resolveConnectorProfileContext(void 0, "linux").configPath;
|
|
1525
|
+
return `[Unit]
|
|
1526
|
+
Description=agent connector
|
|
1527
|
+
|
|
1528
|
+
[Service]
|
|
1529
|
+
ExecStart=${[
|
|
1530
|
+
command.executable,
|
|
1531
|
+
...command.args,
|
|
1532
|
+
"--config",
|
|
1533
|
+
configPath
|
|
1534
|
+
].map(systemdQuote).join(" ")}
|
|
1535
|
+
Restart=always
|
|
1536
|
+
RestartSec=5
|
|
1537
|
+
|
|
1538
|
+
[Install]
|
|
1539
|
+
WantedBy=default.target
|
|
1540
|
+
`;
|
|
1541
|
+
}
|
|
1542
|
+
function windowsScheduledTaskAction(command, configPath = resolveConnectorProfileContext(void 0, "win32").configPath, logPath = resolveConnectorProfileContext(void 0, "win32").logPath, errorLogPath = resolveConnectorProfileContext(void 0, "win32").errorLogPath) {
|
|
1543
|
+
return `cmd.exe /d /c "${[
|
|
1544
|
+
command.executable,
|
|
1545
|
+
...command.args,
|
|
1546
|
+
"--config",
|
|
1547
|
+
configPath
|
|
1548
|
+
].map(windowsCmdQuote).join(" ")} >> ${windowsCmdQuote(logPath)} 2>> ${windowsCmdQuote(errorLogPath)}"`;
|
|
1549
|
+
}
|
|
1550
|
+
function resolveServicePlatform() {
|
|
1551
|
+
const current = platform();
|
|
1552
|
+
if (current === "darwin" || current === "linux" || current === "win32") return current;
|
|
1553
|
+
throw new ConnectorServiceUnsupportedError();
|
|
1554
|
+
}
|
|
1555
|
+
function serviceKind(servicePlatform) {
|
|
1556
|
+
if (servicePlatform === "darwin") return "launch-agent";
|
|
1557
|
+
if (servicePlatform === "win32") return "windows-scheduled-task";
|
|
1558
|
+
return "systemd-user";
|
|
1559
|
+
}
|
|
1560
|
+
function serviceLabel(service) {
|
|
1561
|
+
if (service === "launch-agent") return "LaunchAgent";
|
|
1562
|
+
if (service === "windows-scheduled-task") return "Scheduled Task";
|
|
1563
|
+
return "systemd user service";
|
|
1564
|
+
}
|
|
1565
|
+
async function resolveConnectorCommand(connectorEntryPath) {
|
|
1566
|
+
const bin = connectorEntryPath ?? connectorBin();
|
|
1567
|
+
const executable = await nodeExecutable();
|
|
1568
|
+
await access(executable);
|
|
1569
|
+
await access(bin);
|
|
1570
|
+
return {
|
|
1571
|
+
executable,
|
|
1572
|
+
args: [bin]
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
function connectorBin() {
|
|
1576
|
+
const current = fileURLToPath(import.meta.url);
|
|
1577
|
+
const currentDir = dirname(current);
|
|
1578
|
+
if (currentDir.endsWith(`${sep}dist`)) return join(currentDir, "index.js");
|
|
1579
|
+
return join(dirname(dirname(current)), "bin", "agent-connector.mjs");
|
|
1580
|
+
}
|
|
1581
|
+
async function nodeExecutable() {
|
|
1582
|
+
if (!isElectronExecutable(process.execPath)) return process.execPath;
|
|
1583
|
+
const fromPath = await findNodeOnPath();
|
|
1584
|
+
if (fromPath) return fromPath;
|
|
1585
|
+
throw new Error("Connector service install from desktop requires a Node.js executable on PATH.");
|
|
1586
|
+
}
|
|
1587
|
+
function isElectronExecutable(value) {
|
|
1588
|
+
return Boolean(process.versions.electron) || value.endsWith(`${sep}Electron`);
|
|
1589
|
+
}
|
|
1590
|
+
async function findNodeOnPath() {
|
|
1591
|
+
const names = platform() === "win32" ? ["node.exe", "node"] : ["node"];
|
|
1592
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
1593
|
+
if (!dir) continue;
|
|
1594
|
+
for (const name of names) {
|
|
1595
|
+
const candidate = join(dir, name);
|
|
1596
|
+
if (await exists(candidate)) return candidate;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
async function exists(path) {
|
|
1601
|
+
try {
|
|
1602
|
+
await access(path);
|
|
1603
|
+
return true;
|
|
1604
|
+
} catch {
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function currentUid() {
|
|
1609
|
+
if (!process.getuid) throw new Error("launchctl service management requires a user id.");
|
|
1610
|
+
return String(process.getuid());
|
|
1611
|
+
}
|
|
1612
|
+
function xmlEscape(value) {
|
|
1613
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1614
|
+
}
|
|
1615
|
+
function systemdQuote(value) {
|
|
1616
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
1617
|
+
}
|
|
1618
|
+
function windowsCmdQuote(value) {
|
|
1619
|
+
if (value.includes("\"")) throw new Error("Windows Scheduled Task command values cannot contain double quotes.");
|
|
1620
|
+
return `"${value}"`;
|
|
1621
|
+
}
|
|
1622
|
+
async function printFile(path) {
|
|
1623
|
+
try {
|
|
1624
|
+
await access(path);
|
|
1625
|
+
console.log(await readFile(path, "utf8"));
|
|
1626
|
+
} catch {
|
|
1627
|
+
console.log(`${path} does not exist yet.`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async function run(command, args, options = {}) {
|
|
1631
|
+
const exitCode = await new Promise((resolveExit) => {
|
|
1632
|
+
const child = spawn(command, args, { stdio: options.quietFailure ? "ignore" : "inherit" });
|
|
1633
|
+
child.once("error", () => resolveExit(127));
|
|
1634
|
+
child.once("exit", (code) => resolveExit(code ?? 1));
|
|
1635
|
+
});
|
|
1636
|
+
if (exitCode !== 0 && !options.allowFailure) throw new Error(`${command} ${args.join(" ")} failed with exit code ${exitCode}.`);
|
|
1637
|
+
}
|
|
1638
|
+
const TOKEN_STORE_LOCK_WAIT_MS = 45e3;
|
|
1639
|
+
var TokenStore = class {
|
|
1640
|
+
path;
|
|
1641
|
+
constructor(path = defaultTokenStorePath()) {
|
|
1642
|
+
this.path = path;
|
|
1643
|
+
}
|
|
1644
|
+
async read() {
|
|
1645
|
+
return this.readData();
|
|
1646
|
+
}
|
|
1647
|
+
async resolveServerUrl(explicitServerUrl) {
|
|
1648
|
+
if (explicitServerUrl) return normalizeServerUrl(explicitServerUrl);
|
|
1649
|
+
return (await this.readData()).activeServerUrl ?? "https://devbox.sipher.gg";
|
|
1650
|
+
}
|
|
1651
|
+
async getProfile(serverUrl) {
|
|
1652
|
+
return (await this.readData()).profiles[normalizeServerUrl(serverUrl)];
|
|
1653
|
+
}
|
|
1654
|
+
async saveLogin(input) {
|
|
1655
|
+
const serverUrl = normalizeServerUrl(input.serverUrl);
|
|
1656
|
+
return this.withLockedData((data) => {
|
|
1657
|
+
const existing = data.profiles[serverUrl];
|
|
1658
|
+
const profile = {
|
|
1659
|
+
serverUrl,
|
|
1660
|
+
connectorInstallId: existing?.connectorInstallId ?? randomUUID(),
|
|
1661
|
+
...input.allowInsecureTls ? { allowInsecureTls: true } : {},
|
|
1662
|
+
refreshToken: input.refreshToken,
|
|
1663
|
+
accessToken: input.accessToken,
|
|
1664
|
+
accessTokenExpiresAt: expiresAt$1(input.expiresInSeconds, input.now),
|
|
1665
|
+
...existing?.machineId ? { machineId: existing.machineId } : {}
|
|
1666
|
+
};
|
|
1667
|
+
data.activeServerUrl = serverUrl;
|
|
1668
|
+
data.profiles[serverUrl] = profile;
|
|
1669
|
+
return profile;
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
async updateProfile(serverUrl, update) {
|
|
1673
|
+
const normalized = normalizeServerUrl(serverUrl);
|
|
1674
|
+
return this.withLockedData((data) => {
|
|
1675
|
+
const profile = update(data.profiles[normalized]);
|
|
1676
|
+
data.profiles[normalized] = profile;
|
|
1677
|
+
return profile;
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
async removeProfile(serverUrl) {
|
|
1681
|
+
const normalized = normalizeServerUrl(serverUrl);
|
|
1682
|
+
return this.withLockedData((data) => {
|
|
1683
|
+
const profile = data.profiles[normalized];
|
|
1684
|
+
delete data.profiles[normalized];
|
|
1685
|
+
if (data.activeServerUrl === normalized) {
|
|
1686
|
+
const [nextActive] = Object.keys(data.profiles);
|
|
1687
|
+
if (nextActive) data.activeServerUrl = nextActive;
|
|
1688
|
+
else delete data.activeServerUrl;
|
|
1689
|
+
}
|
|
1690
|
+
return profile;
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
async withLockedData(callback) {
|
|
1694
|
+
const lockPath = `${this.path}.lock`;
|
|
1695
|
+
const owner = await acquireLock(lockPath);
|
|
1696
|
+
try {
|
|
1697
|
+
const data = await this.readData();
|
|
1698
|
+
const result = await callback(data);
|
|
1699
|
+
await this.writeData(data);
|
|
1700
|
+
return result;
|
|
1701
|
+
} finally {
|
|
1702
|
+
await releaseLock(lockPath, owner);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
async readData() {
|
|
1706
|
+
try {
|
|
1707
|
+
const parsed = JSON.parse(await readFile(this.path, "utf8"));
|
|
1708
|
+
if (!parsed || typeof parsed !== "object") return emptyData();
|
|
1709
|
+
const raw = parsed;
|
|
1710
|
+
const profiles = {};
|
|
1711
|
+
if (raw.profiles && typeof raw.profiles === "object") for (const [key, value] of Object.entries(raw.profiles)) {
|
|
1712
|
+
const profile = parseProfile(value);
|
|
1713
|
+
if (profile) profiles[normalizeServerUrl(key)] = profile;
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
profiles,
|
|
1717
|
+
...typeof raw.activeServerUrl === "string" ? { activeServerUrl: normalizeServerUrl(raw.activeServerUrl) } : {}
|
|
1718
|
+
};
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
if (!isNotFoundError(error)) throw error;
|
|
1721
|
+
return emptyData();
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
async writeData(data) {
|
|
1725
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
1726
|
+
const tempPath = `${this.path}.${process.pid}.${Date.now()}.tmp`;
|
|
1727
|
+
await writeFile(tempPath, `${JSON.stringify(data, null, 2)}\n`, { mode: 384 });
|
|
1728
|
+
await chmod(tempPath, 384).catch(() => void 0);
|
|
1729
|
+
await rename(tempPath, this.path);
|
|
1730
|
+
await chmod(this.path, 384).catch(() => void 0);
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
function normalizeServerUrl(value) {
|
|
1734
|
+
const url = new URL(value);
|
|
1735
|
+
url.hash = "";
|
|
1736
|
+
url.search = "";
|
|
1737
|
+
url.pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
1738
|
+
return url.toString().replace(/\/$/, "");
|
|
1739
|
+
}
|
|
1740
|
+
function defaultTokenStorePath() {
|
|
1741
|
+
return defaultConnectorAuthStorePath();
|
|
1742
|
+
}
|
|
1743
|
+
function emptyData() {
|
|
1744
|
+
return { profiles: {} };
|
|
1745
|
+
}
|
|
1746
|
+
function expiresAt$1(expiresInSeconds, now = /* @__PURE__ */ new Date()) {
|
|
1747
|
+
return new Date(now.getTime() + Math.max(0, expiresInSeconds) * 1e3).toISOString();
|
|
1748
|
+
}
|
|
1749
|
+
function parseProfile(value) {
|
|
1750
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1751
|
+
const raw = value;
|
|
1752
|
+
if (typeof raw.serverUrl !== "string" || typeof raw.connectorInstallId !== "string") return;
|
|
1753
|
+
return {
|
|
1754
|
+
serverUrl: normalizeServerUrl(raw.serverUrl),
|
|
1755
|
+
connectorInstallId: raw.connectorInstallId,
|
|
1756
|
+
...raw.allowInsecureTls === true ? { allowInsecureTls: true } : {},
|
|
1757
|
+
...typeof raw.refreshToken === "string" ? { refreshToken: raw.refreshToken } : {},
|
|
1758
|
+
...typeof raw.accessToken === "string" ? { accessToken: raw.accessToken } : {},
|
|
1759
|
+
...typeof raw.accessTokenExpiresAt === "string" ? { accessTokenExpiresAt: raw.accessTokenExpiresAt } : {},
|
|
1760
|
+
...typeof raw.machineId === "string" ? { machineId: raw.machineId } : {}
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
async function acquireLock(lockPath) {
|
|
1764
|
+
const started = Date.now();
|
|
1765
|
+
let attempts = 0;
|
|
1766
|
+
debugLock("acquire:start", {
|
|
1767
|
+
lockPath,
|
|
1768
|
+
waitMs: TOKEN_STORE_LOCK_WAIT_MS
|
|
1769
|
+
});
|
|
1770
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
1771
|
+
while (true) {
|
|
1772
|
+
attempts += 1;
|
|
1773
|
+
const owner = {
|
|
1774
|
+
token: randomUUID(),
|
|
1775
|
+
pid: process.pid,
|
|
1776
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1777
|
+
};
|
|
1778
|
+
try {
|
|
1779
|
+
await mkdir(lockPath, { recursive: false });
|
|
1780
|
+
await writeLockOwner(lockPath, owner);
|
|
1781
|
+
debugLock("acquire:success", {
|
|
1782
|
+
lockPath,
|
|
1783
|
+
attempts,
|
|
1784
|
+
owner
|
|
1785
|
+
});
|
|
1786
|
+
return owner;
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
if (!isAlreadyExistsError(error) || Date.now() - started > 45e3) {
|
|
1789
|
+
const currentOwner = await readLockOwner(lockPath).catch((readError) => ({ readError: errorMessage(readError) }));
|
|
1790
|
+
debugLock("acquire:timeout", {
|
|
1791
|
+
lockPath,
|
|
1792
|
+
attempts,
|
|
1793
|
+
elapsedMs: Date.now() - started,
|
|
1794
|
+
error: errorMessage(error),
|
|
1795
|
+
currentOwner
|
|
1796
|
+
});
|
|
1797
|
+
throw new Error("Timed out waiting for CLI token store lock.");
|
|
1798
|
+
}
|
|
1799
|
+
if (attempts === 1 || attempts % 40 === 0) {
|
|
1800
|
+
const currentOwner = await readLockOwner(lockPath).catch((readError) => ({ readError: errorMessage(readError) }));
|
|
1801
|
+
debugLock("acquire:waiting", {
|
|
1802
|
+
lockPath,
|
|
1803
|
+
attempts,
|
|
1804
|
+
elapsedMs: Date.now() - started,
|
|
1805
|
+
currentOwner
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
await removeStaleLock(lockPath);
|
|
1809
|
+
await setTimeout$1(25);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function isAlreadyExistsError(error) {
|
|
1814
|
+
return Boolean(error && typeof error === "object" && error.code === "EEXIST");
|
|
1815
|
+
}
|
|
1816
|
+
function isNotFoundError(error) {
|
|
1817
|
+
return Boolean(error && typeof error === "object" && error.code === "ENOENT");
|
|
1818
|
+
}
|
|
1819
|
+
async function removeStaleLock(lockPath, beforeConfirm) {
|
|
1820
|
+
const owner = await readLockOwner(lockPath);
|
|
1821
|
+
const stale = owner ? lockOwnerStaleReason(owner) : void 0;
|
|
1822
|
+
debugLock("stale:check", {
|
|
1823
|
+
lockPath,
|
|
1824
|
+
owner,
|
|
1825
|
+
stale
|
|
1826
|
+
});
|
|
1827
|
+
if (!owner || !stale) return;
|
|
1828
|
+
await beforeConfirm?.();
|
|
1829
|
+
const confirmedOwner = await readLockOwner(lockPath);
|
|
1830
|
+
const confirmedStale = confirmedOwner ? lockOwnerStaleReason(confirmedOwner) : void 0;
|
|
1831
|
+
if (!confirmedOwner || confirmedOwner.token !== owner.token || !confirmedStale) {
|
|
1832
|
+
debugLock("stale:skip-remove", {
|
|
1833
|
+
lockPath,
|
|
1834
|
+
owner,
|
|
1835
|
+
confirmedOwner,
|
|
1836
|
+
confirmedStale
|
|
1837
|
+
});
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
await rm(ownerPath(lockPath, owner), { force: true });
|
|
1841
|
+
await rmdir(lockPath).catch(() => void 0);
|
|
1842
|
+
debugLock("stale:removed", {
|
|
1843
|
+
lockPath,
|
|
1844
|
+
owner,
|
|
1845
|
+
stale: confirmedStale
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
async function releaseLock(lockPath, owner, beforeRemove) {
|
|
1849
|
+
await beforeRemove?.();
|
|
1850
|
+
await rm(ownerPath(lockPath, owner), { force: true });
|
|
1851
|
+
await rmdir(lockPath).catch(() => void 0);
|
|
1852
|
+
}
|
|
1853
|
+
async function writeLockOwner(lockPath, owner) {
|
|
1854
|
+
await writeFile(ownerPath(lockPath, owner), `${JSON.stringify(owner)}\n`, { mode: 384 });
|
|
1855
|
+
}
|
|
1856
|
+
async function readLockOwner(lockPath) {
|
|
1857
|
+
const token = await readActiveOwnerToken(lockPath);
|
|
1858
|
+
if (token) return readOwnerFile(join(lockPath, `owner-${token}.json`));
|
|
1859
|
+
const legacyOwner = await readOwnerFile(join(lockPath, "owner.json")).catch((error) => {
|
|
1860
|
+
if (isNotFoundError(error)) return void 0;
|
|
1861
|
+
throw error;
|
|
1862
|
+
});
|
|
1863
|
+
if (legacyOwner) return {
|
|
1864
|
+
...legacyOwner,
|
|
1865
|
+
token: `legacy-owner-json:${legacyOwner.token}`
|
|
1866
|
+
};
|
|
1867
|
+
return fallbackLockOwner(lockPath);
|
|
1868
|
+
}
|
|
1869
|
+
async function readActiveOwnerToken(lockPath) {
|
|
1870
|
+
if (!await stat(lockPath).catch(() => void 0)) return void 0;
|
|
1871
|
+
try {
|
|
1872
|
+
const owners = (await readdir(lockPath)).filter((name) => name.startsWith("owner-") && name.endsWith(".json"));
|
|
1873
|
+
return owners.length === 1 ? owners[0]?.slice(6, -5) : void 0;
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
if (isNotFoundError(error)) return void 0;
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
async function readOwnerFile(path) {
|
|
1880
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
1881
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1882
|
+
const raw = value;
|
|
1883
|
+
if (typeof raw.token !== "string" || typeof raw.pid !== "number" || typeof raw.createdAt !== "string") return;
|
|
1884
|
+
return {
|
|
1885
|
+
token: raw.token,
|
|
1886
|
+
pid: raw.pid,
|
|
1887
|
+
createdAt: raw.createdAt
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
async function fallbackLockOwner(lockPath) {
|
|
1891
|
+
const stats = await stat(lockPath).catch(() => void 0);
|
|
1892
|
+
if (!stats) return void 0;
|
|
1893
|
+
return {
|
|
1894
|
+
token: `legacy:${stats.mtimeMs}`,
|
|
1895
|
+
pid: 0,
|
|
1896
|
+
createdAt: new Date(stats.mtimeMs).toISOString()
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
function lockOwnerStaleReason(owner) {
|
|
1900
|
+
const createdAt = new Date(owner.createdAt).getTime();
|
|
1901
|
+
if (!Number.isFinite(createdAt)) return "invalid-created-at";
|
|
1902
|
+
if (Date.now() - createdAt >= 3e5) return "expired";
|
|
1903
|
+
if (lockOwnerProcessIsGone(owner.pid)) return "owner-process-exited";
|
|
1904
|
+
}
|
|
1905
|
+
function lockOwnerProcessIsGone(pid) {
|
|
1906
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
|
|
1907
|
+
try {
|
|
1908
|
+
process.kill(pid, 0);
|
|
1909
|
+
return false;
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
return error.code !== "EPERM";
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function debugLock(event, payload) {
|
|
1915
|
+
if (process.env.AGENTS_TOKEN_STORE_DEBUG !== "1") return;
|
|
1916
|
+
console.error(`[agents token-store] ${event} ${JSON.stringify(payload)}`);
|
|
1917
|
+
}
|
|
1918
|
+
function errorMessage(error) {
|
|
1919
|
+
return error instanceof Error ? error.message : String(error);
|
|
1920
|
+
}
|
|
1921
|
+
function ownerPath(lockPath, owner) {
|
|
1922
|
+
if (owner.token.startsWith("legacy-owner-json:")) return join(lockPath, "owner.json");
|
|
1923
|
+
return join(lockPath, `owner-${owner.token}.json`);
|
|
1924
|
+
}
|
|
1925
|
+
//#endregion
|
|
1926
|
+
//#region src/local-machine-controller.ts
|
|
1927
|
+
const ACCESS_TOKEN_REFRESH_SKEW_MS = 3e4;
|
|
1928
|
+
async function ensureLocalMachineReady(options) {
|
|
1929
|
+
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
1930
|
+
if (!(await options.store.getProfile(serverUrl))?.refreshToken) return {
|
|
1931
|
+
state: "needs_login",
|
|
1932
|
+
serverUrl,
|
|
1933
|
+
message: `Not logged in to ${serverUrl}. Run agents login.`
|
|
1934
|
+
};
|
|
1935
|
+
let machine;
|
|
1936
|
+
let allowInsecureTls;
|
|
1937
|
+
try {
|
|
1938
|
+
const result = await claimLocalMachine({
|
|
1939
|
+
serverUrl,
|
|
1940
|
+
store: options.store,
|
|
1941
|
+
name: options.name,
|
|
1942
|
+
allowInsecureTls: options.allowInsecureTls
|
|
1943
|
+
});
|
|
1944
|
+
machine = result.machine;
|
|
1945
|
+
allowInsecureTls = result.allowInsecureTls;
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
const claimError = classifyClaimError(error);
|
|
1948
|
+
return {
|
|
1949
|
+
state: "error",
|
|
1950
|
+
code: isRefreshError(error) ? "refresh_failed" : claimError.code,
|
|
1951
|
+
serverUrl,
|
|
1952
|
+
message: error instanceof Error ? error.message : "Failed to prepare local Machine.",
|
|
1953
|
+
canRetry: !isRefreshError(error) && claimError.canRetry
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
if (options.installService) {
|
|
1957
|
+
if (!options.serviceInstaller) return {
|
|
1958
|
+
state: "error",
|
|
1959
|
+
code: "service_unsupported",
|
|
1960
|
+
serverUrl,
|
|
1961
|
+
message: "Connector service install is not available in this runtime.",
|
|
1962
|
+
canRetry: false
|
|
1963
|
+
};
|
|
1964
|
+
try {
|
|
1965
|
+
await options.serviceInstaller({
|
|
1966
|
+
serverUrl,
|
|
1967
|
+
...allowInsecureTls ? { allowInsecureTls: true } : {}
|
|
1968
|
+
});
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
if (isConnectorServiceUnsupportedError(error)) return {
|
|
1971
|
+
state: "error",
|
|
1972
|
+
code: "service_unsupported",
|
|
1973
|
+
serverUrl,
|
|
1974
|
+
message: error.message,
|
|
1975
|
+
canRetry: false
|
|
1976
|
+
};
|
|
1977
|
+
return {
|
|
1978
|
+
state: "error",
|
|
1979
|
+
code: "service_install_failed",
|
|
1980
|
+
serverUrl,
|
|
1981
|
+
message: error instanceof Error ? error.message : "Failed to install connector service.",
|
|
1982
|
+
canRetry: true
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
state: "ready",
|
|
1988
|
+
serverUrl,
|
|
1989
|
+
machineId: machine.machineId,
|
|
1990
|
+
machineName: machine.name
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
async function setupConnectorProfile(options) {
|
|
1994
|
+
const { machine, allowInsecureTls, serverUrl } = await claimLocalMachine({
|
|
1995
|
+
serverUrl: options.serverUrl,
|
|
1996
|
+
store: options.store,
|
|
1997
|
+
name: options.name,
|
|
1998
|
+
allowInsecureTls: options.allowInsecureTls
|
|
1999
|
+
});
|
|
2000
|
+
if (options.installService) {
|
|
2001
|
+
if (!options.serviceInstaller) throw new Error("Connector service install is not available in this runtime.");
|
|
2002
|
+
await options.serviceInstaller({
|
|
2003
|
+
serverUrl,
|
|
2004
|
+
...allowInsecureTls ? { allowInsecureTls: true } : {}
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
return machine;
|
|
2008
|
+
}
|
|
2009
|
+
async function startConnectorDeviceAuthorization(options) {
|
|
2010
|
+
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
2011
|
+
return {
|
|
2012
|
+
...await new CliAuthClient(serverUrl, { allowInsecureTls: options.allowInsecureTls }).startDeviceAuthorization(options.deviceLabel ?? hostname()),
|
|
2013
|
+
serverUrl
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
async function pollConnectorDeviceToken(options) {
|
|
2017
|
+
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
2018
|
+
const token = await pollDeviceToken(new CliAuthClient(serverUrl, { allowInsecureTls: options.allowInsecureTls }), {
|
|
2019
|
+
deviceCode: options.deviceCode,
|
|
2020
|
+
intervalSeconds: options.intervalSeconds,
|
|
2021
|
+
expiresInSeconds: options.expiresInSeconds
|
|
2022
|
+
});
|
|
2023
|
+
return options.store.saveLogin({
|
|
2024
|
+
serverUrl,
|
|
2025
|
+
refreshToken: token.refreshToken,
|
|
2026
|
+
accessToken: token.accessToken,
|
|
2027
|
+
expiresInSeconds: token.expiresIn,
|
|
2028
|
+
allowInsecureTls: options.allowInsecureTls
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
async function ensureAccessToken(input) {
|
|
2032
|
+
const serverUrl = normalizeServerUrl(input.serverUrl);
|
|
2033
|
+
return input.store.withLockedData(async (data) => {
|
|
2034
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
2035
|
+
const profile = data.profiles[serverUrl];
|
|
2036
|
+
if (!profile?.refreshToken) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
|
|
2037
|
+
const auth = input.authClient ?? new CliAuthClient(serverUrl, { allowInsecureTls: input.allowInsecureTls ?? profile.allowInsecureTls });
|
|
2038
|
+
if (!input.forceRefresh && profile.accessToken && tokenIsFresh(profile.accessTokenExpiresAt, now)) return profile.accessToken;
|
|
2039
|
+
const refreshed = await auth.refresh(profile.refreshToken);
|
|
2040
|
+
const updated = {
|
|
2041
|
+
...profile,
|
|
2042
|
+
refreshToken: refreshed.refreshToken,
|
|
2043
|
+
accessToken: refreshed.accessToken,
|
|
2044
|
+
accessTokenExpiresAt: expiresAt(refreshed.expiresIn, now)
|
|
2045
|
+
};
|
|
2046
|
+
data.profiles[serverUrl] = updated;
|
|
2047
|
+
return refreshed.accessToken;
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
function createAccessTokenProvider(input) {
|
|
2051
|
+
return (options) => ensureAccessToken({
|
|
2052
|
+
...input,
|
|
2053
|
+
forceRefresh: options?.forceRefresh
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
async function claimLocalMachine(options) {
|
|
2057
|
+
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
2058
|
+
if (options.allowInsecureTls !== void 0) await options.store.updateProfile(serverUrl, (current) => ({
|
|
2059
|
+
...requireProfile(current, serverUrl),
|
|
2060
|
+
...options.allowInsecureTls ? { allowInsecureTls: true } : {}
|
|
2061
|
+
}));
|
|
2062
|
+
const accessToken = await ensureAccessToken({
|
|
2063
|
+
serverUrl,
|
|
2064
|
+
store: options.store,
|
|
2065
|
+
allowInsecureTls: options.allowInsecureTls
|
|
2066
|
+
}).catch((error) => {
|
|
2067
|
+
throw markRefreshError(error);
|
|
2068
|
+
});
|
|
2069
|
+
const profile = await options.store.getProfile(serverUrl);
|
|
2070
|
+
if (!profile) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
|
|
2071
|
+
const allowInsecureTls = options.allowInsecureTls ?? profile.allowInsecureTls;
|
|
2072
|
+
const machine = await new MachineClient(serverUrl, { allowInsecureTls }).claim({
|
|
2073
|
+
accessToken,
|
|
2074
|
+
connectorInstallId: profile.connectorInstallId,
|
|
2075
|
+
name: options.name
|
|
2076
|
+
});
|
|
2077
|
+
await options.store.updateProfile(serverUrl, (current) => ({
|
|
2078
|
+
...requireProfile(current, serverUrl),
|
|
2079
|
+
machineId: machine.machineId
|
|
2080
|
+
}));
|
|
2081
|
+
return {
|
|
2082
|
+
machine,
|
|
2083
|
+
serverUrl,
|
|
2084
|
+
allowInsecureTls
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
async function pollDeviceToken(auth, input) {
|
|
2088
|
+
const expiresAt = Date.now() + input.expiresInSeconds * 1e3;
|
|
2089
|
+
let intervalMs = Math.max(1, input.intervalSeconds) * 1e3;
|
|
2090
|
+
while (Date.now() < expiresAt) try {
|
|
2091
|
+
return await auth.pollDeviceToken(input.deviceCode);
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
const message = error instanceof Error ? error.message : "";
|
|
2094
|
+
if (message.includes("slow_down")) intervalMs += 5e3;
|
|
2095
|
+
if (!message.includes("authorization_pending") && !message.includes("slow_down")) throw error;
|
|
2096
|
+
await setTimeout$1(intervalMs);
|
|
2097
|
+
}
|
|
2098
|
+
throw new Error("CLI login expired before authorization completed.");
|
|
2099
|
+
}
|
|
2100
|
+
function tokenIsFresh(expiresAtValue, now) {
|
|
2101
|
+
if (!expiresAtValue) return false;
|
|
2102
|
+
return new Date(expiresAtValue).getTime() - now.getTime() > ACCESS_TOKEN_REFRESH_SKEW_MS;
|
|
2103
|
+
}
|
|
2104
|
+
function expiresAt(expiresInSeconds, now) {
|
|
2105
|
+
return new Date(now.getTime() + Math.max(0, expiresInSeconds) * 1e3).toISOString();
|
|
2106
|
+
}
|
|
2107
|
+
function requireProfile(profile, serverUrl) {
|
|
2108
|
+
if (!profile) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
|
|
2109
|
+
return profile;
|
|
2110
|
+
}
|
|
2111
|
+
function markRefreshError(error) {
|
|
2112
|
+
if (error instanceof Error) return Object.assign(error, { localMachineStage: "refresh" });
|
|
2113
|
+
return Object.assign(/* @__PURE__ */ new Error("Failed to refresh connector access token."), { localMachineStage: "refresh" });
|
|
2114
|
+
}
|
|
2115
|
+
function isRefreshError(error) {
|
|
2116
|
+
return Boolean(error && typeof error === "object" && error.localMachineStage === "refresh");
|
|
2117
|
+
}
|
|
2118
|
+
function classifyClaimError(error) {
|
|
2119
|
+
if (error instanceof MachineClaimError && error.code === "machine_claim_conflict") return {
|
|
2120
|
+
code: "claim_conflict",
|
|
2121
|
+
canRetry: false
|
|
2122
|
+
};
|
|
2123
|
+
return {
|
|
2124
|
+
code: "claim_failed",
|
|
2125
|
+
canRetry: true
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
//#endregion
|
|
2129
|
+
export { MACHINE_MCP_MAX_ERROR_MESSAGE_BYTES as A, validateMachineMcpRequest as B, MAX_MACHINE_JOB_OUTPUT_BYTES as C, MAX_TEXT_FILE_WRITE_BYTES as D, MAX_TEXT_FILE_READ_LINES as E, encodeMachineMcpFrame as F, parseArgs as G, MAX_MACHINE_TERMINAL_HISTORY_BYTES as H, sanitizeAndCanonicalizeMachineMcpCatalog as I, substituteMachineMcpLaunch as L, MACHINE_MCP_MAX_STDERR_BYTES as M, MACHINE_MCP_TAG as N, isServerRpcRequest as O, MachineMcpValidationError as P, utf8Bytes as R, MAX_MACHINE_FILE_SEARCH_PATH_LENGTH as S, MAX_TEXT_FILE_READ_BYTES as T, CliAuthClient as U, validateMachineMcpResult as V, allowInsecureTlsForLocalTesting as W, detectShell as _, setupConnectorProfile as a, DEFAULT_TEXT_FILE_READ_LINES as b, defaultTokenStorePath as c, installConnectorService as d, isConnectorServiceUnsupportedError as f, ShellManager as g, resolveConnectorProfileContext as h, pollConnectorDeviceToken as i, MACHINE_MCP_MAX_FRAME_BYTES as j, CONNECTOR_MAX_WEBSOCKET_PAYLOAD_BYTES as k, normalizeServerUrl as l, runServiceCommand as m, ensureAccessToken as n, startConnectorDeviceAuthorization as o, loadServiceConfig as p, ensureLocalMachineReady as r, TokenStore as s, createAccessTokenProvider as t, ConnectorServiceUnsupportedError as u, DEFAULT_MACHINE_FILE_READ_BYTES as v, MAX_PATCH_SOURCE_FILE_BYTES as w, MAX_MACHINE_FILE_READ_BYTES as x, DEFAULT_TEXT_FILE_READ_BYTES as y, validateMachineMcpLaunchSpec as z };
|