appduct 0.14.0 → 0.15.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/README.md +52 -22
- package/dist/artifact-inspect.d.ts +38 -39
- package/dist/bin.js +45 -3
- package/dist/cli/result-types.d.ts +27 -1
- package/dist/cli/routes/daemon/index.js +32 -1
- package/dist/cli/routes/daemon/run.js +962 -521
- package/dist/cli/routes/daemon/start.js +40 -1
- package/dist/cli/routes/daemon/status.js +41 -1
- package/dist/cli/routes/daemon/stop.js +40 -1
- package/dist/cli/routes/doctor.js +40 -2
- package/dist/cli/routes/events/index.js +32 -1
- package/dist/cli/routes/events/ls.js +40 -1
- package/dist/cli/routes/events/since.js +40 -1
- package/dist/cli/routes/events/tail.js +40 -1
- package/dist/cli/routes/init.js +207 -107
- package/dist/cli/routes/keygen.js +32 -1
- package/dist/cli/routes/mcp.js +135 -24
- package/dist/cli/routes/sessions/index.js +32 -1
- package/dist/cli/routes/sessions/link.js +141 -21
- package/dist/cli/routes/sessions/ls.js +40 -1
- package/dist/cli/routes/sessions/revoke.js +40 -1
- package/dist/cli/routes/tools/call.js +40 -1
- package/dist/cli/routes/tools/describe.js +40 -1
- package/dist/cli/routes/tools/index.js +32 -1
- package/dist/cli/routes/tools/ls.js +40 -1
- package/dist/client/attach-page.d.ts +39 -0
- package/dist/client/bootstrap.d.ts +17 -3
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +286 -112
- package/dist/commands/doctor.d.ts +3 -4
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/link.d.ts +10 -1
- package/dist/daemon/calls.d.ts +1 -1
- package/dist/daemon/config.d.ts +6 -0
- package/dist/daemon/daemon.d.ts +6 -1
- package/dist/daemon/links.d.ts +9 -5
- package/dist/daemon/listener.d.ts +28 -1
- package/dist/daemon/rpc-errors.d.ts +3 -0
- package/dist/daemon/sessions.d.ts +11 -7
- package/dist/daemon/tls.d.ts +0 -3
- package/dist/devtools-relay/attach-tab.d.ts +33 -0
- package/dist/devtools-relay/index.d.ts +8 -0
- package/dist/devtools-relay/memory-daemon-sockets.d.ts +20 -0
- package/dist/devtools-relay/memory-devtools-browser.d.ts +10 -0
- package/dist/devtools-relay/memory-page-channel.d.ts +16 -0
- package/dist/devtools-relay/node-daemon-socket.d.ts +3 -0
- package/dist/devtools-relay/node-devtools-browser.d.ts +3 -0
- package/dist/devtools-relay/ports.d.ts +48 -0
- package/dist/devtools-relay/relay.d.ts +16 -0
- package/dist/index.js +45 -3
- package/dist/link.d.ts +35 -1
- package/dist/mcp/connect-tool.d.ts +15 -2
- package/dist/native-scheme.d.ts +11 -7
- package/dist/scheme.d.ts +5 -1
- package/package.json +15 -5
|
@@ -2,131 +2,209 @@
|
|
|
2
2
|
import { rm as rm5 } from "node:fs/promises";
|
|
3
3
|
import {
|
|
4
4
|
MAX_TOOLS_FILTER_LENGTH,
|
|
5
|
-
RPC_METHODS,
|
|
5
|
+
RPC_METHODS as RPC_METHODS3,
|
|
6
6
|
isValidToolGroup,
|
|
7
7
|
summarizeToolGroups,
|
|
8
8
|
toolGroupMatches,
|
|
9
9
|
EVENT_KINDS
|
|
10
10
|
} from "@appduct/shared";
|
|
11
11
|
|
|
12
|
-
// src/
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
12
|
+
// src/devtools-relay/relay.ts
|
|
13
|
+
import { decodeBootstrap } from "@appduct/shared";
|
|
14
|
+
var linkPayload = (link) => {
|
|
15
|
+
const payload = new URLSearchParams(new URL(link.url).hash.slice(1)).get("appduct");
|
|
16
|
+
if (payload === null) throw new Error('The link has no #appduct= payload: pass the result of link({ target: "web", url }).');
|
|
17
|
+
return payload;
|
|
18
|
+
};
|
|
19
|
+
var parseLink = (link) => {
|
|
20
|
+
const bootstrap = decodeBootstrap(linkPayload(link));
|
|
21
|
+
if (!bootstrap) throw new Error("The link's #appduct= payload is not a valid Appduct link.");
|
|
22
|
+
return bootstrap;
|
|
23
|
+
};
|
|
24
|
+
var GOING_AWAY = 1001;
|
|
25
|
+
var relayPage = (page, link, openDaemonSocket) => {
|
|
26
|
+
const bootstrap = parseLink(link);
|
|
27
|
+
const host = bootstrap.address.includes(":") ? `[${bootstrap.address}]` : bootstrap.address;
|
|
28
|
+
const url = `ws://${host}:${bootstrap.port}`;
|
|
29
|
+
let current;
|
|
30
|
+
const tellPage = (message) => {
|
|
31
|
+
page.evaluate(`window.__APPDUCT__.receive(${JSON.stringify(JSON.stringify(message))})`).catch(() => void 0);
|
|
32
|
+
};
|
|
33
|
+
const closeCurrent = (code, reason) => {
|
|
34
|
+
const socket = current;
|
|
35
|
+
current = void 0;
|
|
36
|
+
socket?.close(code, reason);
|
|
37
|
+
};
|
|
38
|
+
page.onBindingCall((json) => {
|
|
39
|
+
const message = JSON.parse(json);
|
|
40
|
+
if (message.kind === "open") {
|
|
41
|
+
closeCurrent(GOING_AWAY, "superseded");
|
|
42
|
+
const socket = openDaemonSocket(url, {
|
|
43
|
+
open: () => current === socket && tellPage({ kind: "open" }),
|
|
44
|
+
message: (text) => current === socket && tellPage({ kind: "message", text }),
|
|
45
|
+
close: (code, reason) => {
|
|
46
|
+
if (current !== socket) return;
|
|
47
|
+
current = void 0;
|
|
48
|
+
tellPage({ kind: "close", code, reason });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
current = socket;
|
|
52
|
+
} else if (message.kind === "send") {
|
|
53
|
+
current?.send(message.text ?? "");
|
|
54
|
+
} else if (message.kind === "close") {
|
|
55
|
+
closeCurrent(message.code ?? 1e3, message.reason ?? "");
|
|
43
56
|
}
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
});
|
|
58
|
+
page.onContextReset(() => closeCurrent(GOING_AWAY, "page_gone"));
|
|
46
59
|
};
|
|
47
60
|
|
|
48
|
-
// src/
|
|
49
|
-
var
|
|
50
|
-
var
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
+
// src/devtools-relay/attach-tab.ts
|
|
62
|
+
var listTabs = (tabs) => tabs.length === 0 ? " (none)" : tabs.map((tab) => ` ${tab.id} ${tab.url}`).join("\n");
|
|
63
|
+
var pickTab = (tabs, url, targetId) => {
|
|
64
|
+
if (targetId !== void 0) {
|
|
65
|
+
const byId = tabs.find((tab) => tab.id === targetId);
|
|
66
|
+
if (byId) return byId;
|
|
67
|
+
throw new Error(`No open tab has target id "${targetId}". Open tabs:
|
|
68
|
+
${listTabs(tabs)}`);
|
|
69
|
+
}
|
|
70
|
+
const matching = tabs.filter((tab) => tab.url.startsWith(url));
|
|
71
|
+
if (matching.length === 1) return matching[0];
|
|
72
|
+
if (matching.length === 0) throw new Error(`No open tab starts with "${url}". Open tabs:
|
|
73
|
+
${listTabs(tabs)}`);
|
|
74
|
+
throw new Error(`${matching.length} open tabs start with "${url}"; pass the target id of the one you want. Matching tabs:
|
|
75
|
+
${listTabs(matching)}`);
|
|
76
|
+
};
|
|
77
|
+
var WAIT_FOR_WEB = `new Promise((resolve, reject) => {
|
|
78
|
+
const ready = () => typeof window.__APPDUCT__?.connect === 'function';
|
|
79
|
+
if (ready()) return resolve(true);
|
|
80
|
+
const timer = setInterval(() => { if (ready()) { clearInterval(timer); clearTimeout(giveUp); resolve(true); } }, 50);
|
|
81
|
+
const giveUp = setTimeout(() => { clearInterval(timer); reject(new Error('@appduct/web did not load')); }, 5000);
|
|
82
|
+
})`;
|
|
83
|
+
var attachBrowserTab = async (options) => {
|
|
84
|
+
const tab = pickTab(await options.browser.targets(), options.url, options.targetId);
|
|
85
|
+
const link = await options.mintLink();
|
|
86
|
+
const page = await options.browser.openPage(tab.id);
|
|
87
|
+
options.relayedTabs.get(tab.id)?.close();
|
|
88
|
+
options.relayedTabs.set(tab.id, page);
|
|
89
|
+
try {
|
|
90
|
+
relayPage(page, link, options.openDaemonSocket);
|
|
91
|
+
await page.evaluate(WAIT_FOR_WEB).catch(() => {
|
|
92
|
+
throw new Error(`The tab ${tab.id} (${tab.url}) does not load @appduct/web. Add it to the page, then try again.`);
|
|
93
|
+
});
|
|
94
|
+
await page.evaluate(`window.__APPDUCT__.connect(${JSON.stringify(linkPayload(link))}, { transport: "devtools" })`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
page.close();
|
|
97
|
+
if (options.relayedTabs.get(tab.id) === page) options.relayedTabs.delete(tab.id);
|
|
98
|
+
throw error;
|
|
61
99
|
}
|
|
62
|
-
|
|
63
|
-
return override !== void 0 && override.length > 0 ? override : getPackageVersion();
|
|
100
|
+
return { sessionId: link.sessionId, url: tab.url, targetId: tab.id, expiresAt: link.expiresAt };
|
|
64
101
|
};
|
|
65
102
|
|
|
66
|
-
// src/
|
|
67
|
-
import
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
// src/devtools-relay/node-devtools-browser.ts
|
|
104
|
+
import WebSocket from "ws";
|
|
105
|
+
var BINDING = "__appductBinding";
|
|
106
|
+
var listTargets = async (browserUrl) => {
|
|
107
|
+
let listed;
|
|
108
|
+
try {
|
|
109
|
+
listed = await (await fetch(new URL("/json/list", browserUrl))).json();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`Could not reach a browser at ${browserUrl}: ${error.message}. Launch Chrome with --remote-debugging-port and its own --user-data-dir.`);
|
|
112
|
+
}
|
|
113
|
+
return listed.filter((target) => target.type === "page");
|
|
114
|
+
};
|
|
115
|
+
var openTab = async (target, browserUrl) => {
|
|
116
|
+
const endpoint = new URL(browserUrl);
|
|
117
|
+
const socketUrl = new URL(target.webSocketDebuggerUrl);
|
|
118
|
+
socketUrl.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:";
|
|
119
|
+
socketUrl.host = endpoint.host;
|
|
120
|
+
const socket = new WebSocket(socketUrl);
|
|
121
|
+
await new Promise((resolve2, reject) => {
|
|
122
|
+
socket.once("open", () => resolve2());
|
|
123
|
+
socket.once("error", reject);
|
|
124
|
+
});
|
|
125
|
+
const pending = /* @__PURE__ */ new Map();
|
|
126
|
+
const bindingHandlers = [];
|
|
127
|
+
const resetHandlers = [];
|
|
128
|
+
let nextId = 0;
|
|
129
|
+
const send = (method, params) => new Promise((resolve2, reject) => {
|
|
130
|
+
nextId += 1;
|
|
131
|
+
pending.set(nextId, { resolve: resolve2, reject });
|
|
132
|
+
socket.send(JSON.stringify({ id: nextId, method, params }));
|
|
133
|
+
});
|
|
134
|
+
socket.on("message", (data) => {
|
|
135
|
+
const message = JSON.parse(data.toString());
|
|
136
|
+
if (message.id !== void 0) {
|
|
137
|
+
const call = pending.get(message.id);
|
|
138
|
+
pending.delete(message.id);
|
|
139
|
+
if (message.error) call?.reject(new Error(message.error.message));
|
|
140
|
+
else call?.resolve(message.result);
|
|
141
|
+
} else if (message.method === "Runtime.bindingCalled" && message.params?.name === BINDING) {
|
|
142
|
+
bindingHandlers.forEach((handler) => handler(message.params.payload));
|
|
143
|
+
} else if (message.method === "Runtime.executionContextsCleared") {
|
|
144
|
+
resetHandlers.forEach((handler) => handler());
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
socket.on("error", () => void 0);
|
|
148
|
+
socket.on("close", () => {
|
|
149
|
+
pending.forEach((call) => call.reject(new Error("The tab's debugging connection closed.")));
|
|
150
|
+
pending.clear();
|
|
151
|
+
resetHandlers.forEach((handler) => handler());
|
|
152
|
+
});
|
|
153
|
+
await send("Runtime.enable", {});
|
|
154
|
+
await send("Runtime.addBinding", { name: BINDING });
|
|
155
|
+
return {
|
|
156
|
+
onBindingCall: (handler) => void bindingHandlers.push(handler),
|
|
157
|
+
onContextReset: (handler) => void resetHandlers.push(handler),
|
|
158
|
+
evaluate: async (expression) => {
|
|
159
|
+
const result = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true });
|
|
160
|
+
const failure = result?.exceptionDetails;
|
|
161
|
+
if (failure) throw new Error(failure.exception?.description ?? failure.text);
|
|
162
|
+
},
|
|
163
|
+
close: () => socket.close()
|
|
164
|
+
};
|
|
84
165
|
};
|
|
85
|
-
var
|
|
86
|
-
|
|
166
|
+
var connectNodeDevtoolsBrowser = (browserUrl) => {
|
|
167
|
+
const open4 = /* @__PURE__ */ new Map();
|
|
168
|
+
return {
|
|
169
|
+
targets: async () => (await listTargets(browserUrl)).map(({ id, url, title }) => ({ id, url, title })),
|
|
170
|
+
openPage: async (targetId) => {
|
|
171
|
+
const target = (await listTargets(browserUrl)).find((candidate) => candidate.id === targetId);
|
|
172
|
+
if (!target?.webSocketDebuggerUrl) throw new Error(`The browser at ${browserUrl} has no tab ${targetId}.`);
|
|
173
|
+
const page = await openTab(target, browserUrl);
|
|
174
|
+
open4.get(targetId)?.close();
|
|
175
|
+
open4.set(targetId, page);
|
|
176
|
+
return page;
|
|
177
|
+
},
|
|
178
|
+
close: () => {
|
|
179
|
+
open4.forEach((page) => page.close());
|
|
180
|
+
open4.clear();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
87
183
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (const entry of entries ?? []) {
|
|
99
|
-
if (entry.internal) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (entry.family === "IPv4") {
|
|
103
|
-
if (isExcludedIpv4Address(entry.address)) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
ipv4Candidates.push(entry.address);
|
|
107
|
-
} else if (entry.family === "IPv6") {
|
|
108
|
-
if (isLinkLocalIpv6Address(entry.address)) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
ipv6Candidates.push(entry.address);
|
|
112
|
-
}
|
|
184
|
+
|
|
185
|
+
// src/devtools-relay/node-daemon-socket.ts
|
|
186
|
+
import WebSocket2 from "ws";
|
|
187
|
+
var openNodeDaemonSocket = (url, events) => {
|
|
188
|
+
const socket = new WebSocket2(url);
|
|
189
|
+
socket.on("open", () => events.open());
|
|
190
|
+
socket.on("message", (data, isBinary) => {
|
|
191
|
+
if (isBinary) {
|
|
192
|
+
socket.close(4008, "binary_frame_not_supported");
|
|
193
|
+
return;
|
|
113
194
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
const preferredIpv6 = ipv6Candidates.find((address) => isUniqueLocalIpv6Address(address)) ?? ipv6Candidates[0];
|
|
120
|
-
if (preferredIpv6) {
|
|
121
|
-
return { family: 6, address: preferredIpv6 };
|
|
122
|
-
}
|
|
123
|
-
return { family: 4, address: "127.0.0.1" };
|
|
195
|
+
events.message(data.toString());
|
|
196
|
+
});
|
|
197
|
+
socket.on("error", () => void 0);
|
|
198
|
+
socket.on("close", (code, reason) => events.close(code, reason.toString()));
|
|
199
|
+
return { send: (text) => socket.send(text), close: (code, reason) => socket.close(code, reason) };
|
|
124
200
|
};
|
|
125
201
|
|
|
126
|
-
// src/
|
|
127
|
-
import {
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
// src/link.ts
|
|
203
|
+
import { RPC_METHODS as RPC_METHODS2 } from "@appduct/shared";
|
|
204
|
+
|
|
205
|
+
// src/rpc/client.ts
|
|
206
|
+
import { join as join4 } from "node:path";
|
|
207
|
+
import { RPC_METHODS } from "@appduct/shared";
|
|
130
208
|
|
|
131
209
|
// src/daemon/config.ts
|
|
132
210
|
import { readFile } from "node:fs/promises";
|
|
@@ -141,6 +219,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
141
219
|
"daemonLogMaxBytes",
|
|
142
220
|
"eventsLogMaxBytes",
|
|
143
221
|
"policy",
|
|
222
|
+
"webOrigins",
|
|
144
223
|
"advertisedIp",
|
|
145
224
|
"scheme",
|
|
146
225
|
"restartDaemonOnVersionMismatch"
|
|
@@ -201,6 +280,7 @@ var defaultConfig = (paths) => {
|
|
|
201
280
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
202
281
|
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
203
282
|
restartDaemonOnVersionMismatch: false,
|
|
283
|
+
webOrigins: [],
|
|
204
284
|
policy: {
|
|
205
285
|
default: "allow",
|
|
206
286
|
destructive: "allow"
|
|
@@ -244,64 +324,482 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
244
324
|
if (parsed.graceSeconds !== void 0) {
|
|
245
325
|
config.graceSeconds = requirePositiveInteger(parsed.graceSeconds, "graceSeconds");
|
|
246
326
|
}
|
|
247
|
-
if (parsed.linkTtlSeconds !== void 0) {
|
|
248
|
-
config.linkTtlSeconds = requirePositiveInteger(parsed.linkTtlSeconds, "linkTtlSeconds");
|
|
327
|
+
if (parsed.linkTtlSeconds !== void 0) {
|
|
328
|
+
config.linkTtlSeconds = requirePositiveInteger(parsed.linkTtlSeconds, "linkTtlSeconds");
|
|
329
|
+
}
|
|
330
|
+
if (parsed.keepaliveIntervalSeconds !== void 0) {
|
|
331
|
+
config.keepaliveIntervalSeconds = requirePositiveInteger(
|
|
332
|
+
parsed.keepaliveIntervalSeconds,
|
|
333
|
+
"keepaliveIntervalSeconds"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (parsed.eventBufferSize !== void 0) {
|
|
337
|
+
config.eventBufferSize = requirePositiveInteger(parsed.eventBufferSize, "eventBufferSize");
|
|
338
|
+
}
|
|
339
|
+
if (parsed.auditRetentionDays !== void 0) {
|
|
340
|
+
config.auditRetentionDays = requirePositiveInteger(parsed.auditRetentionDays, "auditRetentionDays");
|
|
341
|
+
}
|
|
342
|
+
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
343
|
+
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
344
|
+
}
|
|
345
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
346
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
347
|
+
}
|
|
348
|
+
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
349
|
+
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
350
|
+
parsed.restartDaemonOnVersionMismatch,
|
|
351
|
+
"restartDaemonOnVersionMismatch"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (parsed.webOrigins !== void 0) {
|
|
355
|
+
if (!Array.isArray(parsed.webOrigins)) {
|
|
356
|
+
throw configError("webOrigins", "must be an array of origin strings.");
|
|
357
|
+
}
|
|
358
|
+
config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
|
|
359
|
+
}
|
|
360
|
+
if (parsed.policy !== void 0) {
|
|
361
|
+
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
362
|
+
throw configError("policy", "must be an object.");
|
|
363
|
+
}
|
|
364
|
+
const policy = parsed.policy;
|
|
365
|
+
for (const key of Object.keys(policy)) {
|
|
366
|
+
if (!KNOWN_POLICY_KEYS.has(key)) {
|
|
367
|
+
warn(`Unknown Appduct config key "policy.${key}" is ignored.`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (policy.default !== void 0) {
|
|
371
|
+
config.policy.default = requirePolicyDecision(policy.default, "policy.default");
|
|
372
|
+
}
|
|
373
|
+
if (policy.destructive !== void 0) {
|
|
374
|
+
config.policy.destructive = requirePolicyDecision(policy.destructive, "policy.destructive");
|
|
375
|
+
}
|
|
376
|
+
if (policy.tools !== void 0) {
|
|
377
|
+
if (typeof policy.tools !== "object" || policy.tools === null || Array.isArray(policy.tools)) {
|
|
378
|
+
throw configError("policy.tools", "must be an object.");
|
|
379
|
+
}
|
|
380
|
+
const tools = {};
|
|
381
|
+
for (const [toolKey, toolValue] of Object.entries(policy.tools)) {
|
|
382
|
+
tools[toolKey] = requirePolicyDecision(toolValue, `policy.tools["${toolKey}"]`);
|
|
383
|
+
}
|
|
384
|
+
config.policy.tools = tools;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return config;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/daemon/pidfile.ts
|
|
391
|
+
import { readFileSync } from "node:fs";
|
|
392
|
+
import { open, readFile as readFile2, rm } from "node:fs/promises";
|
|
393
|
+
var DaemonAlreadyRunningError = class extends Error {
|
|
394
|
+
constructor(pid) {
|
|
395
|
+
super(`Appduct daemon is already running (pid ${pid}).`);
|
|
396
|
+
this.pid = pid;
|
|
397
|
+
this.name = "DaemonAlreadyRunningError";
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var readProcStatus = (pid) => {
|
|
401
|
+
try {
|
|
402
|
+
return readFileSync(`/proc/${pid}/status`, "utf8");
|
|
403
|
+
} catch {
|
|
404
|
+
return void 0;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var isZombie = (pid, readStatus) => {
|
|
408
|
+
const status = readStatus(pid);
|
|
409
|
+
if (status === void 0) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
return /^State:\s*Z\b/mu.test(status);
|
|
413
|
+
};
|
|
414
|
+
var isProcessAlive = (pid, readStatus = readProcStatus) => {
|
|
415
|
+
try {
|
|
416
|
+
process.kill(pid, 0);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return error.code === "EPERM";
|
|
419
|
+
}
|
|
420
|
+
return !isZombie(pid, readStatus);
|
|
421
|
+
};
|
|
422
|
+
var readPidFromFile = async (pidFilePath) => {
|
|
423
|
+
let contents;
|
|
424
|
+
try {
|
|
425
|
+
contents = await readFile2(pidFilePath, "utf8");
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error.code === "ENOENT") {
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const pid = Number.parseInt(contents.trim(), 10);
|
|
433
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
434
|
+
};
|
|
435
|
+
var acquirePidfile = async (pidFilePath, options = {}) => {
|
|
436
|
+
const pid = options.pid ?? process.pid;
|
|
437
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
438
|
+
try {
|
|
439
|
+
const handle = await open(pidFilePath, "wx", 384);
|
|
440
|
+
try {
|
|
441
|
+
await handle.writeFile(String(pid), "utf8");
|
|
442
|
+
} finally {
|
|
443
|
+
await handle.close();
|
|
444
|
+
}
|
|
445
|
+
let released = false;
|
|
446
|
+
return {
|
|
447
|
+
pid,
|
|
448
|
+
release: async () => {
|
|
449
|
+
if (released) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
released = true;
|
|
453
|
+
await rm(pidFilePath, { force: true });
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error.code !== "EEXIST") {
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
const existingPid = await readPidFromFile(pidFilePath);
|
|
461
|
+
if (existingPid !== void 0 && isProcessAlive(existingPid)) {
|
|
462
|
+
throw new DaemonAlreadyRunningError(existingPid);
|
|
463
|
+
}
|
|
464
|
+
await rm(pidFilePath, { force: true });
|
|
465
|
+
await options.onStaleTakeover?.();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
throw new Error(`Failed to acquire the Appduct daemon pidfile at "${pidFilePath}".`);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// src/daemon/state-dir.ts
|
|
472
|
+
import { join } from "node:path";
|
|
473
|
+
import { mkdir, chmod } from "node:fs/promises";
|
|
474
|
+
var getStateDirPaths = (stateDir) => {
|
|
475
|
+
return {
|
|
476
|
+
root: stateDir,
|
|
477
|
+
socketPath: join(stateDir, "daemon.sock"),
|
|
478
|
+
pidFilePath: join(stateDir, "daemon.pid"),
|
|
479
|
+
logFilePath: join(stateDir, "daemon.log"),
|
|
480
|
+
eventsLogPath: join(stateDir, "events.log"),
|
|
481
|
+
keyPath: join(stateDir, "key.pem"),
|
|
482
|
+
configPath: join(stateDir, "config.json"),
|
|
483
|
+
auditDir: join(stateDir, "audit"),
|
|
484
|
+
spawnLockPath: join(stateDir, "daemon.spawn.lock")
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
var getSocketPath = (paths) => {
|
|
488
|
+
return paths.socketPath;
|
|
489
|
+
};
|
|
490
|
+
var ensureStateDir = async (stateDir) => {
|
|
491
|
+
const paths = getStateDirPaths(stateDir);
|
|
492
|
+
await mkdir(paths.auditDir, { recursive: true });
|
|
493
|
+
await chmod(paths.root, 448);
|
|
494
|
+
await chmod(paths.auditDir, 448);
|
|
495
|
+
return paths;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/package-root.ts
|
|
499
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
500
|
+
import { dirname, join as join2 } from "node:path";
|
|
501
|
+
import { fileURLToPath } from "node:url";
|
|
502
|
+
var PACKAGE_NAME = "appduct";
|
|
503
|
+
var cached;
|
|
504
|
+
var getPackageRoot = () => {
|
|
505
|
+
if (cached !== void 0) {
|
|
506
|
+
return cached;
|
|
507
|
+
}
|
|
508
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
509
|
+
for (; ; ) {
|
|
510
|
+
const candidate = join2(dir, "package.json");
|
|
511
|
+
if (existsSync(candidate)) {
|
|
512
|
+
let name;
|
|
513
|
+
try {
|
|
514
|
+
name = JSON.parse(readFileSync2(candidate, "utf8")).name;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
if (name === PACKAGE_NAME) {
|
|
518
|
+
cached = dir;
|
|
519
|
+
return dir;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const parent = dirname(dir);
|
|
523
|
+
if (parent === dir) {
|
|
524
|
+
throw new Error(`Could not locate the "${PACKAGE_NAME}" package root above ${import.meta.url}.`);
|
|
525
|
+
}
|
|
526
|
+
dir = parent;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/package-version.ts
|
|
531
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
532
|
+
import { join as join3 } from "node:path";
|
|
533
|
+
var DAEMON_VERSION_OVERRIDE_ENV = "APPDUCT_DAEMON_VERSION_OVERRIDE";
|
|
534
|
+
var isTestRuntime = (env) => {
|
|
535
|
+
return Boolean(env.VITEST) || env.NODE_ENV === "test";
|
|
536
|
+
};
|
|
537
|
+
var cached2;
|
|
538
|
+
var getPackageVersion = () => {
|
|
539
|
+
cached2 ??= JSON.parse(readFileSync3(join3(getPackageRoot(), "package.json"), "utf8")).version;
|
|
540
|
+
return cached2;
|
|
541
|
+
};
|
|
542
|
+
var getDaemonReportedVersion = (env = process.env) => {
|
|
543
|
+
if (!isTestRuntime(env)) {
|
|
544
|
+
return getPackageVersion();
|
|
545
|
+
}
|
|
546
|
+
const override = env[DAEMON_VERSION_OVERRIDE_ENV];
|
|
547
|
+
return override !== void 0 && override.length > 0 ? override : getPackageVersion();
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// src/rpc/client.ts
|
|
551
|
+
var defaultBinPath = join4(getPackageRoot(), "bin.js");
|
|
552
|
+
var DaemonRpcError = class extends Error {
|
|
553
|
+
constructor(code, message, data) {
|
|
554
|
+
super(message);
|
|
555
|
+
this.code = code;
|
|
556
|
+
this.data = data;
|
|
557
|
+
this.name = "DaemonRpcError";
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
var DaemonUnavailableError = class extends Error {
|
|
561
|
+
constructor(message, reason = "unreachable") {
|
|
562
|
+
super(message);
|
|
563
|
+
this.reason = reason;
|
|
564
|
+
this.name = "DaemonUnavailableError";
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var describeLiveState = (state) => {
|
|
568
|
+
const parts = [];
|
|
569
|
+
if (state.sessions > 0) {
|
|
570
|
+
parts.push(`${state.sessions} connected session(s)`);
|
|
571
|
+
}
|
|
572
|
+
if (state.pendingLinks > 0) {
|
|
573
|
+
parts.push(`${state.pendingLinks} unclaimed link(s)`);
|
|
574
|
+
}
|
|
575
|
+
return parts.join(" and ");
|
|
576
|
+
};
|
|
577
|
+
var DaemonVersionMismatchError = class extends Error {
|
|
578
|
+
constructor(daemonVersion, clientVersion, live, message) {
|
|
579
|
+
super(
|
|
580
|
+
message ?? `The running Appduct daemon is version ${daemonVersion}, but this client is version ${clientVersion}. It was not restarted automatically because ${describeLiveState(live)} would be lost. Run "appduct daemon stop" once that state is expendable, or force it with "--daemon-restart".`
|
|
581
|
+
);
|
|
582
|
+
this.daemonVersion = daemonVersion;
|
|
583
|
+
this.clientVersion = clientVersion;
|
|
584
|
+
this.name = "DaemonVersionMismatchError";
|
|
585
|
+
this.sessionCount = live.sessions;
|
|
586
|
+
this.pendingLinkCount = live.pendingLinks;
|
|
587
|
+
}
|
|
588
|
+
sessionCount;
|
|
589
|
+
pendingLinkCount;
|
|
590
|
+
};
|
|
591
|
+
var isConnectionMissingError = (error) => {
|
|
592
|
+
const code = error.code;
|
|
593
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
594
|
+
};
|
|
595
|
+
var isDaemonUnreachableError = (error) => {
|
|
596
|
+
return error instanceof DaemonUnavailableError || isConnectionMissingError(error);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/errors.ts
|
|
600
|
+
var EXIT_CODE_BY_ERROR_TYPE = {
|
|
601
|
+
usage_error: 64,
|
|
602
|
+
validation_error: 65,
|
|
603
|
+
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
604
|
+
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
605
|
+
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
606
|
+
// assertion failure.
|
|
607
|
+
inspection_error: 66,
|
|
608
|
+
connection_error: 70,
|
|
609
|
+
session_error: 71,
|
|
610
|
+
tool_error: 72,
|
|
611
|
+
permission_error: 77,
|
|
612
|
+
internal_error: 1,
|
|
613
|
+
// `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
|
|
614
|
+
// assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
|
|
615
|
+
// and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
|
|
616
|
+
// conflate a normal assertion failure with an unexpected crash.
|
|
617
|
+
assertion_error: 3
|
|
618
|
+
};
|
|
619
|
+
var RPC_ERROR_EXIT_CLASS = {
|
|
620
|
+
no_session: "session_error",
|
|
621
|
+
ambiguous_session: "session_error",
|
|
622
|
+
unknown_session: "session_error",
|
|
623
|
+
session_not_active: "session_error",
|
|
624
|
+
session_suspended: "session_error",
|
|
625
|
+
tool_not_found: "tool_error",
|
|
626
|
+
tool_input_validation_error: "tool_error",
|
|
627
|
+
tool_output_validation_error: "tool_error",
|
|
628
|
+
tool_execution_error: "tool_error",
|
|
629
|
+
tool_serialization_error: "tool_error",
|
|
630
|
+
tool_timeout: "tool_error",
|
|
631
|
+
tool_cancelled: "tool_error",
|
|
632
|
+
policy_denied: "permission_error",
|
|
633
|
+
invalid_request: "validation_error"
|
|
634
|
+
};
|
|
635
|
+
var AppductCliError = class extends Error {
|
|
636
|
+
type;
|
|
637
|
+
details;
|
|
638
|
+
constructor(type, message, details) {
|
|
639
|
+
super(message);
|
|
640
|
+
this.name = "AppductCliError";
|
|
641
|
+
this.type = type;
|
|
642
|
+
this.details = details;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
var connectionError = (message, details) => {
|
|
646
|
+
return new AppductCliError("connection_error", message, details);
|
|
647
|
+
};
|
|
648
|
+
var isAppductCliError = (value) => {
|
|
649
|
+
return value instanceof AppductCliError;
|
|
650
|
+
};
|
|
651
|
+
var rpcErrorClass = (error) => {
|
|
652
|
+
const type = error.data?.type;
|
|
653
|
+
return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
|
|
654
|
+
};
|
|
655
|
+
var toCliError = (error) => {
|
|
656
|
+
if (isAppductCliError(error)) {
|
|
657
|
+
return {
|
|
658
|
+
type: error.type,
|
|
659
|
+
message: error.message,
|
|
660
|
+
details: error.details
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
if (error instanceof DaemonVersionMismatchError) {
|
|
664
|
+
return {
|
|
665
|
+
type: "connection_error",
|
|
666
|
+
message: error.message,
|
|
667
|
+
details: {
|
|
668
|
+
daemon_version: error.daemonVersion,
|
|
669
|
+
client_version: error.clientVersion,
|
|
670
|
+
session_count: error.sessionCount,
|
|
671
|
+
pending_link_count: error.pendingLinkCount
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (error instanceof DaemonRpcError && error.data?.type) {
|
|
676
|
+
return {
|
|
677
|
+
type: error.data.type,
|
|
678
|
+
message: error.message,
|
|
679
|
+
details: error.data.details
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (isDaemonUnreachableError(error)) {
|
|
683
|
+
return {
|
|
684
|
+
type: "connection_error",
|
|
685
|
+
message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (error instanceof Error) {
|
|
689
|
+
return {
|
|
690
|
+
type: "internal_error",
|
|
691
|
+
message: error.message
|
|
692
|
+
};
|
|
249
693
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
694
|
+
return {
|
|
695
|
+
type: "internal_error",
|
|
696
|
+
message: "An unexpected error occurred.",
|
|
697
|
+
details: error
|
|
698
|
+
};
|
|
699
|
+
};
|
|
700
|
+
var getExitCodeForError = (error) => {
|
|
701
|
+
if (isAppductCliError(error)) {
|
|
702
|
+
return EXIT_CODE_BY_ERROR_TYPE[error.type];
|
|
255
703
|
}
|
|
256
|
-
if (
|
|
257
|
-
|
|
704
|
+
if (error instanceof DaemonVersionMismatchError) {
|
|
705
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
258
706
|
}
|
|
259
|
-
if (
|
|
260
|
-
|
|
707
|
+
if (error instanceof DaemonRpcError) {
|
|
708
|
+
const cliErrorClass = rpcErrorClass(error);
|
|
709
|
+
return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
|
|
261
710
|
}
|
|
262
|
-
if (
|
|
263
|
-
|
|
711
|
+
if (isDaemonUnreachableError(error)) {
|
|
712
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
264
713
|
}
|
|
265
|
-
|
|
266
|
-
|
|
714
|
+
return EXIT_CODE_BY_ERROR_TYPE.internal_error;
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// src/scheme.ts
|
|
718
|
+
import { dirname as dirname2, join as join5, resolve } from "node:path";
|
|
719
|
+
|
|
720
|
+
// src/native-scheme.ts
|
|
721
|
+
var MAX_PROBE_FILE_BYTES = 2 * 1024 * 1024;
|
|
722
|
+
|
|
723
|
+
// src/scheme.ts
|
|
724
|
+
var PROJECT_CONFIG_DIR = ".appduct";
|
|
725
|
+
var PROJECT_CONFIG_FILENAME = "config.json";
|
|
726
|
+
var PROJECT_CONFIG_RELATIVE_PATH = join5(PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILENAME);
|
|
727
|
+
|
|
728
|
+
// src/link.ts
|
|
729
|
+
var composeWebLink = (pageUrl, result) => {
|
|
730
|
+
const payload = result.deepLinkPayload;
|
|
731
|
+
return {
|
|
732
|
+
sessionId: result.sessionId,
|
|
733
|
+
url: `${pageUrl.split("#")[0]}#appduct=${payload}`,
|
|
734
|
+
script: `window.__APPDUCT__.connect("${payload}")`,
|
|
735
|
+
expiresAt: result.expiresAt
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/daemon/address.ts
|
|
740
|
+
import { networkInterfaces } from "node:os";
|
|
741
|
+
import {
|
|
742
|
+
isLinkLocalIpv6Address,
|
|
743
|
+
isPrivateIpv4Address,
|
|
744
|
+
isUniqueLocalIpv6Address
|
|
745
|
+
} from "@appduct/shared";
|
|
746
|
+
var isDockerDefaultBridgeAddress = (address) => {
|
|
747
|
+
return address.startsWith("172.17.");
|
|
748
|
+
};
|
|
749
|
+
var isCgnatIpv4Address = (address) => {
|
|
750
|
+
const parts = address.split(".");
|
|
751
|
+
if (parts.length !== 4) {
|
|
752
|
+
return false;
|
|
267
753
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
754
|
+
const first = Number(parts[0]);
|
|
755
|
+
const second = Number(parts[1]);
|
|
756
|
+
return first === 100 && Number.isInteger(second) && second >= 64 && second <= 127;
|
|
757
|
+
};
|
|
758
|
+
var isExcludedIpv4Address = (address) => {
|
|
759
|
+
return isDockerDefaultBridgeAddress(address) || isCgnatIpv4Address(address);
|
|
760
|
+
};
|
|
761
|
+
var detectAdvertisedAddress = (options = {}) => {
|
|
762
|
+
if (options.override) {
|
|
763
|
+
return {
|
|
764
|
+
family: options.override.includes(":") ? 6 : 4,
|
|
765
|
+
address: options.override
|
|
766
|
+
};
|
|
273
767
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (!KNOWN_POLICY_KEYS.has(key)) {
|
|
281
|
-
warn(`Unknown Appduct config key "policy.${key}" is ignored.`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (policy.default !== void 0) {
|
|
285
|
-
config.policy.default = requirePolicyDecision(policy.default, "policy.default");
|
|
286
|
-
}
|
|
287
|
-
if (policy.destructive !== void 0) {
|
|
288
|
-
config.policy.destructive = requirePolicyDecision(policy.destructive, "policy.destructive");
|
|
289
|
-
}
|
|
290
|
-
if (policy.tools !== void 0) {
|
|
291
|
-
if (typeof policy.tools !== "object" || policy.tools === null || Array.isArray(policy.tools)) {
|
|
292
|
-
throw configError("policy.tools", "must be an object.");
|
|
768
|
+
const ipv4Candidates = [];
|
|
769
|
+
const ipv6Candidates = [];
|
|
770
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
771
|
+
for (const entry of entries ?? []) {
|
|
772
|
+
if (entry.internal) {
|
|
773
|
+
continue;
|
|
293
774
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
775
|
+
if (entry.family === "IPv4") {
|
|
776
|
+
if (isExcludedIpv4Address(entry.address)) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
ipv4Candidates.push(entry.address);
|
|
780
|
+
} else if (entry.family === "IPv6") {
|
|
781
|
+
if (isLinkLocalIpv6Address(entry.address)) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
ipv6Candidates.push(entry.address);
|
|
297
785
|
}
|
|
298
|
-
config.policy.tools = tools;
|
|
299
786
|
}
|
|
300
787
|
}
|
|
301
|
-
|
|
788
|
+
const preferredIpv4 = ipv4Candidates.find((address) => isPrivateIpv4Address(address)) ?? ipv4Candidates[0];
|
|
789
|
+
if (preferredIpv4) {
|
|
790
|
+
return { family: 4, address: preferredIpv4 };
|
|
791
|
+
}
|
|
792
|
+
const preferredIpv6 = ipv6Candidates.find((address) => isUniqueLocalIpv6Address(address)) ?? ipv6Candidates[0];
|
|
793
|
+
if (preferredIpv6) {
|
|
794
|
+
return { family: 6, address: preferredIpv6 };
|
|
795
|
+
}
|
|
796
|
+
return { family: 4, address: "127.0.0.1" };
|
|
302
797
|
};
|
|
303
798
|
|
|
304
799
|
// src/daemon/audit.ts
|
|
800
|
+
import { createHash } from "node:crypto";
|
|
801
|
+
import { chmod as chmod2, mkdir as mkdir2, open as open2, readdir, rm as rm2, stat } from "node:fs/promises";
|
|
802
|
+
import { join as join6 } from "node:path";
|
|
305
803
|
var canonicalize = (value) => {
|
|
306
804
|
if (Array.isArray(value)) {
|
|
307
805
|
return value.map(canonicalize);
|
|
@@ -338,13 +836,13 @@ var oldestRetainedStamp = (now, retentionDays) => {
|
|
|
338
836
|
return Number.isNaN(cutoff.getTime()) ? "0000-00-00" : dayStamp(cutoff);
|
|
339
837
|
};
|
|
340
838
|
var auditFilePathForDate = (auditDir, date) => {
|
|
341
|
-
return
|
|
839
|
+
return join6(auditDir, `${dayStamp(date)}.jsonl`);
|
|
342
840
|
};
|
|
343
841
|
var appendLine = async (filePath, line) => {
|
|
344
|
-
const handle = await
|
|
842
|
+
const handle = await open2(filePath, "a", 384);
|
|
345
843
|
try {
|
|
346
844
|
await handle.appendFile(line);
|
|
347
|
-
await
|
|
845
|
+
await chmod2(filePath, 384);
|
|
348
846
|
} finally {
|
|
349
847
|
await handle.close();
|
|
350
848
|
}
|
|
@@ -397,7 +895,7 @@ var createAuditLogger = (options) => {
|
|
|
397
895
|
continue;
|
|
398
896
|
}
|
|
399
897
|
try {
|
|
400
|
-
await
|
|
898
|
+
await rm2(join6(options.auditDir, file.name), { force: true });
|
|
401
899
|
deleted += 1;
|
|
402
900
|
} catch (error) {
|
|
403
901
|
failed += 1;
|
|
@@ -415,7 +913,7 @@ var createAuditLogger = (options) => {
|
|
|
415
913
|
const filePath = auditFilePathForDate(options.auditDir, now);
|
|
416
914
|
queue = queue.then(async () => {
|
|
417
915
|
try {
|
|
418
|
-
await
|
|
916
|
+
await mkdir2(options.auditDir, { recursive: true });
|
|
419
917
|
await appendLine(filePath, line);
|
|
420
918
|
} catch (error) {
|
|
421
919
|
failedWrites += 1;
|
|
@@ -425,8 +923,8 @@ var createAuditLogger = (options) => {
|
|
|
425
923
|
};
|
|
426
924
|
const prune = () => {
|
|
427
925
|
let settle;
|
|
428
|
-
const result = new Promise((
|
|
429
|
-
settle =
|
|
926
|
+
const result = new Promise((resolve2) => {
|
|
927
|
+
settle = resolve2;
|
|
430
928
|
});
|
|
431
929
|
queue = queue.then(async () => {
|
|
432
930
|
try {
|
|
@@ -448,7 +946,7 @@ var createAuditLogger = (options) => {
|
|
|
448
946
|
let bytes = 0;
|
|
449
947
|
for (const file of listing.files) {
|
|
450
948
|
try {
|
|
451
|
-
bytes += (await stat(
|
|
949
|
+
bytes += (await stat(join6(options.auditDir, file.name))).size;
|
|
452
950
|
files += 1;
|
|
453
951
|
} catch {
|
|
454
952
|
}
|
|
@@ -485,6 +983,7 @@ var RpcApplicationError = class extends Error {
|
|
|
485
983
|
}
|
|
486
984
|
};
|
|
487
985
|
var appBackgroundedMessage = (alias) => `Session "${alias}" is suspended because the app is in the background. Bring the app to the foreground to resume it.`;
|
|
986
|
+
var appReconnectedMessage = (alias) => `Session "${alias}" reconnected while the call was pending, so its result was lost. The call can be retried.`;
|
|
488
987
|
|
|
489
988
|
// src/daemon/timers.ts
|
|
490
989
|
var systemTimers = {
|
|
@@ -505,6 +1004,7 @@ var clampTimeout = (timeoutMs) => {
|
|
|
505
1004
|
};
|
|
506
1005
|
var TERMINATING_KIND_ERROR_TYPES = {
|
|
507
1006
|
session_suspended: "session_suspended",
|
|
1007
|
+
session_resumed: "session_suspended",
|
|
508
1008
|
session_revoked: "unknown_session",
|
|
509
1009
|
session_expired: "unknown_session"
|
|
510
1010
|
};
|
|
@@ -526,7 +1026,7 @@ var createCallsManager = (options) => {
|
|
|
526
1026
|
};
|
|
527
1027
|
const call = (session, name, args, timeoutMs) => {
|
|
528
1028
|
const callId = generateCallId();
|
|
529
|
-
const result = new Promise((
|
|
1029
|
+
const result = new Promise((resolve2, reject) => {
|
|
530
1030
|
const effectiveTimeoutMs = clampTimeout(timeoutMs);
|
|
531
1031
|
const timer = timers.setTimeout(() => {
|
|
532
1032
|
const pending = takePending(session.sessionId, callId);
|
|
@@ -539,7 +1039,7 @@ var createCallsManager = (options) => {
|
|
|
539
1039
|
sessionMap = /* @__PURE__ */ new Map();
|
|
540
1040
|
pendingBySession.set(session.sessionId, sessionMap);
|
|
541
1041
|
}
|
|
542
|
-
sessionMap.set(callId, { alias: session.alias, resolve, reject, timer });
|
|
1042
|
+
sessionMap.set(callId, { alias: session.alias, resolve: resolve2, reject, timer });
|
|
543
1043
|
const message = {
|
|
544
1044
|
type: "tool_call",
|
|
545
1045
|
session_id: session.sessionId,
|
|
@@ -598,7 +1098,7 @@ var createCallsManager = (options) => {
|
|
|
598
1098
|
pending.reject(
|
|
599
1099
|
new RpcApplicationError(
|
|
600
1100
|
errorType,
|
|
601
|
-
options2.
|
|
1101
|
+
options2.message?.(pending.alias) ?? "Session transitioned while the call was pending."
|
|
602
1102
|
)
|
|
603
1103
|
);
|
|
604
1104
|
}
|
|
@@ -607,7 +1107,8 @@ var createCallsManager = (options) => {
|
|
|
607
1107
|
const errorType = TERMINATING_KIND_ERROR_TYPES[event.kind];
|
|
608
1108
|
if (errorType && event.sessionId) {
|
|
609
1109
|
const backgrounded = event.kind === "session_suspended" && event.data.reason === "app_backgrounded";
|
|
610
|
-
|
|
1110
|
+
const message = event.kind === "session_resumed" ? appReconnectedMessage : backgrounded ? appBackgroundedMessage : void 0;
|
|
1111
|
+
rejectSession(event.sessionId, errorType, { message });
|
|
611
1112
|
}
|
|
612
1113
|
});
|
|
613
1114
|
const disposeAll = () => {
|
|
@@ -788,8 +1289,14 @@ var createEventsLog = (options) => {
|
|
|
788
1289
|
};
|
|
789
1290
|
|
|
790
1291
|
// src/daemon/listener.ts
|
|
1292
|
+
import { createServer as createHttpServer } from "node:http";
|
|
791
1293
|
import { createServer as createHttpsServer } from "node:https";
|
|
792
|
-
import {
|
|
1294
|
+
import { createServer as createNetServer } from "node:net";
|
|
1295
|
+
import {
|
|
1296
|
+
isSessionClaimMessage,
|
|
1297
|
+
isSessionResumeMessage,
|
|
1298
|
+
isSessionBoundMessage
|
|
1299
|
+
} from "@appduct/shared";
|
|
793
1300
|
import { WebSocketServer } from "ws";
|
|
794
1301
|
|
|
795
1302
|
// src/daemon/sessions.ts
|
|
@@ -830,13 +1337,13 @@ var createPendingLinkRegistry = (options) => {
|
|
|
830
1337
|
options.eventBus.drop(sessionId);
|
|
831
1338
|
};
|
|
832
1339
|
return {
|
|
833
|
-
create: (ttlSeconds, addressOverride) => {
|
|
1340
|
+
create: (ttlSeconds, addressOverride, transport = "native") => {
|
|
834
1341
|
const sessionId = generateSessionId();
|
|
835
1342
|
const token = randomBytes2(TOKEN_BYTES);
|
|
836
1343
|
const createdAt = options.clock.now();
|
|
837
1344
|
const expiresAt = new Date(createdAt.getTime() + ttlSeconds * 1e3);
|
|
838
|
-
const detectedEndpoint = options.getEndpoint();
|
|
839
|
-
const endpoint = addressOverride ? {
|
|
1345
|
+
const detectedEndpoint = options.getEndpoint(transport);
|
|
1346
|
+
const endpoint = addressOverride && transport === "native" ? {
|
|
840
1347
|
family: addressOverride.includes(":") ? 6 : 4,
|
|
841
1348
|
address: addressOverride,
|
|
842
1349
|
port: detectedEndpoint.port
|
|
@@ -848,7 +1355,7 @@ var createPendingLinkRegistry = (options) => {
|
|
|
848
1355
|
record.timer = options.timers.setTimeout(() => discard(sessionId), ttlSeconds * 1e3);
|
|
849
1356
|
}
|
|
850
1357
|
}, ttlSeconds * 1e3);
|
|
851
|
-
const link = { sessionId, token, createdAt, expiresAt };
|
|
1358
|
+
const link = { sessionId, token, createdAt, expiresAt, transport };
|
|
852
1359
|
records.set(sessionId, { link, attempts: 0, timer });
|
|
853
1360
|
const deepLinkPayload = encodeBootstrap({
|
|
854
1361
|
...endpoint,
|
|
@@ -1099,10 +1606,11 @@ var createSessionManager = (options) => {
|
|
|
1099
1606
|
}
|
|
1100
1607
|
}
|
|
1101
1608
|
}, options.keepaliveIntervalSeconds * 1e3);
|
|
1102
|
-
const createLink = (ttlSeconds, addressOverride) => {
|
|
1609
|
+
const createLink = (ttlSeconds, addressOverride, transport) => {
|
|
1103
1610
|
const { link, deepLinkPayload, endpoint } = pendingLinks.create(
|
|
1104
1611
|
ttlSeconds ?? options.linkTtlSeconds,
|
|
1105
|
-
addressOverride
|
|
1612
|
+
addressOverride,
|
|
1613
|
+
transport
|
|
1106
1614
|
);
|
|
1107
1615
|
return {
|
|
1108
1616
|
sessionId: link.sessionId,
|
|
@@ -1111,7 +1619,7 @@ var createSessionManager = (options) => {
|
|
|
1111
1619
|
expiresAt: Math.floor(link.expiresAt.getTime() / 1e3)
|
|
1112
1620
|
};
|
|
1113
1621
|
};
|
|
1114
|
-
const handleClaim = (socket, message) => {
|
|
1622
|
+
const handleClaim = (socket, message, transport) => {
|
|
1115
1623
|
const pending = pendingLinks.get(message.session_id);
|
|
1116
1624
|
if (!pending) {
|
|
1117
1625
|
if (sessions.has(message.session_id)) {
|
|
@@ -1121,6 +1629,10 @@ var createSessionManager = (options) => {
|
|
|
1121
1629
|
}
|
|
1122
1630
|
return null;
|
|
1123
1631
|
}
|
|
1632
|
+
if (pending.transport !== transport) {
|
|
1633
|
+
closeSocket(socket, 1008, "wrong_transport");
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1124
1636
|
const nowMs = clock.now().getTime();
|
|
1125
1637
|
if (pending.expiresAt.getTime() <= nowMs) {
|
|
1126
1638
|
pendingLinks.discard(pending.sessionId);
|
|
@@ -1150,6 +1662,7 @@ var createSessionManager = (options) => {
|
|
|
1150
1662
|
device,
|
|
1151
1663
|
createdAt: pending.createdAt,
|
|
1152
1664
|
claimedAt: clock.now(),
|
|
1665
|
+
transport,
|
|
1153
1666
|
resumeToken: randomBytes3(RESUME_TOKEN_BYTES),
|
|
1154
1667
|
socket,
|
|
1155
1668
|
missedPongs: 0,
|
|
@@ -1166,12 +1679,16 @@ var createSessionManager = (options) => {
|
|
|
1166
1679
|
emit("session_claimed", sessionId, alias, { device });
|
|
1167
1680
|
return sessionId;
|
|
1168
1681
|
};
|
|
1169
|
-
const handleResume = (socket, message) => {
|
|
1682
|
+
const handleResume = (socket, message, transport) => {
|
|
1170
1683
|
const session = sessions.get(message.session_id);
|
|
1171
1684
|
if (!session) {
|
|
1172
1685
|
closeSocket(socket, 1008, "unknown_session");
|
|
1173
1686
|
return null;
|
|
1174
1687
|
}
|
|
1688
|
+
if (session.transport !== transport) {
|
|
1689
|
+
closeSocket(socket, 1008, "wrong_transport");
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1175
1692
|
const candidateToken = decodeFixedLengthToken(message.resume_token, session.resumeToken.length);
|
|
1176
1693
|
if (!tokensMatch(candidateToken, session.resumeToken)) {
|
|
1177
1694
|
closeSocket(socket, 1008, "invalid_resume_token");
|
|
@@ -1203,6 +1720,9 @@ var createSessionManager = (options) => {
|
|
|
1203
1720
|
closeSocket(socket, 1008, "unknown_session");
|
|
1204
1721
|
return;
|
|
1205
1722
|
}
|
|
1723
|
+
if (socket !== session.socket) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1206
1726
|
switch (message.type) {
|
|
1207
1727
|
case "tool_registry_snapshot": {
|
|
1208
1728
|
if (!isToolRegistrySnapshotMessage(message)) {
|
|
@@ -1352,19 +1872,12 @@ var createSessionManager = (options) => {
|
|
|
1352
1872
|
// src/daemon/listener.ts
|
|
1353
1873
|
var DEFAULT_PRE_CLAIM_TIMEOUT_MS = 1e4;
|
|
1354
1874
|
var MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
1875
|
+
var MAX_PORT_ATTEMPTS = 5;
|
|
1355
1876
|
var isRecord = (value) => {
|
|
1356
1877
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1357
1878
|
};
|
|
1358
|
-
var
|
|
1359
|
-
const timers = options
|
|
1360
|
-
const preClaimTimeoutMs = options.preClaimTimeoutMs ?? DEFAULT_PRE_CLAIM_TIMEOUT_MS;
|
|
1361
|
-
const material = options.tls.current();
|
|
1362
|
-
const httpsServer = createHttpsServer({ key: material.keyPem, cert: material.certPem });
|
|
1363
|
-
httpsServer.on("error", () => {
|
|
1364
|
-
});
|
|
1365
|
-
const wss = new WebSocketServer({ server: httpsServer, maxPayload: MAX_PAYLOAD_BYTES });
|
|
1366
|
-
wss.on("error", () => {
|
|
1367
|
-
});
|
|
1879
|
+
var attachFrameGate = (wss, options) => {
|
|
1880
|
+
const { timers, preClaimTimeoutMs } = options;
|
|
1368
1881
|
wss.on("connection", (socket) => {
|
|
1369
1882
|
socket.on("error", () => {
|
|
1370
1883
|
});
|
|
@@ -1395,7 +1908,7 @@ var startListener = async (options) => {
|
|
|
1395
1908
|
}
|
|
1396
1909
|
if (claimedSessionId === null) {
|
|
1397
1910
|
if (isSessionClaimMessage(parsed)) {
|
|
1398
|
-
const result = options.sessionManager.handleClaim(socket, parsed);
|
|
1911
|
+
const result = options.sessionManager.handleClaim(socket, parsed, options.transport);
|
|
1399
1912
|
if (result) {
|
|
1400
1913
|
claimedSessionId = result;
|
|
1401
1914
|
timers.clearTimeout(preClaimTimer);
|
|
@@ -1403,7 +1916,7 @@ var startListener = async (options) => {
|
|
|
1403
1916
|
return;
|
|
1404
1917
|
}
|
|
1405
1918
|
if (isSessionResumeMessage(parsed)) {
|
|
1406
|
-
const result = options.sessionManager.handleResume(socket, parsed);
|
|
1919
|
+
const result = options.sessionManager.handleResume(socket, parsed, options.transport);
|
|
1407
1920
|
if (result) {
|
|
1408
1921
|
claimedSessionId = result;
|
|
1409
1922
|
timers.clearTimeout(preClaimTimer);
|
|
@@ -1424,134 +1937,152 @@ var startListener = async (options) => {
|
|
|
1424
1937
|
options.sessionManager.handlePostClaimMessage(claimedSessionId, socket, parsed);
|
|
1425
1938
|
});
|
|
1426
1939
|
});
|
|
1427
|
-
|
|
1940
|
+
};
|
|
1941
|
+
var listen = (server, port, host) => {
|
|
1942
|
+
return new Promise((resolve2, reject) => {
|
|
1428
1943
|
const onError = (error) => {
|
|
1429
|
-
|
|
1944
|
+
server.off("listening", onListening);
|
|
1430
1945
|
reject(error);
|
|
1431
1946
|
};
|
|
1432
1947
|
const onListening = () => {
|
|
1433
|
-
|
|
1434
|
-
|
|
1948
|
+
server.off("error", onError);
|
|
1949
|
+
resolve2();
|
|
1435
1950
|
};
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1951
|
+
server.once("error", onError);
|
|
1952
|
+
server.once("listening", onListening);
|
|
1953
|
+
server.listen(port, host);
|
|
1439
1954
|
});
|
|
1440
|
-
return {
|
|
1441
|
-
httpsServer,
|
|
1442
|
-
wss,
|
|
1443
|
-
port: () => {
|
|
1444
|
-
const address = httpsServer.address();
|
|
1445
|
-
return address && typeof address !== "string" ? address.port : void 0;
|
|
1446
|
-
},
|
|
1447
|
-
applyTls: (nextMaterial) => {
|
|
1448
|
-
const server = httpsServer;
|
|
1449
|
-
server.setSecureContext?.({ key: nextMaterial.keyPem, cert: nextMaterial.certPem });
|
|
1450
|
-
},
|
|
1451
|
-
close: () => new Promise((resolve) => {
|
|
1452
|
-
for (const client of wss.clients) {
|
|
1453
|
-
client.terminate();
|
|
1454
|
-
}
|
|
1455
|
-
wss.close(() => {
|
|
1456
|
-
httpsServer.closeAllConnections();
|
|
1457
|
-
httpsServer.close(() => resolve());
|
|
1458
|
-
});
|
|
1459
|
-
})
|
|
1460
|
-
};
|
|
1461
|
-
};
|
|
1462
|
-
|
|
1463
|
-
// src/daemon/policy.ts
|
|
1464
|
-
var policyOverrideKey = (alias, toolName) => `${alias}/${toolName}`;
|
|
1465
|
-
var evaluate = (descriptor, session, policy) => {
|
|
1466
|
-
const override = policy.tools?.[policyOverrideKey(session.alias, descriptor.name)];
|
|
1467
|
-
if (override !== void 0) {
|
|
1468
|
-
return override;
|
|
1469
|
-
}
|
|
1470
|
-
if (descriptor.annotations?.destructiveHint === true) {
|
|
1471
|
-
return policy.destructive;
|
|
1472
|
-
}
|
|
1473
|
-
return policy.default;
|
|
1474
1955
|
};
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
}
|
|
1956
|
+
var closeServer = (wss, server) => {
|
|
1957
|
+
return new Promise((resolve2) => {
|
|
1958
|
+
for (const client of wss.clients) {
|
|
1959
|
+
client.terminate();
|
|
1960
|
+
}
|
|
1961
|
+
wss.close(() => {
|
|
1962
|
+
server.closeAllConnections();
|
|
1963
|
+
server.close(() => resolve2());
|
|
1964
|
+
});
|
|
1965
|
+
});
|
|
1485
1966
|
};
|
|
1486
|
-
var
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
} catch {
|
|
1490
|
-
return void 0;
|
|
1491
|
-
}
|
|
1967
|
+
var boundPort = (server) => {
|
|
1968
|
+
const address = server.address();
|
|
1969
|
+
return address && typeof address !== "string" ? address.port : void 0;
|
|
1492
1970
|
};
|
|
1493
|
-
var
|
|
1494
|
-
|
|
1495
|
-
if (status === void 0) {
|
|
1496
|
-
return false;
|
|
1497
|
-
}
|
|
1498
|
-
return /^State:\s*Z\b/mu.test(status);
|
|
1971
|
+
var isAddressInUse = (error) => {
|
|
1972
|
+
return error.code === "EADDRINUSE";
|
|
1499
1973
|
};
|
|
1500
|
-
var
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
return error.code === "EPERM";
|
|
1505
|
-
}
|
|
1506
|
-
return !isZombie(pid, readStatus);
|
|
1974
|
+
var portInUseError = (port) => {
|
|
1975
|
+
return new Error(
|
|
1976
|
+
`Port ${port} is already in use by another program. Stop that program, or set a different wssPort in config.json.`
|
|
1977
|
+
);
|
|
1507
1978
|
};
|
|
1508
|
-
var
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1979
|
+
var startListener = async (options) => {
|
|
1980
|
+
const timers = options.timers ?? systemTimers;
|
|
1981
|
+
const preClaimTimeoutMs = options.preClaimTimeoutMs ?? DEFAULT_PRE_CLAIM_TIMEOUT_MS;
|
|
1982
|
+
const material = options.tls.current();
|
|
1983
|
+
const httpsServer = createHttpsServer({ key: material.keyPem, cert: material.certPem });
|
|
1984
|
+
httpsServer.on("error", () => {
|
|
1985
|
+
});
|
|
1986
|
+
const wss = new WebSocketServer({ server: httpsServer, maxPayload: MAX_PAYLOAD_BYTES });
|
|
1987
|
+
wss.on("error", () => {
|
|
1988
|
+
});
|
|
1989
|
+
attachFrameGate(wss, { sessionManager: options.sessionManager, transport: "native", preClaimTimeoutMs, timers });
|
|
1990
|
+
let loopbackServer;
|
|
1991
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
1992
|
+
try {
|
|
1993
|
+
await listen(httpsServer, options.port);
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
throw isAddressInUse(error) ? portInUseError(options.port) : error;
|
|
1515
1996
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
1997
|
+
if (!options.bindLoopback) {
|
|
1998
|
+
break;
|
|
1999
|
+
}
|
|
2000
|
+
const port = boundPort(httpsServer);
|
|
2001
|
+
const candidate = createNetServer((socket) => httpsServer.emit("connection", socket));
|
|
2002
|
+
candidate.on("error", () => {
|
|
2003
|
+
});
|
|
1524
2004
|
try {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
} finally {
|
|
1529
|
-
await handle.close();
|
|
1530
|
-
}
|
|
1531
|
-
let released = false;
|
|
1532
|
-
return {
|
|
1533
|
-
pid,
|
|
1534
|
-
release: async () => {
|
|
1535
|
-
if (released) {
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
released = true;
|
|
1539
|
-
await rm2(pidFilePath, { force: true });
|
|
1540
|
-
}
|
|
1541
|
-
};
|
|
2005
|
+
await listen(candidate, port, "127.0.0.1");
|
|
2006
|
+
loopbackServer = candidate;
|
|
2007
|
+
break;
|
|
1542
2008
|
} catch (error) {
|
|
1543
|
-
|
|
2009
|
+
await new Promise((resolve2) => httpsServer.close(() => resolve2()));
|
|
2010
|
+
if (!isAddressInUse(error)) {
|
|
1544
2011
|
throw error;
|
|
1545
2012
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2013
|
+
if (options.port !== 0) {
|
|
2014
|
+
throw portInUseError(port);
|
|
2015
|
+
}
|
|
2016
|
+
if (attempt === MAX_PORT_ATTEMPTS) {
|
|
2017
|
+
throw new Error(`Could not find a free port: ${MAX_PORT_ATTEMPTS} ports in a row were in use on 127.0.0.1.`);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return {
|
|
2022
|
+
httpsServer,
|
|
2023
|
+
wss,
|
|
2024
|
+
port: () => boundPort(httpsServer),
|
|
2025
|
+
applyTls: (nextMaterial) => {
|
|
2026
|
+
const server = httpsServer;
|
|
2027
|
+
server.setSecureContext?.({ key: nextMaterial.keyPem, cert: nextMaterial.certPem });
|
|
2028
|
+
},
|
|
2029
|
+
close: () => {
|
|
2030
|
+
loopbackServer?.close();
|
|
2031
|
+
return closeServer(wss, httpsServer);
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
};
|
|
2035
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
2036
|
+
var isLoopbackOrigin = (origin) => {
|
|
2037
|
+
try {
|
|
2038
|
+
const url = new URL(origin);
|
|
2039
|
+
return (url.protocol === "http:" || url.protocol === "https:") && LOOPBACK_HOSTNAMES.has(url.hostname);
|
|
2040
|
+
} catch {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
var startWebListener = async (options) => {
|
|
2045
|
+
const timers = options.timers ?? systemTimers;
|
|
2046
|
+
const preClaimTimeoutMs = options.preClaimTimeoutMs ?? DEFAULT_PRE_CLAIM_TIMEOUT_MS;
|
|
2047
|
+
const allowed = new Set(options.origins);
|
|
2048
|
+
const httpServer = createHttpServer((_request, response) => {
|
|
2049
|
+
response.writeHead(426, { "content-type": "text/plain" });
|
|
2050
|
+
response.end("Appduct web listener: WebSocket upgrades only.");
|
|
2051
|
+
});
|
|
2052
|
+
httpServer.on("error", () => {
|
|
2053
|
+
});
|
|
2054
|
+
const wss = new WebSocketServer({
|
|
2055
|
+
server: httpServer,
|
|
2056
|
+
maxPayload: MAX_PAYLOAD_BYTES,
|
|
2057
|
+
verifyClient: ({ origin }, callback) => {
|
|
2058
|
+
if (origin === void 0 || isLoopbackOrigin(origin) || allowed.has(origin)) {
|
|
2059
|
+
callback(true);
|
|
2060
|
+
} else {
|
|
2061
|
+
callback(false, 403, "Forbidden");
|
|
1549
2062
|
}
|
|
1550
|
-
await rm2(pidFilePath, { force: true });
|
|
1551
|
-
await options.onStaleTakeover?.();
|
|
1552
2063
|
}
|
|
2064
|
+
});
|
|
2065
|
+
wss.on("error", () => {
|
|
2066
|
+
});
|
|
2067
|
+
attachFrameGate(wss, { sessionManager: options.sessionManager, transport: "web", preClaimTimeoutMs, timers });
|
|
2068
|
+
await listen(httpServer, 0, "127.0.0.1");
|
|
2069
|
+
return {
|
|
2070
|
+
port: () => boundPort(httpServer),
|
|
2071
|
+
close: () => closeServer(wss, httpServer)
|
|
2072
|
+
};
|
|
2073
|
+
};
|
|
2074
|
+
|
|
2075
|
+
// src/daemon/policy.ts
|
|
2076
|
+
var policyOverrideKey = (alias, toolName) => `${alias}/${toolName}`;
|
|
2077
|
+
var evaluate = (descriptor, session, policy) => {
|
|
2078
|
+
const override = policy.tools?.[policyOverrideKey(session.alias, descriptor.name)];
|
|
2079
|
+
if (override !== void 0) {
|
|
2080
|
+
return override;
|
|
1553
2081
|
}
|
|
1554
|
-
|
|
2082
|
+
if (descriptor.annotations?.destructiveHint === true) {
|
|
2083
|
+
return policy.destructive;
|
|
2084
|
+
}
|
|
2085
|
+
return policy.default;
|
|
1555
2086
|
};
|
|
1556
2087
|
|
|
1557
2088
|
// src/daemon/node-append-only-file.ts
|
|
@@ -1587,7 +2118,7 @@ var NodeAppendOnlyFile = class {
|
|
|
1587
2118
|
|
|
1588
2119
|
// src/daemon/rpc-server.ts
|
|
1589
2120
|
import { createServer } from "node:net";
|
|
1590
|
-
import { chmod as
|
|
2121
|
+
import { chmod as chmod3, rm as rm3 } from "node:fs/promises";
|
|
1591
2122
|
var MAX_LINE_BYTES = 1024 * 1024;
|
|
1592
2123
|
var MAX_NOTIFY_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
1593
2124
|
var isPlainObject = (value) => {
|
|
@@ -1725,20 +2256,20 @@ var startRpcServer = async (options) => {
|
|
|
1725
2256
|
});
|
|
1726
2257
|
server.on("error", () => {
|
|
1727
2258
|
});
|
|
1728
|
-
await new Promise((
|
|
2259
|
+
await new Promise((resolve2, reject) => {
|
|
1729
2260
|
const onError = (error) => {
|
|
1730
2261
|
server.off("listening", onListening);
|
|
1731
2262
|
reject(error);
|
|
1732
2263
|
};
|
|
1733
2264
|
const onListening = () => {
|
|
1734
2265
|
server.off("error", onError);
|
|
1735
|
-
|
|
2266
|
+
resolve2();
|
|
1736
2267
|
};
|
|
1737
2268
|
server.once("error", onError);
|
|
1738
2269
|
server.once("listening", onListening);
|
|
1739
2270
|
server.listen(options.socketPath);
|
|
1740
2271
|
});
|
|
1741
|
-
await
|
|
2272
|
+
await chmod3(options.socketPath, 384);
|
|
1742
2273
|
return {
|
|
1743
2274
|
server,
|
|
1744
2275
|
notify: (connection, payload) => {
|
|
@@ -1752,42 +2283,15 @@ var startRpcServer = async (options) => {
|
|
|
1752
2283
|
writeLine(connection.socket, { jsonrpc: "2.0", method: "event", params: payload });
|
|
1753
2284
|
},
|
|
1754
2285
|
connections: () => Array.from(connections.values()),
|
|
1755
|
-
close: () => new Promise((
|
|
2286
|
+
close: () => new Promise((resolve2) => {
|
|
1756
2287
|
for (const connection of connections.values()) {
|
|
1757
2288
|
connection.socket.destroy();
|
|
1758
2289
|
}
|
|
1759
|
-
server.close(() =>
|
|
2290
|
+
server.close(() => resolve2());
|
|
1760
2291
|
})
|
|
1761
2292
|
};
|
|
1762
2293
|
};
|
|
1763
2294
|
|
|
1764
|
-
// src/daemon/state-dir.ts
|
|
1765
|
-
import { join as join4 } from "node:path";
|
|
1766
|
-
import { mkdir as mkdir2, chmod as chmod3 } from "node:fs/promises";
|
|
1767
|
-
var getStateDirPaths = (stateDir) => {
|
|
1768
|
-
return {
|
|
1769
|
-
root: stateDir,
|
|
1770
|
-
socketPath: join4(stateDir, "daemon.sock"),
|
|
1771
|
-
pidFilePath: join4(stateDir, "daemon.pid"),
|
|
1772
|
-
logFilePath: join4(stateDir, "daemon.log"),
|
|
1773
|
-
eventsLogPath: join4(stateDir, "events.log"),
|
|
1774
|
-
keyPath: join4(stateDir, "key.pem"),
|
|
1775
|
-
configPath: join4(stateDir, "config.json"),
|
|
1776
|
-
auditDir: join4(stateDir, "audit"),
|
|
1777
|
-
spawnLockPath: join4(stateDir, "daemon.spawn.lock")
|
|
1778
|
-
};
|
|
1779
|
-
};
|
|
1780
|
-
var getSocketPath = (paths) => {
|
|
1781
|
-
return paths.socketPath;
|
|
1782
|
-
};
|
|
1783
|
-
var ensureStateDir = async (stateDir) => {
|
|
1784
|
-
const paths = getStateDirPaths(stateDir);
|
|
1785
|
-
await mkdir2(paths.auditDir, { recursive: true });
|
|
1786
|
-
await chmod3(paths.root, 448);
|
|
1787
|
-
await chmod3(paths.auditDir, 448);
|
|
1788
|
-
return paths;
|
|
1789
|
-
};
|
|
1790
|
-
|
|
1791
2295
|
// src/daemon/tls.ts
|
|
1792
2296
|
import { stat as stat3, readFile as readFile3 } from "node:fs/promises";
|
|
1793
2297
|
|
|
@@ -1965,7 +2469,7 @@ var generateHostCertificate = async (keyPem, advertisedIp) => {
|
|
|
1965
2469
|
|
|
1966
2470
|
// src/key-material.ts
|
|
1967
2471
|
import { generateKeyPairSync } from "node:crypto";
|
|
1968
|
-
import { dirname as
|
|
2472
|
+
import { dirname as dirname3 } from "node:path";
|
|
1969
2473
|
import { mkdir as mkdir3, rename as rename2, rm as rm4, writeFile } from "node:fs/promises";
|
|
1970
2474
|
var generatePrivateKeyPem = () => {
|
|
1971
2475
|
const { privateKey } = generateKeyPairSync("rsa", {
|
|
@@ -1979,7 +2483,7 @@ var generatePrivateKeyPem = () => {
|
|
|
1979
2483
|
};
|
|
1980
2484
|
var writePrivateKeyAtomically = async (path, keyPem) => {
|
|
1981
2485
|
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1982
|
-
await mkdir3(
|
|
2486
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1983
2487
|
try {
|
|
1984
2488
|
await writeFile(temporaryPath, keyPem, {
|
|
1985
2489
|
encoding: "utf8",
|
|
@@ -2052,13 +2556,14 @@ var toAgentEndpoint = (address, port) => {
|
|
|
2052
2556
|
var KNOWN_EVENT_KINDS = new Set(EVENT_KINDS);
|
|
2053
2557
|
var packageVersion = () => getDaemonReportedVersion();
|
|
2054
2558
|
var AUDIT_PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2055
|
-
var buildStatusResult = async (config, boundWssPort, startedAt, tls, sessionManager, auditLogger, auditDir) => {
|
|
2559
|
+
var buildStatusResult = async (config, boundWssPort, boundWebPort, startedAt, tls, sessionManager, auditLogger, auditDir) => {
|
|
2056
2560
|
const stats = await auditLogger.stats();
|
|
2057
2561
|
return {
|
|
2058
2562
|
version: packageVersion(),
|
|
2059
2563
|
pid: process.pid,
|
|
2060
2564
|
startedAt: startedAt.toISOString(),
|
|
2061
2565
|
wssPort: boundWssPort,
|
|
2566
|
+
webPort: boundWebPort,
|
|
2062
2567
|
pinnedKeys: tls.pinnedKeys(),
|
|
2063
2568
|
sessions: sessionManager.list(),
|
|
2064
2569
|
pendingLinks: sessionManager.pendingLinkCount(),
|
|
@@ -2272,11 +2777,51 @@ var asLinkCreateParams = (params) => {
|
|
|
2272
2777
|
if (addressOverride !== void 0 && (typeof addressOverride !== "string" || addressOverride.length === 0)) {
|
|
2273
2778
|
throw new RpcApplicationError("invalid_request", '"addressOverride" must be a non-empty string.');
|
|
2274
2779
|
}
|
|
2275
|
-
|
|
2780
|
+
const transport = record.transport;
|
|
2781
|
+
if (transport !== void 0 && transport !== "native" && transport !== "web") {
|
|
2782
|
+
throw new RpcApplicationError("invalid_request", '"transport" must be "native" or "web".');
|
|
2783
|
+
}
|
|
2784
|
+
return { ttlSeconds, addressOverride, transport };
|
|
2785
|
+
};
|
|
2786
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "[::1]", "localhost"]);
|
|
2787
|
+
var asWebAttachParams = (params) => {
|
|
2788
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
2789
|
+
throw new RpcApplicationError("invalid_request", "Params must be an object.");
|
|
2790
|
+
}
|
|
2791
|
+
const { url, browserUrl, targetId, ttlSeconds } = params;
|
|
2792
|
+
for (const [field, value] of [["url", url], ["browserUrl", browserUrl], ["targetId", targetId]]) {
|
|
2793
|
+
if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
|
|
2794
|
+
throw new RpcApplicationError("invalid_request", `"${field}" must be a non-empty string.`);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
if (url === void 0 || browserUrl === void 0) {
|
|
2798
|
+
throw new RpcApplicationError("invalid_request", '"url" and "browserUrl" are required.');
|
|
2799
|
+
}
|
|
2800
|
+
let parsedBrowserUrl;
|
|
2801
|
+
try {
|
|
2802
|
+
parsedBrowserUrl = new URL(browserUrl);
|
|
2803
|
+
} catch {
|
|
2804
|
+
}
|
|
2805
|
+
if (parsedBrowserUrl === void 0 || parsedBrowserUrl.protocol !== "http:" && parsedBrowserUrl.protocol !== "https:") {
|
|
2806
|
+
throw new RpcApplicationError("invalid_request", `"browserUrl" must be an http(s) URL such as http://127.0.0.1:9222 (got "${browserUrl}").`);
|
|
2807
|
+
}
|
|
2808
|
+
if (!LOOPBACK_HOSTS.has(parsedBrowserUrl.hostname)) {
|
|
2809
|
+
throw new RpcApplicationError(
|
|
2810
|
+
"invalid_request",
|
|
2811
|
+
`"browserUrl" must be a loopback address (127.0.0.1, [::1] or localhost), because a debugging port gives full control of the browser (got "${browserUrl}").`
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
if (ttlSeconds !== void 0 && (typeof ttlSeconds !== "number" || !Number.isInteger(ttlSeconds) || ttlSeconds <= 0)) {
|
|
2815
|
+
throw new RpcApplicationError("invalid_request", '"ttlSeconds" must be a positive integer.');
|
|
2816
|
+
}
|
|
2817
|
+
return { url, browserUrl, targetId, ttlSeconds };
|
|
2276
2818
|
};
|
|
2277
2819
|
var startDaemon = async (options) => {
|
|
2278
2820
|
const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
2279
2821
|
const timers = options.timers ?? systemTimers;
|
|
2822
|
+
const connectDevtoolsBrowser = options.connectDevtoolsBrowser ?? connectNodeDevtoolsBrowser;
|
|
2823
|
+
const devtoolsBrowsers = /* @__PURE__ */ new Map();
|
|
2824
|
+
const relayedTabs = /* @__PURE__ */ new Map();
|
|
2280
2825
|
const paths = getStateDirPaths(options.stateDir);
|
|
2281
2826
|
await ensureStateDir(options.stateDir);
|
|
2282
2827
|
const config = await loadConfig(paths, { warn: options.warn });
|
|
@@ -2299,15 +2844,17 @@ var startDaemon = async (options) => {
|
|
|
2299
2844
|
let pidfile;
|
|
2300
2845
|
let server;
|
|
2301
2846
|
let listener;
|
|
2847
|
+
let webListener;
|
|
2302
2848
|
let tlsManager;
|
|
2303
2849
|
let sessionManager;
|
|
2304
2850
|
let callsManager;
|
|
2305
2851
|
let eventBus;
|
|
2306
2852
|
let boundWssPort = config.wssPort;
|
|
2853
|
+
let boundWebPort = 0;
|
|
2307
2854
|
let shuttingDown = false;
|
|
2308
2855
|
let resolveExited;
|
|
2309
|
-
const exited = new Promise((
|
|
2310
|
-
resolveExited =
|
|
2856
|
+
const exited = new Promise((resolve2) => {
|
|
2857
|
+
resolveExited = resolve2;
|
|
2311
2858
|
});
|
|
2312
2859
|
const shutdown = async () => {
|
|
2313
2860
|
if (shuttingDown) {
|
|
@@ -2320,8 +2867,10 @@ var startDaemon = async (options) => {
|
|
|
2320
2867
|
auditPruneInterval = void 0;
|
|
2321
2868
|
}
|
|
2322
2869
|
callsManager?.disposeAll();
|
|
2870
|
+
devtoolsBrowsers.forEach((browser) => browser.close());
|
|
2323
2871
|
sessionManager?.disposeAll(1001, "daemon_shutdown");
|
|
2324
2872
|
await listener?.close();
|
|
2873
|
+
await webListener?.close();
|
|
2325
2874
|
await server?.close();
|
|
2326
2875
|
await pidfile?.release();
|
|
2327
2876
|
await rm5(getSocketPath(paths), { force: true });
|
|
@@ -2353,7 +2902,8 @@ var startDaemon = async (options) => {
|
|
|
2353
2902
|
linkTtlSeconds: config.linkTtlSeconds,
|
|
2354
2903
|
// `boundWssPort`, not `config.wssPort`: a link minted by a daemon on an OS-assigned port
|
|
2355
2904
|
// must advertise the port the app can actually reach, not the `0` that asked for one.
|
|
2356
|
-
|
|
2905
|
+
// A web link points at the loopback web listener, never the advertised LAN address.
|
|
2906
|
+
getEndpoint: (transport) => transport === "web" ? { family: 4, address: "127.0.0.1", port: boundWebPort } : toAgentEndpoint(tls.current().advertisedAddress, boundWssPort),
|
|
2357
2907
|
eventBus: activeEventBus,
|
|
2358
2908
|
clock,
|
|
2359
2909
|
onToolFrame: (message) => {
|
|
@@ -2386,11 +2936,14 @@ var startDaemon = async (options) => {
|
|
|
2386
2936
|
const activeCallsManager = callsManager;
|
|
2387
2937
|
listener = await startListener({
|
|
2388
2938
|
port: config.wssPort,
|
|
2939
|
+
bindLoopback: process.platform === "darwin",
|
|
2389
2940
|
tls,
|
|
2390
2941
|
sessionManager
|
|
2391
2942
|
});
|
|
2392
2943
|
const activeListener = listener;
|
|
2393
2944
|
boundWssPort = activeListener.port() ?? config.wssPort;
|
|
2945
|
+
webListener = await startWebListener({ sessionManager, origins: config.webOrigins, timers });
|
|
2946
|
+
boundWebPort = webListener.port() ?? 0;
|
|
2394
2947
|
activeEventBus.subscribe((event) => {
|
|
2395
2948
|
for (const connection of server?.connections() ?? []) {
|
|
2396
2949
|
const subscription = connection.state.eventSubscription;
|
|
@@ -2413,13 +2966,22 @@ var startDaemon = async (options) => {
|
|
|
2413
2966
|
server?.notify(connection, projected);
|
|
2414
2967
|
}
|
|
2415
2968
|
});
|
|
2969
|
+
const mintLink = async (ttlSeconds, addressOverride, transport) => {
|
|
2970
|
+
const previousMaterial = tls.current();
|
|
2971
|
+
const nextMaterial = await tls.refresh();
|
|
2972
|
+
if (nextMaterial !== previousMaterial) {
|
|
2973
|
+
activeListener.applyTls(nextMaterial);
|
|
2974
|
+
}
|
|
2975
|
+
return { ...activeSessionManager.createLink(ttlSeconds, addressOverride, transport), pin: tls.pinnedKeys()[0] };
|
|
2976
|
+
};
|
|
2416
2977
|
server = await startRpcServer({
|
|
2417
2978
|
socketPath: getSocketPath(paths),
|
|
2418
2979
|
dispatch: {
|
|
2419
|
-
[
|
|
2980
|
+
[RPC_METHODS3.daemonStatus]: async () => {
|
|
2420
2981
|
return buildStatusResult(
|
|
2421
2982
|
config,
|
|
2422
2983
|
boundWssPort,
|
|
2984
|
+
boundWebPort,
|
|
2423
2985
|
startedAt,
|
|
2424
2986
|
tls,
|
|
2425
2987
|
activeSessionManager,
|
|
@@ -2427,34 +2989,49 @@ var startDaemon = async (options) => {
|
|
|
2427
2989
|
paths.auditDir
|
|
2428
2990
|
);
|
|
2429
2991
|
},
|
|
2430
|
-
[
|
|
2992
|
+
[RPC_METHODS3.daemonShutdown]: (_params, context) => {
|
|
2431
2993
|
context.afterSend(() => {
|
|
2432
2994
|
void shutdown();
|
|
2433
2995
|
});
|
|
2434
2996
|
return { ok: true };
|
|
2435
2997
|
},
|
|
2436
|
-
[
|
|
2437
|
-
const { ttlSeconds, addressOverride } = asLinkCreateParams(params);
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2998
|
+
[RPC_METHODS3.linkCreate]: async (params) => {
|
|
2999
|
+
const { ttlSeconds, addressOverride, transport } = asLinkCreateParams(params);
|
|
3000
|
+
return mintLink(ttlSeconds, addressOverride, transport);
|
|
3001
|
+
},
|
|
3002
|
+
[RPC_METHODS3.webAttach]: async (params) => {
|
|
3003
|
+
const { url, browserUrl, targetId, ttlSeconds } = asWebAttachParams(params);
|
|
3004
|
+
let browser = devtoolsBrowsers.get(browserUrl);
|
|
3005
|
+
if (browser === void 0) {
|
|
3006
|
+
browser = connectDevtoolsBrowser(browserUrl);
|
|
3007
|
+
devtoolsBrowsers.set(browserUrl, browser);
|
|
3008
|
+
}
|
|
3009
|
+
try {
|
|
3010
|
+
return await attachBrowserTab({
|
|
3011
|
+
browser,
|
|
3012
|
+
relayedTabs,
|
|
3013
|
+
url,
|
|
3014
|
+
targetId,
|
|
3015
|
+
openDaemonSocket: openNodeDaemonSocket,
|
|
3016
|
+
mintLink: async () => composeWebLink(url, await mintLink(ttlSeconds, void 0, "web"))
|
|
3017
|
+
});
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
throw new RpcApplicationError("invalid_request", error instanceof Error ? error.message : String(error));
|
|
2442
3020
|
}
|
|
2443
|
-
return { ...activeSessionManager.createLink(ttlSeconds, addressOverride), pin: tls.pinnedKeys()[0] };
|
|
2444
3021
|
},
|
|
2445
|
-
[
|
|
3022
|
+
[RPC_METHODS3.sessionsList]: () => {
|
|
2446
3023
|
return activeSessionManager.list();
|
|
2447
3024
|
},
|
|
2448
|
-
[
|
|
3025
|
+
[RPC_METHODS3.sessionsDescribe]: (params) => {
|
|
2449
3026
|
const { selector } = asSelectorParams(params);
|
|
2450
3027
|
return activeSessionManager.describe(selector);
|
|
2451
3028
|
},
|
|
2452
|
-
[
|
|
3029
|
+
[RPC_METHODS3.sessionsRevoke]: (params) => {
|
|
2453
3030
|
const { selector } = asSelectorParams(params);
|
|
2454
3031
|
activeSessionManager.revoke(selector);
|
|
2455
3032
|
return { ok: true };
|
|
2456
3033
|
},
|
|
2457
|
-
[
|
|
3034
|
+
[RPC_METHODS3.eventsList]: (params) => {
|
|
2458
3035
|
const { selector, name, limit, offset } = asEventsListParams(params);
|
|
2459
3036
|
const registered = activeSessionManager.resolveForTools(selector).eventRegistry.list();
|
|
2460
3037
|
const sorted = registered.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
@@ -2465,7 +3042,7 @@ var startDaemon = async (options) => {
|
|
|
2465
3042
|
total: matching.length
|
|
2466
3043
|
};
|
|
2467
3044
|
},
|
|
2468
|
-
[
|
|
3045
|
+
[RPC_METHODS3.toolsList]: (params) => {
|
|
2469
3046
|
const { selector, group, filter, limit, offset } = asToolsListParams(params);
|
|
2470
3047
|
const resolved = activeSessionManager.resolveForTools(selector);
|
|
2471
3048
|
const entries = resolved.registry.list().map((descriptor) => ({
|
|
@@ -2490,7 +3067,7 @@ var startDaemon = async (options) => {
|
|
|
2490
3067
|
// This handler is the single seam every `tools.call` passes through: policy (ARCHITECTURE.md
|
|
2491
3068
|
// §12) is evaluated once the target tool descriptor is known, and one audit record is
|
|
2492
3069
|
// written for every attempt that reaches a resolved session, across ok/error/denied.
|
|
2493
|
-
[
|
|
3070
|
+
[RPC_METHODS3.toolsCall]: async (params, context) => {
|
|
2494
3071
|
const { selector, name, args, timeoutMs, caller, consent } = asToolsCallParams(params);
|
|
2495
3072
|
const effectiveCaller = caller ?? "cli";
|
|
2496
3073
|
const resolved = activeSessionManager.resolveForTools(selector);
|
|
@@ -2596,13 +3173,13 @@ var startDaemon = async (options) => {
|
|
|
2596
3173
|
unregisterOnClose();
|
|
2597
3174
|
}
|
|
2598
3175
|
},
|
|
2599
|
-
[
|
|
3176
|
+
[RPC_METHODS3.toolsCancel]: (params) => {
|
|
2600
3177
|
const { selector, callId, reason } = asToolsCancelParams(params);
|
|
2601
3178
|
const resolved = activeSessionManager.resolveForTools(selector);
|
|
2602
3179
|
const cancelled = activeCallsManager.cancel(resolved.sessionId, callId, reason ?? "client_cancelled");
|
|
2603
3180
|
return { cancelled };
|
|
2604
3181
|
},
|
|
2605
|
-
[
|
|
3182
|
+
[RPC_METHODS3.eventsSubscribe]: (params, context) => {
|
|
2606
3183
|
const { sessionSelector, kinds, name, payloadMaxBytes } = asEventsSubscribeParams(params);
|
|
2607
3184
|
context.connection.state.eventSubscription = {
|
|
2608
3185
|
sessionSelector,
|
|
@@ -2612,7 +3189,7 @@ var startDaemon = async (options) => {
|
|
|
2612
3189
|
};
|
|
2613
3190
|
return { ok: true };
|
|
2614
3191
|
},
|
|
2615
|
-
[
|
|
3192
|
+
[RPC_METHODS3.eventsSince]: (params) => {
|
|
2616
3193
|
const { selector, since, limit, name, payloadMaxBytes } = asEventsSinceParams(params);
|
|
2617
3194
|
const resolved = activeSessionManager.describe(selector);
|
|
2618
3195
|
return activeEventBus.since(resolved.sessionId, { since, limit, name, payloadMaxBytes });
|
|
@@ -2631,6 +3208,7 @@ var startDaemon = async (options) => {
|
|
|
2631
3208
|
callsManager?.disposeAll();
|
|
2632
3209
|
sessionManager?.disposeAll(1001, "daemon_startup_failed");
|
|
2633
3210
|
await listener?.close();
|
|
3211
|
+
await webListener?.close();
|
|
2634
3212
|
await pidfile?.release();
|
|
2635
3213
|
throw error;
|
|
2636
3214
|
}
|
|
@@ -2641,6 +3219,7 @@ var startDaemon = async (options) => {
|
|
|
2641
3219
|
startedAt,
|
|
2642
3220
|
server,
|
|
2643
3221
|
listener,
|
|
3222
|
+
webListener,
|
|
2644
3223
|
tls: tlsManager,
|
|
2645
3224
|
eventBus,
|
|
2646
3225
|
exited,
|
|
@@ -2648,175 +3227,6 @@ var startDaemon = async (options) => {
|
|
|
2648
3227
|
};
|
|
2649
3228
|
};
|
|
2650
3229
|
|
|
2651
|
-
// src/rpc/client.ts
|
|
2652
|
-
import { join as join5 } from "node:path";
|
|
2653
|
-
import { RPC_METHODS as RPC_METHODS2 } from "@appduct/shared";
|
|
2654
|
-
var defaultBinPath = join5(getPackageRoot(), "bin.js");
|
|
2655
|
-
var DaemonRpcError = class extends Error {
|
|
2656
|
-
constructor(code, message, data) {
|
|
2657
|
-
super(message);
|
|
2658
|
-
this.code = code;
|
|
2659
|
-
this.data = data;
|
|
2660
|
-
this.name = "DaemonRpcError";
|
|
2661
|
-
}
|
|
2662
|
-
};
|
|
2663
|
-
var DaemonUnavailableError = class extends Error {
|
|
2664
|
-
constructor(message, reason = "unreachable") {
|
|
2665
|
-
super(message);
|
|
2666
|
-
this.reason = reason;
|
|
2667
|
-
this.name = "DaemonUnavailableError";
|
|
2668
|
-
}
|
|
2669
|
-
};
|
|
2670
|
-
var describeLiveState = (state) => {
|
|
2671
|
-
const parts = [];
|
|
2672
|
-
if (state.sessions > 0) {
|
|
2673
|
-
parts.push(`${state.sessions} connected session(s)`);
|
|
2674
|
-
}
|
|
2675
|
-
if (state.pendingLinks > 0) {
|
|
2676
|
-
parts.push(`${state.pendingLinks} unclaimed link(s)`);
|
|
2677
|
-
}
|
|
2678
|
-
return parts.join(" and ");
|
|
2679
|
-
};
|
|
2680
|
-
var DaemonVersionMismatchError = class extends Error {
|
|
2681
|
-
constructor(daemonVersion, clientVersion, live, message) {
|
|
2682
|
-
super(
|
|
2683
|
-
message ?? `The running Appduct daemon is version ${daemonVersion}, but this client is version ${clientVersion}. It was not restarted automatically because ${describeLiveState(live)} would be lost. Run "appduct daemon stop" once that state is expendable, or force it with "--daemon-restart".`
|
|
2684
|
-
);
|
|
2685
|
-
this.daemonVersion = daemonVersion;
|
|
2686
|
-
this.clientVersion = clientVersion;
|
|
2687
|
-
this.name = "DaemonVersionMismatchError";
|
|
2688
|
-
this.sessionCount = live.sessions;
|
|
2689
|
-
this.pendingLinkCount = live.pendingLinks;
|
|
2690
|
-
}
|
|
2691
|
-
sessionCount;
|
|
2692
|
-
pendingLinkCount;
|
|
2693
|
-
};
|
|
2694
|
-
var isConnectionMissingError = (error) => {
|
|
2695
|
-
const code = error.code;
|
|
2696
|
-
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
2697
|
-
};
|
|
2698
|
-
var isDaemonUnreachableError = (error) => {
|
|
2699
|
-
return error instanceof DaemonUnavailableError || isConnectionMissingError(error);
|
|
2700
|
-
};
|
|
2701
|
-
|
|
2702
|
-
// src/errors.ts
|
|
2703
|
-
var EXIT_CODE_BY_ERROR_TYPE = {
|
|
2704
|
-
usage_error: 64,
|
|
2705
|
-
validation_error: 65,
|
|
2706
|
-
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
2707
|
-
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
2708
|
-
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
2709
|
-
// assertion failure (docs/tasks/08-appduct-doctor.md).
|
|
2710
|
-
inspection_error: 66,
|
|
2711
|
-
connection_error: 70,
|
|
2712
|
-
session_error: 71,
|
|
2713
|
-
tool_error: 72,
|
|
2714
|
-
permission_error: 77,
|
|
2715
|
-
internal_error: 1,
|
|
2716
|
-
// `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
|
|
2717
|
-
// assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
|
|
2718
|
-
// and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
|
|
2719
|
-
// conflate a normal assertion failure with an unexpected crash.
|
|
2720
|
-
assertion_error: 3
|
|
2721
|
-
};
|
|
2722
|
-
var RPC_ERROR_EXIT_CLASS = {
|
|
2723
|
-
no_session: "session_error",
|
|
2724
|
-
ambiguous_session: "session_error",
|
|
2725
|
-
unknown_session: "session_error",
|
|
2726
|
-
session_not_active: "session_error",
|
|
2727
|
-
session_suspended: "session_error",
|
|
2728
|
-
tool_not_found: "tool_error",
|
|
2729
|
-
tool_input_validation_error: "tool_error",
|
|
2730
|
-
tool_output_validation_error: "tool_error",
|
|
2731
|
-
tool_execution_error: "tool_error",
|
|
2732
|
-
tool_serialization_error: "tool_error",
|
|
2733
|
-
tool_timeout: "tool_error",
|
|
2734
|
-
tool_cancelled: "tool_error",
|
|
2735
|
-
policy_denied: "permission_error",
|
|
2736
|
-
invalid_request: "validation_error"
|
|
2737
|
-
};
|
|
2738
|
-
var AppductCliError = class extends Error {
|
|
2739
|
-
type;
|
|
2740
|
-
details;
|
|
2741
|
-
constructor(type, message, details) {
|
|
2742
|
-
super(message);
|
|
2743
|
-
this.name = "AppductCliError";
|
|
2744
|
-
this.type = type;
|
|
2745
|
-
this.details = details;
|
|
2746
|
-
}
|
|
2747
|
-
};
|
|
2748
|
-
var connectionError = (message, details) => {
|
|
2749
|
-
return new AppductCliError("connection_error", message, details);
|
|
2750
|
-
};
|
|
2751
|
-
var isAppductCliError = (value) => {
|
|
2752
|
-
return value instanceof AppductCliError;
|
|
2753
|
-
};
|
|
2754
|
-
var rpcErrorClass = (error) => {
|
|
2755
|
-
const type = error.data?.type;
|
|
2756
|
-
return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
|
|
2757
|
-
};
|
|
2758
|
-
var toCliError = (error) => {
|
|
2759
|
-
if (isAppductCliError(error)) {
|
|
2760
|
-
return {
|
|
2761
|
-
type: error.type,
|
|
2762
|
-
message: error.message,
|
|
2763
|
-
details: error.details
|
|
2764
|
-
};
|
|
2765
|
-
}
|
|
2766
|
-
if (error instanceof DaemonVersionMismatchError) {
|
|
2767
|
-
return {
|
|
2768
|
-
type: "connection_error",
|
|
2769
|
-
message: error.message,
|
|
2770
|
-
details: {
|
|
2771
|
-
daemon_version: error.daemonVersion,
|
|
2772
|
-
client_version: error.clientVersion,
|
|
2773
|
-
session_count: error.sessionCount,
|
|
2774
|
-
pending_link_count: error.pendingLinkCount
|
|
2775
|
-
}
|
|
2776
|
-
};
|
|
2777
|
-
}
|
|
2778
|
-
if (error instanceof DaemonRpcError && error.data?.type) {
|
|
2779
|
-
return {
|
|
2780
|
-
type: error.data.type,
|
|
2781
|
-
message: error.message,
|
|
2782
|
-
details: error.data.details
|
|
2783
|
-
};
|
|
2784
|
-
}
|
|
2785
|
-
if (isDaemonUnreachableError(error)) {
|
|
2786
|
-
return {
|
|
2787
|
-
type: "connection_error",
|
|
2788
|
-
message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
|
|
2789
|
-
};
|
|
2790
|
-
}
|
|
2791
|
-
if (error instanceof Error) {
|
|
2792
|
-
return {
|
|
2793
|
-
type: "internal_error",
|
|
2794
|
-
message: error.message
|
|
2795
|
-
};
|
|
2796
|
-
}
|
|
2797
|
-
return {
|
|
2798
|
-
type: "internal_error",
|
|
2799
|
-
message: "An unexpected error occurred.",
|
|
2800
|
-
details: error
|
|
2801
|
-
};
|
|
2802
|
-
};
|
|
2803
|
-
var getExitCodeForError = (error) => {
|
|
2804
|
-
if (isAppductCliError(error)) {
|
|
2805
|
-
return EXIT_CODE_BY_ERROR_TYPE[error.type];
|
|
2806
|
-
}
|
|
2807
|
-
if (error instanceof DaemonVersionMismatchError) {
|
|
2808
|
-
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
2809
|
-
}
|
|
2810
|
-
if (error instanceof DaemonRpcError) {
|
|
2811
|
-
const cliErrorClass = rpcErrorClass(error);
|
|
2812
|
-
return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
|
|
2813
|
-
}
|
|
2814
|
-
if (isDaemonUnreachableError(error)) {
|
|
2815
|
-
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
2816
|
-
}
|
|
2817
|
-
return EXIT_CODE_BY_ERROR_TYPE.internal_error;
|
|
2818
|
-
};
|
|
2819
|
-
|
|
2820
3230
|
// src/commands/daemon/run.ts
|
|
2821
3231
|
var handleDaemonRunCommand = async (context) => {
|
|
2822
3232
|
let daemon;
|
|
@@ -3195,6 +3605,36 @@ var renderInvokeData = (colors, data, flags) => {
|
|
|
3195
3605
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
3196
3606
|
};
|
|
3197
3607
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
3608
|
+
if ("attached" in data) {
|
|
3609
|
+
return [
|
|
3610
|
+
colors.green("Page Attached"),
|
|
3611
|
+
...renderFields(
|
|
3612
|
+
"Page",
|
|
3613
|
+
[
|
|
3614
|
+
["Session", data.sessionId],
|
|
3615
|
+
["Tab", data.targetId],
|
|
3616
|
+
["URL", data.url],
|
|
3617
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
3618
|
+
],
|
|
3619
|
+
flags
|
|
3620
|
+
)
|
|
3621
|
+
];
|
|
3622
|
+
}
|
|
3623
|
+
if ("script" in data) {
|
|
3624
|
+
return [
|
|
3625
|
+
colors.green("Link Created"),
|
|
3626
|
+
...renderFields(
|
|
3627
|
+
"Link",
|
|
3628
|
+
[
|
|
3629
|
+
["Session", data.sessionId],
|
|
3630
|
+
["URL", data.url],
|
|
3631
|
+
["Script", data.script],
|
|
3632
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
3633
|
+
],
|
|
3634
|
+
flags
|
|
3635
|
+
)
|
|
3636
|
+
];
|
|
3637
|
+
}
|
|
3198
3638
|
const lines = [
|
|
3199
3639
|
colors.green("Link Created"),
|
|
3200
3640
|
...renderFields(
|
|
@@ -3315,6 +3755,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
3315
3755
|
["PID", data.daemon.pid],
|
|
3316
3756
|
["Started at", data.daemon.started_at],
|
|
3317
3757
|
["WSS port", data.daemon.wss_port],
|
|
3758
|
+
["Web port", data.daemon.web_port],
|
|
3318
3759
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
3319
3760
|
["Sessions", data.daemon.session_count]
|
|
3320
3761
|
],
|