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