appduct 0.13.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 +55 -24
- package/dist/artifact-inspect.d.ts +38 -39
- package/dist/bin.js +106 -7
- package/dist/cli/result-types.d.ts +35 -2
- package/dist/cli/routes/daemon/index.js +90 -2
- package/dist/cli/routes/daemon/run.js +1088 -517
- package/dist/cli/routes/daemon/start.js +98 -2
- package/dist/cli/routes/daemon/status.js +99 -2
- package/dist/cli/routes/daemon/stop.js +98 -2
- package/dist/cli/routes/doctor.js +98 -3
- package/dist/cli/routes/events/index.d.ts +0 -3
- package/dist/cli/routes/events/index.js +92 -3
- package/dist/cli/routes/events/ls.d.ts +5 -0
- package/dist/cli/routes/events/ls.js +1837 -0
- package/dist/cli/routes/events/since.js +98 -2
- package/dist/cli/routes/events/tail.js +98 -2
- package/dist/cli/routes/init.js +265 -108
- package/dist/cli/routes/keygen.js +90 -2
- package/dist/cli/routes/mcp.js +243 -25
- package/dist/cli/routes/sessions/index.js +90 -2
- package/dist/cli/routes/sessions/link.js +199 -22
- package/dist/cli/routes/sessions/ls.js +98 -2
- package/dist/cli/routes/sessions/revoke.js +98 -2
- package/dist/cli/routes/tools/call.js +98 -2
- package/dist/cli/routes/tools/describe.js +98 -2
- package/dist/cli/routes/tools/index.js +90 -2
- package/dist/cli/routes/tools/ls.js +98 -2
- 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/events-ls.d.ts +21 -0
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/link.d.ts +10 -1
- package/dist/daemon/calls.d.ts +3 -1
- package/dist/daemon/config.d.ts +6 -0
- package/dist/daemon/daemon.d.ts +6 -1
- package/dist/daemon/event-bus.d.ts +10 -0
- package/dist/daemon/links.d.ts +9 -5
- package/dist/daemon/listener.d.ts +28 -1
- package/dist/daemon/registry.d.ts +20 -12
- package/dist/daemon/rpc-errors.d.ts +6 -0
- package/dist/daemon/sessions.d.ts +14 -8
- 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 +106 -7
- package/dist/link.d.ts +35 -1
- package/dist/mcp/app-tools.d.ts +19 -0
- package/dist/mcp/connect-tool.d.ts +15 -2
- package/dist/mcp/events-tool.d.ts +39 -0
- 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"
|
|
@@ -265,43 +345,461 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
265
345
|
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
266
346
|
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
267
347
|
}
|
|
268
|
-
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
269
|
-
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
270
|
-
parsed.restartDaemonOnVersionMismatch,
|
|
271
|
-
"restartDaemonOnVersionMismatch"
|
|
272
|
-
);
|
|
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
|
+
};
|
|
693
|
+
}
|
|
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];
|
|
703
|
+
}
|
|
704
|
+
if (error instanceof DaemonVersionMismatchError) {
|
|
705
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
706
|
+
}
|
|
707
|
+
if (error instanceof DaemonRpcError) {
|
|
708
|
+
const cliErrorClass = rpcErrorClass(error);
|
|
709
|
+
return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
|
|
710
|
+
}
|
|
711
|
+
if (isDaemonUnreachableError(error)) {
|
|
712
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
713
|
+
}
|
|
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;
|
|
753
|
+
}
|
|
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
|
}
|
|
@@ -484,6 +982,8 @@ var RpcApplicationError = class extends Error {
|
|
|
484
982
|
this.name = "RpcApplicationError";
|
|
485
983
|
}
|
|
486
984
|
};
|
|
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.`;
|
|
487
987
|
|
|
488
988
|
// src/daemon/timers.ts
|
|
489
989
|
var systemTimers = {
|
|
@@ -504,6 +1004,7 @@ var clampTimeout = (timeoutMs) => {
|
|
|
504
1004
|
};
|
|
505
1005
|
var TERMINATING_KIND_ERROR_TYPES = {
|
|
506
1006
|
session_suspended: "session_suspended",
|
|
1007
|
+
session_resumed: "session_suspended",
|
|
507
1008
|
session_revoked: "unknown_session",
|
|
508
1009
|
session_expired: "unknown_session"
|
|
509
1010
|
};
|
|
@@ -525,7 +1026,7 @@ var createCallsManager = (options) => {
|
|
|
525
1026
|
};
|
|
526
1027
|
const call = (session, name, args, timeoutMs) => {
|
|
527
1028
|
const callId = generateCallId();
|
|
528
|
-
const result = new Promise((
|
|
1029
|
+
const result = new Promise((resolve2, reject) => {
|
|
529
1030
|
const effectiveTimeoutMs = clampTimeout(timeoutMs);
|
|
530
1031
|
const timer = timers.setTimeout(() => {
|
|
531
1032
|
const pending = takePending(session.sessionId, callId);
|
|
@@ -538,7 +1039,7 @@ var createCallsManager = (options) => {
|
|
|
538
1039
|
sessionMap = /* @__PURE__ */ new Map();
|
|
539
1040
|
pendingBySession.set(session.sessionId, sessionMap);
|
|
540
1041
|
}
|
|
541
|
-
sessionMap.set(callId, { alias: session.alias, resolve, reject, timer });
|
|
1042
|
+
sessionMap.set(callId, { alias: session.alias, resolve: resolve2, reject, timer });
|
|
542
1043
|
const message = {
|
|
543
1044
|
type: "tool_call",
|
|
544
1045
|
session_id: session.sessionId,
|
|
@@ -586,7 +1087,7 @@ var createCallsManager = (options) => {
|
|
|
586
1087
|
const message = { type: "tool_cancel", session_id: sessionId, id: callId, reason };
|
|
587
1088
|
return options.sendCancel(sessionId, message);
|
|
588
1089
|
};
|
|
589
|
-
const rejectSession = (sessionId, errorType) => {
|
|
1090
|
+
const rejectSession = (sessionId, errorType, options2 = {}) => {
|
|
590
1091
|
const sessionMap = pendingBySession.get(sessionId);
|
|
591
1092
|
if (!sessionMap) {
|
|
592
1093
|
return;
|
|
@@ -594,13 +1095,20 @@ var createCallsManager = (options) => {
|
|
|
594
1095
|
pendingBySession.delete(sessionId);
|
|
595
1096
|
for (const pending of sessionMap.values()) {
|
|
596
1097
|
timers.clearTimeout(pending.timer);
|
|
597
|
-
pending.reject(
|
|
1098
|
+
pending.reject(
|
|
1099
|
+
new RpcApplicationError(
|
|
1100
|
+
errorType,
|
|
1101
|
+
options2.message?.(pending.alias) ?? "Session transitioned while the call was pending."
|
|
1102
|
+
)
|
|
1103
|
+
);
|
|
598
1104
|
}
|
|
599
1105
|
};
|
|
600
1106
|
const unsubscribeFromEventBus = options.eventBus.subscribe((event) => {
|
|
601
1107
|
const errorType = TERMINATING_KIND_ERROR_TYPES[event.kind];
|
|
602
1108
|
if (errorType && event.sessionId) {
|
|
603
|
-
|
|
1109
|
+
const backgrounded = event.kind === "session_suspended" && event.data.reason === "app_backgrounded";
|
|
1110
|
+
const message = event.kind === "session_resumed" ? appReconnectedMessage : backgrounded ? appBackgroundedMessage : void 0;
|
|
1111
|
+
rejectSession(event.sessionId, errorType, { message });
|
|
604
1112
|
}
|
|
605
1113
|
});
|
|
606
1114
|
const disposeAll = () => {
|
|
@@ -781,8 +1289,14 @@ var createEventsLog = (options) => {
|
|
|
781
1289
|
};
|
|
782
1290
|
|
|
783
1291
|
// src/daemon/listener.ts
|
|
1292
|
+
import { createServer as createHttpServer } from "node:http";
|
|
784
1293
|
import { createServer as createHttpsServer } from "node:https";
|
|
785
|
-
import {
|
|
1294
|
+
import { createServer as createNetServer } from "node:net";
|
|
1295
|
+
import {
|
|
1296
|
+
isSessionClaimMessage,
|
|
1297
|
+
isSessionResumeMessage,
|
|
1298
|
+
isSessionBoundMessage
|
|
1299
|
+
} from "@appduct/shared";
|
|
786
1300
|
import { WebSocketServer } from "ws";
|
|
787
1301
|
|
|
788
1302
|
// src/daemon/sessions.ts
|
|
@@ -791,6 +1305,8 @@ import {
|
|
|
791
1305
|
isEventMessage,
|
|
792
1306
|
isToolCallProgressMessage,
|
|
793
1307
|
isToolErrorMessage,
|
|
1308
|
+
isEventRegistryDeltaMessage,
|
|
1309
|
+
isEventRegistrySnapshotMessage,
|
|
794
1310
|
isToolRegistryDeltaMessage,
|
|
795
1311
|
isToolRegistrySnapshotMessage,
|
|
796
1312
|
isToolResultMessage,
|
|
@@ -821,13 +1337,13 @@ var createPendingLinkRegistry = (options) => {
|
|
|
821
1337
|
options.eventBus.drop(sessionId);
|
|
822
1338
|
};
|
|
823
1339
|
return {
|
|
824
|
-
create: (ttlSeconds, addressOverride) => {
|
|
1340
|
+
create: (ttlSeconds, addressOverride, transport = "native") => {
|
|
825
1341
|
const sessionId = generateSessionId();
|
|
826
1342
|
const token = randomBytes2(TOKEN_BYTES);
|
|
827
1343
|
const createdAt = options.clock.now();
|
|
828
1344
|
const expiresAt = new Date(createdAt.getTime() + ttlSeconds * 1e3);
|
|
829
|
-
const detectedEndpoint = options.getEndpoint();
|
|
830
|
-
const endpoint = addressOverride ? {
|
|
1345
|
+
const detectedEndpoint = options.getEndpoint(transport);
|
|
1346
|
+
const endpoint = addressOverride && transport === "native" ? {
|
|
831
1347
|
family: addressOverride.includes(":") ? 6 : 4,
|
|
832
1348
|
address: addressOverride,
|
|
833
1349
|
port: detectedEndpoint.port
|
|
@@ -839,7 +1355,7 @@ var createPendingLinkRegistry = (options) => {
|
|
|
839
1355
|
record.timer = options.timers.setTimeout(() => discard(sessionId), ttlSeconds * 1e3);
|
|
840
1356
|
}
|
|
841
1357
|
}, ttlSeconds * 1e3);
|
|
842
|
-
const link = { sessionId, token, createdAt, expiresAt };
|
|
1358
|
+
const link = { sessionId, token, createdAt, expiresAt, transport };
|
|
843
1359
|
records.set(sessionId, { link, attempts: 0, timer });
|
|
844
1360
|
const deepLinkPayload = encodeBootstrap({
|
|
845
1361
|
...endpoint,
|
|
@@ -888,7 +1404,7 @@ var createPendingLinkRegistry = (options) => {
|
|
|
888
1404
|
};
|
|
889
1405
|
|
|
890
1406
|
// src/daemon/registry.ts
|
|
891
|
-
var
|
|
1407
|
+
var createRegistry = (onChange) => {
|
|
892
1408
|
let tools = /* @__PURE__ */ new Map();
|
|
893
1409
|
const notify = () => {
|
|
894
1410
|
onChange?.();
|
|
@@ -918,6 +1434,8 @@ var RESUME_TOKEN_BYTES = 32;
|
|
|
918
1434
|
var POST_CLAIM_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
919
1435
|
"tool_registry_snapshot",
|
|
920
1436
|
"tool_registry_delta",
|
|
1437
|
+
"event_registry_snapshot",
|
|
1438
|
+
"event_registry_delta",
|
|
921
1439
|
"tool_result",
|
|
922
1440
|
"tool_error",
|
|
923
1441
|
"tool_call_progress",
|
|
@@ -988,6 +1506,7 @@ var createSessionManager = (options) => {
|
|
|
988
1506
|
createdAt: session.createdAt.toISOString(),
|
|
989
1507
|
claimedAt: session.claimedAt.toISOString(),
|
|
990
1508
|
...session.suspendedAt ? { suspendedAt: session.suspendedAt.toISOString() } : {},
|
|
1509
|
+
...session.suspendReason ? { suspendReason: session.suspendReason } : {},
|
|
991
1510
|
toolCount: session.registry.count()
|
|
992
1511
|
};
|
|
993
1512
|
};
|
|
@@ -1024,7 +1543,8 @@ var createSessionManager = (options) => {
|
|
|
1024
1543
|
alias: session.alias,
|
|
1025
1544
|
resume_token: session.resumeToken.toString("base64url"),
|
|
1026
1545
|
keepalive_interval_s: options.keepaliveIntervalSeconds,
|
|
1027
|
-
grace_s: options.graceSeconds
|
|
1546
|
+
grace_s: options.graceSeconds,
|
|
1547
|
+
event_registry: true
|
|
1028
1548
|
};
|
|
1029
1549
|
socket.send(JSON.stringify(ack), (error) => {
|
|
1030
1550
|
if (error) {
|
|
@@ -1032,17 +1552,18 @@ var createSessionManager = (options) => {
|
|
|
1032
1552
|
}
|
|
1033
1553
|
});
|
|
1034
1554
|
};
|
|
1035
|
-
const suspend = (sessionId) => {
|
|
1555
|
+
const suspend = (sessionId, reason) => {
|
|
1036
1556
|
const session = sessions.get(sessionId);
|
|
1037
1557
|
if (!session || session.state !== "active") {
|
|
1038
1558
|
return;
|
|
1039
1559
|
}
|
|
1040
1560
|
session.state = "suspended";
|
|
1041
1561
|
session.suspendedAt = clock.now();
|
|
1562
|
+
session.suspendReason = reason;
|
|
1042
1563
|
session.socket = void 0;
|
|
1043
1564
|
session.missedPongs = 0;
|
|
1044
1565
|
session.graceTimer = timers.setTimeout(() => expire(sessionId), options.graceSeconds * 1e3);
|
|
1045
|
-
emit("session_suspended", session.sessionId, session.alias, {});
|
|
1566
|
+
emit("session_suspended", session.sessionId, session.alias, { reason });
|
|
1046
1567
|
};
|
|
1047
1568
|
const expire = (sessionId) => {
|
|
1048
1569
|
const session = sessions.get(sessionId);
|
|
@@ -1059,9 +1580,10 @@ var createSessionManager = (options) => {
|
|
|
1059
1580
|
socket.on("pong", () => {
|
|
1060
1581
|
session.missedPongs = 0;
|
|
1061
1582
|
});
|
|
1062
|
-
socket.on("close", () => {
|
|
1583
|
+
socket.on("close", (code, reason) => {
|
|
1063
1584
|
if (session.socket === socket) {
|
|
1064
|
-
|
|
1585
|
+
const backgrounded = code === 1001 && reason.toString("utf8") === "app_backgrounded";
|
|
1586
|
+
suspend(session.sessionId, backgrounded ? "app_backgrounded" : "connection_lost");
|
|
1065
1587
|
}
|
|
1066
1588
|
});
|
|
1067
1589
|
};
|
|
@@ -1073,7 +1595,7 @@ var createSessionManager = (options) => {
|
|
|
1073
1595
|
if (session.missedPongs >= 2) {
|
|
1074
1596
|
const staleSocket = session.socket;
|
|
1075
1597
|
session.socket = void 0;
|
|
1076
|
-
suspend(session.sessionId);
|
|
1598
|
+
suspend(session.sessionId, "connection_lost");
|
|
1077
1599
|
staleSocket.terminate();
|
|
1078
1600
|
continue;
|
|
1079
1601
|
}
|
|
@@ -1084,10 +1606,11 @@ var createSessionManager = (options) => {
|
|
|
1084
1606
|
}
|
|
1085
1607
|
}
|
|
1086
1608
|
}, options.keepaliveIntervalSeconds * 1e3);
|
|
1087
|
-
const createLink = (ttlSeconds, addressOverride) => {
|
|
1609
|
+
const createLink = (ttlSeconds, addressOverride, transport) => {
|
|
1088
1610
|
const { link, deepLinkPayload, endpoint } = pendingLinks.create(
|
|
1089
1611
|
ttlSeconds ?? options.linkTtlSeconds,
|
|
1090
|
-
addressOverride
|
|
1612
|
+
addressOverride,
|
|
1613
|
+
transport
|
|
1091
1614
|
);
|
|
1092
1615
|
return {
|
|
1093
1616
|
sessionId: link.sessionId,
|
|
@@ -1096,7 +1619,7 @@ var createSessionManager = (options) => {
|
|
|
1096
1619
|
expiresAt: Math.floor(link.expiresAt.getTime() / 1e3)
|
|
1097
1620
|
};
|
|
1098
1621
|
};
|
|
1099
|
-
const handleClaim = (socket, message) => {
|
|
1622
|
+
const handleClaim = (socket, message, transport) => {
|
|
1100
1623
|
const pending = pendingLinks.get(message.session_id);
|
|
1101
1624
|
if (!pending) {
|
|
1102
1625
|
if (sessions.has(message.session_id)) {
|
|
@@ -1106,6 +1629,10 @@ var createSessionManager = (options) => {
|
|
|
1106
1629
|
}
|
|
1107
1630
|
return null;
|
|
1108
1631
|
}
|
|
1632
|
+
if (pending.transport !== transport) {
|
|
1633
|
+
closeSocket(socket, 1008, "wrong_transport");
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1109
1636
|
const nowMs = clock.now().getTime();
|
|
1110
1637
|
if (pending.expiresAt.getTime() <= nowMs) {
|
|
1111
1638
|
pendingLinks.discard(pending.sessionId);
|
|
@@ -1135,11 +1662,15 @@ var createSessionManager = (options) => {
|
|
|
1135
1662
|
device,
|
|
1136
1663
|
createdAt: pending.createdAt,
|
|
1137
1664
|
claimedAt: clock.now(),
|
|
1665
|
+
transport,
|
|
1138
1666
|
resumeToken: randomBytes3(RESUME_TOKEN_BYTES),
|
|
1139
1667
|
socket,
|
|
1140
1668
|
missedPongs: 0,
|
|
1141
|
-
registry:
|
|
1669
|
+
registry: createRegistry(() => {
|
|
1142
1670
|
emit("tools_changed", sessionId, alias, { toolCount: session.registry.count() });
|
|
1671
|
+
}),
|
|
1672
|
+
eventRegistry: createRegistry(() => {
|
|
1673
|
+
emit("events_changed", sessionId, alias, { eventCount: session.eventRegistry.count() });
|
|
1143
1674
|
})
|
|
1144
1675
|
};
|
|
1145
1676
|
sessions.set(sessionId, session);
|
|
@@ -1148,12 +1679,16 @@ var createSessionManager = (options) => {
|
|
|
1148
1679
|
emit("session_claimed", sessionId, alias, { device });
|
|
1149
1680
|
return sessionId;
|
|
1150
1681
|
};
|
|
1151
|
-
const handleResume = (socket, message) => {
|
|
1682
|
+
const handleResume = (socket, message, transport) => {
|
|
1152
1683
|
const session = sessions.get(message.session_id);
|
|
1153
1684
|
if (!session) {
|
|
1154
1685
|
closeSocket(socket, 1008, "unknown_session");
|
|
1155
1686
|
return null;
|
|
1156
1687
|
}
|
|
1688
|
+
if (session.transport !== transport) {
|
|
1689
|
+
closeSocket(socket, 1008, "wrong_transport");
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1157
1692
|
const candidateToken = decodeFixedLengthToken(message.resume_token, session.resumeToken.length);
|
|
1158
1693
|
if (!tokensMatch(candidateToken, session.resumeToken)) {
|
|
1159
1694
|
closeSocket(socket, 1008, "invalid_resume_token");
|
|
@@ -1171,6 +1706,7 @@ var createSessionManager = (options) => {
|
|
|
1171
1706
|
session.resumeToken = randomBytes3(RESUME_TOKEN_BYTES);
|
|
1172
1707
|
session.state = "active";
|
|
1173
1708
|
session.suspendedAt = void 0;
|
|
1709
|
+
session.suspendReason = void 0;
|
|
1174
1710
|
session.socket = socket;
|
|
1175
1711
|
session.missedPongs = 0;
|
|
1176
1712
|
attachSocketHandlers(session);
|
|
@@ -1184,6 +1720,9 @@ var createSessionManager = (options) => {
|
|
|
1184
1720
|
closeSocket(socket, 1008, "unknown_session");
|
|
1185
1721
|
return;
|
|
1186
1722
|
}
|
|
1723
|
+
if (socket !== session.socket) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1187
1726
|
switch (message.type) {
|
|
1188
1727
|
case "tool_registry_snapshot": {
|
|
1189
1728
|
if (!isToolRegistrySnapshotMessage(message)) {
|
|
@@ -1205,6 +1744,26 @@ var createSessionManager = (options) => {
|
|
|
1205
1744
|
}
|
|
1206
1745
|
return;
|
|
1207
1746
|
}
|
|
1747
|
+
case "event_registry_snapshot": {
|
|
1748
|
+
if (!isEventRegistrySnapshotMessage(message)) {
|
|
1749
|
+
closeSocket(socket, 1008, "invalid_registry");
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
session.eventRegistry.snapshot(message.events);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
case "event_registry_delta": {
|
|
1756
|
+
if (!isEventRegistryDeltaMessage(message)) {
|
|
1757
|
+
closeSocket(socket, 1008, "invalid_registry");
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (message.operation === "upsert") {
|
|
1761
|
+
session.eventRegistry.upsert(message.event);
|
|
1762
|
+
} else {
|
|
1763
|
+
session.eventRegistry.remove(message.name);
|
|
1764
|
+
}
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1208
1767
|
case "tool_result": {
|
|
1209
1768
|
if (!isToolResultMessage(message)) {
|
|
1210
1769
|
closeSocket(socket, 1008, "invalid_message");
|
|
@@ -1272,7 +1831,14 @@ var createSessionManager = (options) => {
|
|
|
1272
1831
|
};
|
|
1273
1832
|
const resolveForTools = (selector) => {
|
|
1274
1833
|
const session = resolveSession(selector);
|
|
1275
|
-
return {
|
|
1834
|
+
return {
|
|
1835
|
+
sessionId: session.sessionId,
|
|
1836
|
+
alias: session.alias,
|
|
1837
|
+
state: session.state,
|
|
1838
|
+
suspendReason: session.suspendReason,
|
|
1839
|
+
registry: session.registry,
|
|
1840
|
+
eventRegistry: session.eventRegistry
|
|
1841
|
+
};
|
|
1276
1842
|
};
|
|
1277
1843
|
const sendToApp = (sessionId, message) => {
|
|
1278
1844
|
const session = sessions.get(sessionId);
|
|
@@ -1306,19 +1872,12 @@ var createSessionManager = (options) => {
|
|
|
1306
1872
|
// src/daemon/listener.ts
|
|
1307
1873
|
var DEFAULT_PRE_CLAIM_TIMEOUT_MS = 1e4;
|
|
1308
1874
|
var MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
1875
|
+
var MAX_PORT_ATTEMPTS = 5;
|
|
1309
1876
|
var isRecord = (value) => {
|
|
1310
1877
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1311
1878
|
};
|
|
1312
|
-
var
|
|
1313
|
-
const timers = options
|
|
1314
|
-
const preClaimTimeoutMs = options.preClaimTimeoutMs ?? DEFAULT_PRE_CLAIM_TIMEOUT_MS;
|
|
1315
|
-
const material = options.tls.current();
|
|
1316
|
-
const httpsServer = createHttpsServer({ key: material.keyPem, cert: material.certPem });
|
|
1317
|
-
httpsServer.on("error", () => {
|
|
1318
|
-
});
|
|
1319
|
-
const wss = new WebSocketServer({ server: httpsServer, maxPayload: MAX_PAYLOAD_BYTES });
|
|
1320
|
-
wss.on("error", () => {
|
|
1321
|
-
});
|
|
1879
|
+
var attachFrameGate = (wss, options) => {
|
|
1880
|
+
const { timers, preClaimTimeoutMs } = options;
|
|
1322
1881
|
wss.on("connection", (socket) => {
|
|
1323
1882
|
socket.on("error", () => {
|
|
1324
1883
|
});
|
|
@@ -1349,7 +1908,7 @@ var startListener = async (options) => {
|
|
|
1349
1908
|
}
|
|
1350
1909
|
if (claimedSessionId === null) {
|
|
1351
1910
|
if (isSessionClaimMessage(parsed)) {
|
|
1352
|
-
const result = options.sessionManager.handleClaim(socket, parsed);
|
|
1911
|
+
const result = options.sessionManager.handleClaim(socket, parsed, options.transport);
|
|
1353
1912
|
if (result) {
|
|
1354
1913
|
claimedSessionId = result;
|
|
1355
1914
|
timers.clearTimeout(preClaimTimer);
|
|
@@ -1357,7 +1916,7 @@ var startListener = async (options) => {
|
|
|
1357
1916
|
return;
|
|
1358
1917
|
}
|
|
1359
1918
|
if (isSessionResumeMessage(parsed)) {
|
|
1360
|
-
const result = options.sessionManager.handleResume(socket, parsed);
|
|
1919
|
+
const result = options.sessionManager.handleResume(socket, parsed, options.transport);
|
|
1361
1920
|
if (result) {
|
|
1362
1921
|
claimedSessionId = result;
|
|
1363
1922
|
timers.clearTimeout(preClaimTimer);
|
|
@@ -1378,39 +1937,138 @@ var startListener = async (options) => {
|
|
|
1378
1937
|
options.sessionManager.handlePostClaimMessage(claimedSessionId, socket, parsed);
|
|
1379
1938
|
});
|
|
1380
1939
|
});
|
|
1381
|
-
|
|
1940
|
+
};
|
|
1941
|
+
var listen = (server, port, host) => {
|
|
1942
|
+
return new Promise((resolve2, reject) => {
|
|
1382
1943
|
const onError = (error) => {
|
|
1383
|
-
|
|
1944
|
+
server.off("listening", onListening);
|
|
1384
1945
|
reject(error);
|
|
1385
1946
|
};
|
|
1386
1947
|
const onListening = () => {
|
|
1387
|
-
|
|
1388
|
-
|
|
1948
|
+
server.off("error", onError);
|
|
1949
|
+
resolve2();
|
|
1389
1950
|
};
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1951
|
+
server.once("error", onError);
|
|
1952
|
+
server.once("listening", onListening);
|
|
1953
|
+
server.listen(port, host);
|
|
1954
|
+
});
|
|
1955
|
+
};
|
|
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
|
+
});
|
|
1966
|
+
};
|
|
1967
|
+
var boundPort = (server) => {
|
|
1968
|
+
const address = server.address();
|
|
1969
|
+
return address && typeof address !== "string" ? address.port : void 0;
|
|
1970
|
+
};
|
|
1971
|
+
var isAddressInUse = (error) => {
|
|
1972
|
+
return error.code === "EADDRINUSE";
|
|
1973
|
+
};
|
|
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
|
+
);
|
|
1978
|
+
};
|
|
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", () => {
|
|
1393
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;
|
|
1996
|
+
}
|
|
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
|
+
});
|
|
2004
|
+
try {
|
|
2005
|
+
await listen(candidate, port, "127.0.0.1");
|
|
2006
|
+
loopbackServer = candidate;
|
|
2007
|
+
break;
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
await new Promise((resolve2) => httpsServer.close(() => resolve2()));
|
|
2010
|
+
if (!isAddressInUse(error)) {
|
|
2011
|
+
throw error;
|
|
2012
|
+
}
|
|
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
|
+
}
|
|
1394
2021
|
return {
|
|
1395
2022
|
httpsServer,
|
|
1396
2023
|
wss,
|
|
1397
|
-
port: () =>
|
|
1398
|
-
const address = httpsServer.address();
|
|
1399
|
-
return address && typeof address !== "string" ? address.port : void 0;
|
|
1400
|
-
},
|
|
2024
|
+
port: () => boundPort(httpsServer),
|
|
1401
2025
|
applyTls: (nextMaterial) => {
|
|
1402
2026
|
const server = httpsServer;
|
|
1403
2027
|
server.setSecureContext?.({ key: nextMaterial.keyPem, cert: nextMaterial.certPem });
|
|
1404
2028
|
},
|
|
1405
|
-
close: () =>
|
|
1406
|
-
|
|
1407
|
-
|
|
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");
|
|
1408
2062
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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)
|
|
1414
2072
|
};
|
|
1415
2073
|
};
|
|
1416
2074
|
|
|
@@ -1424,88 +2082,7 @@ var evaluate = (descriptor, session, policy) => {
|
|
|
1424
2082
|
if (descriptor.annotations?.destructiveHint === true) {
|
|
1425
2083
|
return policy.destructive;
|
|
1426
2084
|
}
|
|
1427
|
-
return policy.default;
|
|
1428
|
-
};
|
|
1429
|
-
|
|
1430
|
-
// src/daemon/pidfile.ts
|
|
1431
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1432
|
-
import { open as open2, readFile as readFile2, rm as rm2 } from "node:fs/promises";
|
|
1433
|
-
var DaemonAlreadyRunningError = class extends Error {
|
|
1434
|
-
constructor(pid) {
|
|
1435
|
-
super(`Appduct daemon is already running (pid ${pid}).`);
|
|
1436
|
-
this.pid = pid;
|
|
1437
|
-
this.name = "DaemonAlreadyRunningError";
|
|
1438
|
-
}
|
|
1439
|
-
};
|
|
1440
|
-
var readProcStatus = (pid) => {
|
|
1441
|
-
try {
|
|
1442
|
-
return readFileSync3(`/proc/${pid}/status`, "utf8");
|
|
1443
|
-
} catch {
|
|
1444
|
-
return void 0;
|
|
1445
|
-
}
|
|
1446
|
-
};
|
|
1447
|
-
var isZombie = (pid, readStatus) => {
|
|
1448
|
-
const status = readStatus(pid);
|
|
1449
|
-
if (status === void 0) {
|
|
1450
|
-
return false;
|
|
1451
|
-
}
|
|
1452
|
-
return /^State:\s*Z\b/mu.test(status);
|
|
1453
|
-
};
|
|
1454
|
-
var isProcessAlive = (pid, readStatus = readProcStatus) => {
|
|
1455
|
-
try {
|
|
1456
|
-
process.kill(pid, 0);
|
|
1457
|
-
} catch (error) {
|
|
1458
|
-
return error.code === "EPERM";
|
|
1459
|
-
}
|
|
1460
|
-
return !isZombie(pid, readStatus);
|
|
1461
|
-
};
|
|
1462
|
-
var readPidFromFile = async (pidFilePath) => {
|
|
1463
|
-
let contents;
|
|
1464
|
-
try {
|
|
1465
|
-
contents = await readFile2(pidFilePath, "utf8");
|
|
1466
|
-
} catch (error) {
|
|
1467
|
-
if (error.code === "ENOENT") {
|
|
1468
|
-
return void 0;
|
|
1469
|
-
}
|
|
1470
|
-
throw error;
|
|
1471
|
-
}
|
|
1472
|
-
const pid = Number.parseInt(contents.trim(), 10);
|
|
1473
|
-
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
1474
|
-
};
|
|
1475
|
-
var acquirePidfile = async (pidFilePath, options = {}) => {
|
|
1476
|
-
const pid = options.pid ?? process.pid;
|
|
1477
|
-
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
1478
|
-
try {
|
|
1479
|
-
const handle = await open2(pidFilePath, "wx", 384);
|
|
1480
|
-
try {
|
|
1481
|
-
await handle.writeFile(String(pid), "utf8");
|
|
1482
|
-
} finally {
|
|
1483
|
-
await handle.close();
|
|
1484
|
-
}
|
|
1485
|
-
let released = false;
|
|
1486
|
-
return {
|
|
1487
|
-
pid,
|
|
1488
|
-
release: async () => {
|
|
1489
|
-
if (released) {
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
released = true;
|
|
1493
|
-
await rm2(pidFilePath, { force: true });
|
|
1494
|
-
}
|
|
1495
|
-
};
|
|
1496
|
-
} catch (error) {
|
|
1497
|
-
if (error.code !== "EEXIST") {
|
|
1498
|
-
throw error;
|
|
1499
|
-
}
|
|
1500
|
-
const existingPid = await readPidFromFile(pidFilePath);
|
|
1501
|
-
if (existingPid !== void 0 && isProcessAlive(existingPid)) {
|
|
1502
|
-
throw new DaemonAlreadyRunningError(existingPid);
|
|
1503
|
-
}
|
|
1504
|
-
await rm2(pidFilePath, { force: true });
|
|
1505
|
-
await options.onStaleTakeover?.();
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
throw new Error(`Failed to acquire the Appduct daemon pidfile at "${pidFilePath}".`);
|
|
2085
|
+
return policy.default;
|
|
1509
2086
|
};
|
|
1510
2087
|
|
|
1511
2088
|
// src/daemon/node-append-only-file.ts
|
|
@@ -1541,7 +2118,7 @@ var NodeAppendOnlyFile = class {
|
|
|
1541
2118
|
|
|
1542
2119
|
// src/daemon/rpc-server.ts
|
|
1543
2120
|
import { createServer } from "node:net";
|
|
1544
|
-
import { chmod as
|
|
2121
|
+
import { chmod as chmod3, rm as rm3 } from "node:fs/promises";
|
|
1545
2122
|
var MAX_LINE_BYTES = 1024 * 1024;
|
|
1546
2123
|
var MAX_NOTIFY_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
1547
2124
|
var isPlainObject = (value) => {
|
|
@@ -1679,20 +2256,20 @@ var startRpcServer = async (options) => {
|
|
|
1679
2256
|
});
|
|
1680
2257
|
server.on("error", () => {
|
|
1681
2258
|
});
|
|
1682
|
-
await new Promise((
|
|
2259
|
+
await new Promise((resolve2, reject) => {
|
|
1683
2260
|
const onError = (error) => {
|
|
1684
2261
|
server.off("listening", onListening);
|
|
1685
2262
|
reject(error);
|
|
1686
2263
|
};
|
|
1687
2264
|
const onListening = () => {
|
|
1688
2265
|
server.off("error", onError);
|
|
1689
|
-
|
|
2266
|
+
resolve2();
|
|
1690
2267
|
};
|
|
1691
2268
|
server.once("error", onError);
|
|
1692
2269
|
server.once("listening", onListening);
|
|
1693
2270
|
server.listen(options.socketPath);
|
|
1694
2271
|
});
|
|
1695
|
-
await
|
|
2272
|
+
await chmod3(options.socketPath, 384);
|
|
1696
2273
|
return {
|
|
1697
2274
|
server,
|
|
1698
2275
|
notify: (connection, payload) => {
|
|
@@ -1706,42 +2283,15 @@ var startRpcServer = async (options) => {
|
|
|
1706
2283
|
writeLine(connection.socket, { jsonrpc: "2.0", method: "event", params: payload });
|
|
1707
2284
|
},
|
|
1708
2285
|
connections: () => Array.from(connections.values()),
|
|
1709
|
-
close: () => new Promise((
|
|
2286
|
+
close: () => new Promise((resolve2) => {
|
|
1710
2287
|
for (const connection of connections.values()) {
|
|
1711
2288
|
connection.socket.destroy();
|
|
1712
2289
|
}
|
|
1713
|
-
server.close(() =>
|
|
2290
|
+
server.close(() => resolve2());
|
|
1714
2291
|
})
|
|
1715
2292
|
};
|
|
1716
2293
|
};
|
|
1717
2294
|
|
|
1718
|
-
// src/daemon/state-dir.ts
|
|
1719
|
-
import { join as join4 } from "node:path";
|
|
1720
|
-
import { mkdir as mkdir2, chmod as chmod3 } from "node:fs/promises";
|
|
1721
|
-
var getStateDirPaths = (stateDir) => {
|
|
1722
|
-
return {
|
|
1723
|
-
root: stateDir,
|
|
1724
|
-
socketPath: join4(stateDir, "daemon.sock"),
|
|
1725
|
-
pidFilePath: join4(stateDir, "daemon.pid"),
|
|
1726
|
-
logFilePath: join4(stateDir, "daemon.log"),
|
|
1727
|
-
eventsLogPath: join4(stateDir, "events.log"),
|
|
1728
|
-
keyPath: join4(stateDir, "key.pem"),
|
|
1729
|
-
configPath: join4(stateDir, "config.json"),
|
|
1730
|
-
auditDir: join4(stateDir, "audit"),
|
|
1731
|
-
spawnLockPath: join4(stateDir, "daemon.spawn.lock")
|
|
1732
|
-
};
|
|
1733
|
-
};
|
|
1734
|
-
var getSocketPath = (paths) => {
|
|
1735
|
-
return paths.socketPath;
|
|
1736
|
-
};
|
|
1737
|
-
var ensureStateDir = async (stateDir) => {
|
|
1738
|
-
const paths = getStateDirPaths(stateDir);
|
|
1739
|
-
await mkdir2(paths.auditDir, { recursive: true });
|
|
1740
|
-
await chmod3(paths.root, 448);
|
|
1741
|
-
await chmod3(paths.auditDir, 448);
|
|
1742
|
-
return paths;
|
|
1743
|
-
};
|
|
1744
|
-
|
|
1745
2295
|
// src/daemon/tls.ts
|
|
1746
2296
|
import { stat as stat3, readFile as readFile3 } from "node:fs/promises";
|
|
1747
2297
|
|
|
@@ -1919,7 +2469,7 @@ var generateHostCertificate = async (keyPem, advertisedIp) => {
|
|
|
1919
2469
|
|
|
1920
2470
|
// src/key-material.ts
|
|
1921
2471
|
import { generateKeyPairSync } from "node:crypto";
|
|
1922
|
-
import { dirname as
|
|
2472
|
+
import { dirname as dirname3 } from "node:path";
|
|
1923
2473
|
import { mkdir as mkdir3, rename as rename2, rm as rm4, writeFile } from "node:fs/promises";
|
|
1924
2474
|
var generatePrivateKeyPem = () => {
|
|
1925
2475
|
const { privateKey } = generateKeyPairSync("rsa", {
|
|
@@ -1933,7 +2483,7 @@ var generatePrivateKeyPem = () => {
|
|
|
1933
2483
|
};
|
|
1934
2484
|
var writePrivateKeyAtomically = async (path, keyPem) => {
|
|
1935
2485
|
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1936
|
-
await mkdir3(
|
|
2486
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1937
2487
|
try {
|
|
1938
2488
|
await writeFile(temporaryPath, keyPem, {
|
|
1939
2489
|
encoding: "utf8",
|
|
@@ -2006,13 +2556,14 @@ var toAgentEndpoint = (address, port) => {
|
|
|
2006
2556
|
var KNOWN_EVENT_KINDS = new Set(EVENT_KINDS);
|
|
2007
2557
|
var packageVersion = () => getDaemonReportedVersion();
|
|
2008
2558
|
var AUDIT_PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2009
|
-
var buildStatusResult = async (config, boundWssPort, startedAt, tls, sessionManager, auditLogger, auditDir) => {
|
|
2559
|
+
var buildStatusResult = async (config, boundWssPort, boundWebPort, startedAt, tls, sessionManager, auditLogger, auditDir) => {
|
|
2010
2560
|
const stats = await auditLogger.stats();
|
|
2011
2561
|
return {
|
|
2012
2562
|
version: packageVersion(),
|
|
2013
2563
|
pid: process.pid,
|
|
2014
2564
|
startedAt: startedAt.toISOString(),
|
|
2015
2565
|
wssPort: boundWssPort,
|
|
2566
|
+
webPort: boundWebPort,
|
|
2016
2567
|
pinnedKeys: tls.pinnedKeys(),
|
|
2017
2568
|
sessions: sessionManager.list(),
|
|
2018
2569
|
pendingLinks: sessionManager.pendingLinkCount(),
|
|
@@ -2040,6 +2591,26 @@ var asSelectorParams = (params) => {
|
|
|
2040
2591
|
}
|
|
2041
2592
|
return { selector };
|
|
2042
2593
|
};
|
|
2594
|
+
var asPageParams = (record) => {
|
|
2595
|
+
const limit = record.limit;
|
|
2596
|
+
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
|
|
2597
|
+
throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
|
|
2598
|
+
}
|
|
2599
|
+
const offset = record.offset;
|
|
2600
|
+
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) {
|
|
2601
|
+
throw new RpcApplicationError("invalid_request", '"offset" must be a non-negative integer.');
|
|
2602
|
+
}
|
|
2603
|
+
return { limit, offset };
|
|
2604
|
+
};
|
|
2605
|
+
var asEventsListParams = (params) => {
|
|
2606
|
+
const { selector } = asSelectorParams(params);
|
|
2607
|
+
const record = asRecordParams(params);
|
|
2608
|
+
const name = record.name;
|
|
2609
|
+
if (name !== void 0 && typeof name !== "string") {
|
|
2610
|
+
throw new RpcApplicationError("invalid_request", '"name" must be a string.');
|
|
2611
|
+
}
|
|
2612
|
+
return { selector, name, ...asPageParams(record) };
|
|
2613
|
+
};
|
|
2043
2614
|
var asToolsListParams = (params) => {
|
|
2044
2615
|
const { selector } = asSelectorParams(params);
|
|
2045
2616
|
const record = asRecordParams(params);
|
|
@@ -2057,14 +2628,7 @@ var asToolsListParams = (params) => {
|
|
|
2057
2628
|
`"filter" must be a string of at most ${MAX_TOOLS_FILTER_LENGTH} characters.`
|
|
2058
2629
|
);
|
|
2059
2630
|
}
|
|
2060
|
-
const limit = record
|
|
2061
|
-
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
|
|
2062
|
-
throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
|
|
2063
|
-
}
|
|
2064
|
-
const offset = record.offset;
|
|
2065
|
-
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) {
|
|
2066
|
-
throw new RpcApplicationError("invalid_request", '"offset" must be a non-negative integer.');
|
|
2067
|
-
}
|
|
2631
|
+
const { limit, offset } = asPageParams(record);
|
|
2068
2632
|
return {
|
|
2069
2633
|
selector,
|
|
2070
2634
|
group,
|
|
@@ -2213,11 +2777,51 @@ var asLinkCreateParams = (params) => {
|
|
|
2213
2777
|
if (addressOverride !== void 0 && (typeof addressOverride !== "string" || addressOverride.length === 0)) {
|
|
2214
2778
|
throw new RpcApplicationError("invalid_request", '"addressOverride" must be a non-empty string.');
|
|
2215
2779
|
}
|
|
2216
|
-
|
|
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 };
|
|
2217
2818
|
};
|
|
2218
2819
|
var startDaemon = async (options) => {
|
|
2219
2820
|
const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
2220
2821
|
const timers = options.timers ?? systemTimers;
|
|
2822
|
+
const connectDevtoolsBrowser = options.connectDevtoolsBrowser ?? connectNodeDevtoolsBrowser;
|
|
2823
|
+
const devtoolsBrowsers = /* @__PURE__ */ new Map();
|
|
2824
|
+
const relayedTabs = /* @__PURE__ */ new Map();
|
|
2221
2825
|
const paths = getStateDirPaths(options.stateDir);
|
|
2222
2826
|
await ensureStateDir(options.stateDir);
|
|
2223
2827
|
const config = await loadConfig(paths, { warn: options.warn });
|
|
@@ -2240,15 +2844,17 @@ var startDaemon = async (options) => {
|
|
|
2240
2844
|
let pidfile;
|
|
2241
2845
|
let server;
|
|
2242
2846
|
let listener;
|
|
2847
|
+
let webListener;
|
|
2243
2848
|
let tlsManager;
|
|
2244
2849
|
let sessionManager;
|
|
2245
2850
|
let callsManager;
|
|
2246
2851
|
let eventBus;
|
|
2247
2852
|
let boundWssPort = config.wssPort;
|
|
2853
|
+
let boundWebPort = 0;
|
|
2248
2854
|
let shuttingDown = false;
|
|
2249
2855
|
let resolveExited;
|
|
2250
|
-
const exited = new Promise((
|
|
2251
|
-
resolveExited =
|
|
2856
|
+
const exited = new Promise((resolve2) => {
|
|
2857
|
+
resolveExited = resolve2;
|
|
2252
2858
|
});
|
|
2253
2859
|
const shutdown = async () => {
|
|
2254
2860
|
if (shuttingDown) {
|
|
@@ -2261,8 +2867,10 @@ var startDaemon = async (options) => {
|
|
|
2261
2867
|
auditPruneInterval = void 0;
|
|
2262
2868
|
}
|
|
2263
2869
|
callsManager?.disposeAll();
|
|
2870
|
+
devtoolsBrowsers.forEach((browser) => browser.close());
|
|
2264
2871
|
sessionManager?.disposeAll(1001, "daemon_shutdown");
|
|
2265
2872
|
await listener?.close();
|
|
2873
|
+
await webListener?.close();
|
|
2266
2874
|
await server?.close();
|
|
2267
2875
|
await pidfile?.release();
|
|
2268
2876
|
await rm5(getSocketPath(paths), { force: true });
|
|
@@ -2294,7 +2902,8 @@ var startDaemon = async (options) => {
|
|
|
2294
2902
|
linkTtlSeconds: config.linkTtlSeconds,
|
|
2295
2903
|
// `boundWssPort`, not `config.wssPort`: a link minted by a daemon on an OS-assigned port
|
|
2296
2904
|
// must advertise the port the app can actually reach, not the `0` that asked for one.
|
|
2297
|
-
|
|
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),
|
|
2298
2907
|
eventBus: activeEventBus,
|
|
2299
2908
|
clock,
|
|
2300
2909
|
onToolFrame: (message) => {
|
|
@@ -2327,11 +2936,14 @@ var startDaemon = async (options) => {
|
|
|
2327
2936
|
const activeCallsManager = callsManager;
|
|
2328
2937
|
listener = await startListener({
|
|
2329
2938
|
port: config.wssPort,
|
|
2939
|
+
bindLoopback: process.platform === "darwin",
|
|
2330
2940
|
tls,
|
|
2331
2941
|
sessionManager
|
|
2332
2942
|
});
|
|
2333
2943
|
const activeListener = listener;
|
|
2334
2944
|
boundWssPort = activeListener.port() ?? config.wssPort;
|
|
2945
|
+
webListener = await startWebListener({ sessionManager, origins: config.webOrigins, timers });
|
|
2946
|
+
boundWebPort = webListener.port() ?? 0;
|
|
2335
2947
|
activeEventBus.subscribe((event) => {
|
|
2336
2948
|
for (const connection of server?.connections() ?? []) {
|
|
2337
2949
|
const subscription = connection.state.eventSubscription;
|
|
@@ -2354,13 +2966,22 @@ var startDaemon = async (options) => {
|
|
|
2354
2966
|
server?.notify(connection, projected);
|
|
2355
2967
|
}
|
|
2356
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
|
+
};
|
|
2357
2977
|
server = await startRpcServer({
|
|
2358
2978
|
socketPath: getSocketPath(paths),
|
|
2359
2979
|
dispatch: {
|
|
2360
|
-
[
|
|
2980
|
+
[RPC_METHODS3.daemonStatus]: async () => {
|
|
2361
2981
|
return buildStatusResult(
|
|
2362
2982
|
config,
|
|
2363
2983
|
boundWssPort,
|
|
2984
|
+
boundWebPort,
|
|
2364
2985
|
startedAt,
|
|
2365
2986
|
tls,
|
|
2366
2987
|
activeSessionManager,
|
|
@@ -2368,34 +2989,60 @@ var startDaemon = async (options) => {
|
|
|
2368
2989
|
paths.auditDir
|
|
2369
2990
|
);
|
|
2370
2991
|
},
|
|
2371
|
-
[
|
|
2992
|
+
[RPC_METHODS3.daemonShutdown]: (_params, context) => {
|
|
2372
2993
|
context.afterSend(() => {
|
|
2373
2994
|
void shutdown();
|
|
2374
2995
|
});
|
|
2375
2996
|
return { ok: true };
|
|
2376
2997
|
},
|
|
2377
|
-
[
|
|
2378
|
-
const { ttlSeconds, addressOverride } = asLinkCreateParams(params);
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
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));
|
|
2383
3020
|
}
|
|
2384
|
-
return { ...activeSessionManager.createLink(ttlSeconds, addressOverride), pin: tls.pinnedKeys()[0] };
|
|
2385
3021
|
},
|
|
2386
|
-
[
|
|
3022
|
+
[RPC_METHODS3.sessionsList]: () => {
|
|
2387
3023
|
return activeSessionManager.list();
|
|
2388
3024
|
},
|
|
2389
|
-
[
|
|
3025
|
+
[RPC_METHODS3.sessionsDescribe]: (params) => {
|
|
2390
3026
|
const { selector } = asSelectorParams(params);
|
|
2391
3027
|
return activeSessionManager.describe(selector);
|
|
2392
3028
|
},
|
|
2393
|
-
[
|
|
3029
|
+
[RPC_METHODS3.sessionsRevoke]: (params) => {
|
|
2394
3030
|
const { selector } = asSelectorParams(params);
|
|
2395
3031
|
activeSessionManager.revoke(selector);
|
|
2396
3032
|
return { ok: true };
|
|
2397
3033
|
},
|
|
2398
|
-
[
|
|
3034
|
+
[RPC_METHODS3.eventsList]: (params) => {
|
|
3035
|
+
const { selector, name, limit, offset } = asEventsListParams(params);
|
|
3036
|
+
const registered = activeSessionManager.resolveForTools(selector).eventRegistry.list();
|
|
3037
|
+
const sorted = registered.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
3038
|
+
const matching = name === void 0 ? sorted : sorted.filter((event) => matchesNameGlob(name, event.name));
|
|
3039
|
+
const start = offset ?? 0;
|
|
3040
|
+
return {
|
|
3041
|
+
events: limit === void 0 ? matching.slice(start) : matching.slice(start, start + limit),
|
|
3042
|
+
total: matching.length
|
|
3043
|
+
};
|
|
3044
|
+
},
|
|
3045
|
+
[RPC_METHODS3.toolsList]: (params) => {
|
|
2399
3046
|
const { selector, group, filter, limit, offset } = asToolsListParams(params);
|
|
2400
3047
|
const resolved = activeSessionManager.resolveForTools(selector);
|
|
2401
3048
|
const entries = resolved.registry.list().map((descriptor) => ({
|
|
@@ -2420,7 +3067,7 @@ var startDaemon = async (options) => {
|
|
|
2420
3067
|
// This handler is the single seam every `tools.call` passes through: policy (ARCHITECTURE.md
|
|
2421
3068
|
// §12) is evaluated once the target tool descriptor is known, and one audit record is
|
|
2422
3069
|
// written for every attempt that reaches a resolved session, across ok/error/denied.
|
|
2423
|
-
[
|
|
3070
|
+
[RPC_METHODS3.toolsCall]: async (params, context) => {
|
|
2424
3071
|
const { selector, name, args, timeoutMs, caller, consent } = asToolsCallParams(params);
|
|
2425
3072
|
const effectiveCaller = caller ?? "cli";
|
|
2426
3073
|
const resolved = activeSessionManager.resolveForTools(selector);
|
|
@@ -2442,7 +3089,10 @@ var startDaemon = async (options) => {
|
|
|
2442
3089
|
if (resolved.state !== "active") {
|
|
2443
3090
|
const errorType = resolved.state === "suspended" ? "session_suspended" : "session_not_active";
|
|
2444
3091
|
writeAudit("error", errorType);
|
|
2445
|
-
throw new RpcApplicationError(
|
|
3092
|
+
throw new RpcApplicationError(
|
|
3093
|
+
errorType,
|
|
3094
|
+
resolved.suspendReason === "app_backgrounded" ? appBackgroundedMessage(resolved.alias) : `Session "${resolved.alias}" is not active.`
|
|
3095
|
+
);
|
|
2446
3096
|
}
|
|
2447
3097
|
const tool = resolved.registry.get(name);
|
|
2448
3098
|
if (!tool) {
|
|
@@ -2523,13 +3173,13 @@ var startDaemon = async (options) => {
|
|
|
2523
3173
|
unregisterOnClose();
|
|
2524
3174
|
}
|
|
2525
3175
|
},
|
|
2526
|
-
[
|
|
3176
|
+
[RPC_METHODS3.toolsCancel]: (params) => {
|
|
2527
3177
|
const { selector, callId, reason } = asToolsCancelParams(params);
|
|
2528
3178
|
const resolved = activeSessionManager.resolveForTools(selector);
|
|
2529
3179
|
const cancelled = activeCallsManager.cancel(resolved.sessionId, callId, reason ?? "client_cancelled");
|
|
2530
3180
|
return { cancelled };
|
|
2531
3181
|
},
|
|
2532
|
-
[
|
|
3182
|
+
[RPC_METHODS3.eventsSubscribe]: (params, context) => {
|
|
2533
3183
|
const { sessionSelector, kinds, name, payloadMaxBytes } = asEventsSubscribeParams(params);
|
|
2534
3184
|
context.connection.state.eventSubscription = {
|
|
2535
3185
|
sessionSelector,
|
|
@@ -2539,7 +3189,7 @@ var startDaemon = async (options) => {
|
|
|
2539
3189
|
};
|
|
2540
3190
|
return { ok: true };
|
|
2541
3191
|
},
|
|
2542
|
-
[
|
|
3192
|
+
[RPC_METHODS3.eventsSince]: (params) => {
|
|
2543
3193
|
const { selector, since, limit, name, payloadMaxBytes } = asEventsSinceParams(params);
|
|
2544
3194
|
const resolved = activeSessionManager.describe(selector);
|
|
2545
3195
|
return activeEventBus.since(resolved.sessionId, { since, limit, name, payloadMaxBytes });
|
|
@@ -2558,6 +3208,7 @@ var startDaemon = async (options) => {
|
|
|
2558
3208
|
callsManager?.disposeAll();
|
|
2559
3209
|
sessionManager?.disposeAll(1001, "daemon_startup_failed");
|
|
2560
3210
|
await listener?.close();
|
|
3211
|
+
await webListener?.close();
|
|
2561
3212
|
await pidfile?.release();
|
|
2562
3213
|
throw error;
|
|
2563
3214
|
}
|
|
@@ -2568,6 +3219,7 @@ var startDaemon = async (options) => {
|
|
|
2568
3219
|
startedAt,
|
|
2569
3220
|
server,
|
|
2570
3221
|
listener,
|
|
3222
|
+
webListener,
|
|
2571
3223
|
tls: tlsManager,
|
|
2572
3224
|
eventBus,
|
|
2573
3225
|
exited,
|
|
@@ -2575,175 +3227,6 @@ var startDaemon = async (options) => {
|
|
|
2575
3227
|
};
|
|
2576
3228
|
};
|
|
2577
3229
|
|
|
2578
|
-
// src/rpc/client.ts
|
|
2579
|
-
import { join as join5 } from "node:path";
|
|
2580
|
-
import { RPC_METHODS as RPC_METHODS2 } from "@appduct/shared";
|
|
2581
|
-
var defaultBinPath = join5(getPackageRoot(), "bin.js");
|
|
2582
|
-
var DaemonRpcError = class extends Error {
|
|
2583
|
-
constructor(code, message, data) {
|
|
2584
|
-
super(message);
|
|
2585
|
-
this.code = code;
|
|
2586
|
-
this.data = data;
|
|
2587
|
-
this.name = "DaemonRpcError";
|
|
2588
|
-
}
|
|
2589
|
-
};
|
|
2590
|
-
var DaemonUnavailableError = class extends Error {
|
|
2591
|
-
constructor(message, reason = "unreachable") {
|
|
2592
|
-
super(message);
|
|
2593
|
-
this.reason = reason;
|
|
2594
|
-
this.name = "DaemonUnavailableError";
|
|
2595
|
-
}
|
|
2596
|
-
};
|
|
2597
|
-
var describeLiveState = (state) => {
|
|
2598
|
-
const parts = [];
|
|
2599
|
-
if (state.sessions > 0) {
|
|
2600
|
-
parts.push(`${state.sessions} connected session(s)`);
|
|
2601
|
-
}
|
|
2602
|
-
if (state.pendingLinks > 0) {
|
|
2603
|
-
parts.push(`${state.pendingLinks} unclaimed link(s)`);
|
|
2604
|
-
}
|
|
2605
|
-
return parts.join(" and ");
|
|
2606
|
-
};
|
|
2607
|
-
var DaemonVersionMismatchError = class extends Error {
|
|
2608
|
-
constructor(daemonVersion, clientVersion, live, message) {
|
|
2609
|
-
super(
|
|
2610
|
-
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".`
|
|
2611
|
-
);
|
|
2612
|
-
this.daemonVersion = daemonVersion;
|
|
2613
|
-
this.clientVersion = clientVersion;
|
|
2614
|
-
this.name = "DaemonVersionMismatchError";
|
|
2615
|
-
this.sessionCount = live.sessions;
|
|
2616
|
-
this.pendingLinkCount = live.pendingLinks;
|
|
2617
|
-
}
|
|
2618
|
-
sessionCount;
|
|
2619
|
-
pendingLinkCount;
|
|
2620
|
-
};
|
|
2621
|
-
var isConnectionMissingError = (error) => {
|
|
2622
|
-
const code = error.code;
|
|
2623
|
-
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
2624
|
-
};
|
|
2625
|
-
var isDaemonUnreachableError = (error) => {
|
|
2626
|
-
return error instanceof DaemonUnavailableError || isConnectionMissingError(error);
|
|
2627
|
-
};
|
|
2628
|
-
|
|
2629
|
-
// src/errors.ts
|
|
2630
|
-
var EXIT_CODE_BY_ERROR_TYPE = {
|
|
2631
|
-
usage_error: 64,
|
|
2632
|
-
validation_error: 65,
|
|
2633
|
-
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
2634
|
-
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
2635
|
-
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
2636
|
-
// assertion failure (docs/tasks/08-appduct-doctor.md).
|
|
2637
|
-
inspection_error: 66,
|
|
2638
|
-
connection_error: 70,
|
|
2639
|
-
session_error: 71,
|
|
2640
|
-
tool_error: 72,
|
|
2641
|
-
permission_error: 77,
|
|
2642
|
-
internal_error: 1,
|
|
2643
|
-
// `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
|
|
2644
|
-
// assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
|
|
2645
|
-
// and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
|
|
2646
|
-
// conflate a normal assertion failure with an unexpected crash.
|
|
2647
|
-
assertion_error: 3
|
|
2648
|
-
};
|
|
2649
|
-
var RPC_ERROR_EXIT_CLASS = {
|
|
2650
|
-
no_session: "session_error",
|
|
2651
|
-
ambiguous_session: "session_error",
|
|
2652
|
-
unknown_session: "session_error",
|
|
2653
|
-
session_not_active: "session_error",
|
|
2654
|
-
session_suspended: "session_error",
|
|
2655
|
-
tool_not_found: "tool_error",
|
|
2656
|
-
tool_input_validation_error: "tool_error",
|
|
2657
|
-
tool_output_validation_error: "tool_error",
|
|
2658
|
-
tool_execution_error: "tool_error",
|
|
2659
|
-
tool_serialization_error: "tool_error",
|
|
2660
|
-
tool_timeout: "tool_error",
|
|
2661
|
-
tool_cancelled: "tool_error",
|
|
2662
|
-
policy_denied: "permission_error",
|
|
2663
|
-
invalid_request: "validation_error"
|
|
2664
|
-
};
|
|
2665
|
-
var AppductCliError = class extends Error {
|
|
2666
|
-
type;
|
|
2667
|
-
details;
|
|
2668
|
-
constructor(type, message, details) {
|
|
2669
|
-
super(message);
|
|
2670
|
-
this.name = "AppductCliError";
|
|
2671
|
-
this.type = type;
|
|
2672
|
-
this.details = details;
|
|
2673
|
-
}
|
|
2674
|
-
};
|
|
2675
|
-
var connectionError = (message, details) => {
|
|
2676
|
-
return new AppductCliError("connection_error", message, details);
|
|
2677
|
-
};
|
|
2678
|
-
var isAppductCliError = (value) => {
|
|
2679
|
-
return value instanceof AppductCliError;
|
|
2680
|
-
};
|
|
2681
|
-
var rpcErrorClass = (error) => {
|
|
2682
|
-
const type = error.data?.type;
|
|
2683
|
-
return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
|
|
2684
|
-
};
|
|
2685
|
-
var toCliError = (error) => {
|
|
2686
|
-
if (isAppductCliError(error)) {
|
|
2687
|
-
return {
|
|
2688
|
-
type: error.type,
|
|
2689
|
-
message: error.message,
|
|
2690
|
-
details: error.details
|
|
2691
|
-
};
|
|
2692
|
-
}
|
|
2693
|
-
if (error instanceof DaemonVersionMismatchError) {
|
|
2694
|
-
return {
|
|
2695
|
-
type: "connection_error",
|
|
2696
|
-
message: error.message,
|
|
2697
|
-
details: {
|
|
2698
|
-
daemon_version: error.daemonVersion,
|
|
2699
|
-
client_version: error.clientVersion,
|
|
2700
|
-
session_count: error.sessionCount,
|
|
2701
|
-
pending_link_count: error.pendingLinkCount
|
|
2702
|
-
}
|
|
2703
|
-
};
|
|
2704
|
-
}
|
|
2705
|
-
if (error instanceof DaemonRpcError && error.data?.type) {
|
|
2706
|
-
return {
|
|
2707
|
-
type: error.data.type,
|
|
2708
|
-
message: error.message,
|
|
2709
|
-
details: error.data.details
|
|
2710
|
-
};
|
|
2711
|
-
}
|
|
2712
|
-
if (isDaemonUnreachableError(error)) {
|
|
2713
|
-
return {
|
|
2714
|
-
type: "connection_error",
|
|
2715
|
-
message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
|
|
2716
|
-
};
|
|
2717
|
-
}
|
|
2718
|
-
if (error instanceof Error) {
|
|
2719
|
-
return {
|
|
2720
|
-
type: "internal_error",
|
|
2721
|
-
message: error.message
|
|
2722
|
-
};
|
|
2723
|
-
}
|
|
2724
|
-
return {
|
|
2725
|
-
type: "internal_error",
|
|
2726
|
-
message: "An unexpected error occurred.",
|
|
2727
|
-
details: error
|
|
2728
|
-
};
|
|
2729
|
-
};
|
|
2730
|
-
var getExitCodeForError = (error) => {
|
|
2731
|
-
if (isAppductCliError(error)) {
|
|
2732
|
-
return EXIT_CODE_BY_ERROR_TYPE[error.type];
|
|
2733
|
-
}
|
|
2734
|
-
if (error instanceof DaemonVersionMismatchError) {
|
|
2735
|
-
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
2736
|
-
}
|
|
2737
|
-
if (error instanceof DaemonRpcError) {
|
|
2738
|
-
const cliErrorClass = rpcErrorClass(error);
|
|
2739
|
-
return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
|
|
2740
|
-
}
|
|
2741
|
-
if (isDaemonUnreachableError(error)) {
|
|
2742
|
-
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
2743
|
-
}
|
|
2744
|
-
return EXIT_CODE_BY_ERROR_TYPE.internal_error;
|
|
2745
|
-
};
|
|
2746
|
-
|
|
2747
3230
|
// src/commands/daemon/run.ts
|
|
2748
3231
|
var handleDaemonRunCommand = async (context) => {
|
|
2749
3232
|
let daemon;
|
|
@@ -2786,6 +3269,7 @@ var formatJson = (value, flags) => {
|
|
|
2786
3269
|
import pc from "picocolors";
|
|
2787
3270
|
import {
|
|
2788
3271
|
formatAgentWebSocketUrl,
|
|
3272
|
+
renderEventSignature,
|
|
2789
3273
|
renderToolSignature,
|
|
2790
3274
|
summarizeToolDescription
|
|
2791
3275
|
} from "@appduct/shared";
|
|
@@ -2881,6 +3365,13 @@ var formatDevice = (device) => {
|
|
|
2881
3365
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
2882
3366
|
return device.os ? `${label} (${device.os})` : label;
|
|
2883
3367
|
};
|
|
3368
|
+
var SUSPEND_REASON_LABELS = {
|
|
3369
|
+
app_backgrounded: "app in background",
|
|
3370
|
+
connection_lost: "connection lost"
|
|
3371
|
+
};
|
|
3372
|
+
var formatSessionState = (session) => {
|
|
3373
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
3374
|
+
};
|
|
2884
3375
|
var renderLsData = (colors, data, now) => {
|
|
2885
3376
|
if (data.length === 0) {
|
|
2886
3377
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -2889,7 +3380,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
2889
3380
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
2890
3381
|
const rows = data.map((session) => [
|
|
2891
3382
|
session.alias,
|
|
2892
|
-
session
|
|
3383
|
+
formatSessionState(session),
|
|
2893
3384
|
formatDevice(session.device),
|
|
2894
3385
|
String(session.toolCount),
|
|
2895
3386
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -3063,10 +3554,87 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
3063
3554
|
}
|
|
3064
3555
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
3065
3556
|
};
|
|
3557
|
+
var renderEventsTruncationLine = (data) => {
|
|
3558
|
+
const offset = data.offset ?? 0;
|
|
3559
|
+
if (data.events.length >= data.total) {
|
|
3560
|
+
return [];
|
|
3561
|
+
}
|
|
3562
|
+
return [
|
|
3563
|
+
"",
|
|
3564
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
3565
|
+
];
|
|
3566
|
+
};
|
|
3567
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
3568
|
+
const [only] = data.events;
|
|
3569
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
3570
|
+
return renderFields(
|
|
3571
|
+
colors.green(`Event: ${only.name}`),
|
|
3572
|
+
[
|
|
3573
|
+
["Signature", renderEventSignature(only)],
|
|
3574
|
+
["Description", only.description],
|
|
3575
|
+
["Payload schema", only.payload_schema]
|
|
3576
|
+
],
|
|
3577
|
+
flags
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
3581
|
+
return [
|
|
3582
|
+
colors.green("Events"),
|
|
3583
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
3584
|
+
];
|
|
3585
|
+
}
|
|
3586
|
+
if (data.events.length === 0) {
|
|
3587
|
+
return [
|
|
3588
|
+
colors.green("Events"),
|
|
3589
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
3590
|
+
];
|
|
3591
|
+
}
|
|
3592
|
+
const lines = data.events.flatMap((event) => [
|
|
3593
|
+
` ${renderEventSignature(event)}`,
|
|
3594
|
+
` ${summarizeToolDescription(event.description)}`
|
|
3595
|
+
]);
|
|
3596
|
+
return [
|
|
3597
|
+
colors.green("Events"),
|
|
3598
|
+
...lines,
|
|
3599
|
+
...renderEventsTruncationLine(data),
|
|
3600
|
+
"",
|
|
3601
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
3602
|
+
];
|
|
3603
|
+
};
|
|
3066
3604
|
var renderInvokeData = (colors, data, flags) => {
|
|
3067
3605
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
3068
3606
|
};
|
|
3069
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
|
+
}
|
|
3070
3638
|
const lines = [
|
|
3071
3639
|
colors.green("Link Created"),
|
|
3072
3640
|
...renderFields(
|
|
@@ -3187,6 +3755,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
3187
3755
|
["PID", data.daemon.pid],
|
|
3188
3756
|
["Started at", data.daemon.started_at],
|
|
3189
3757
|
["WSS port", data.daemon.wss_port],
|
|
3758
|
+
["Web port", data.daemon.web_port],
|
|
3190
3759
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
3191
3760
|
["Sessions", data.daemon.session_count]
|
|
3192
3761
|
],
|
|
@@ -3254,6 +3823,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
3254
3823
|
return renderToolsListData(colors, data, flags, options.full);
|
|
3255
3824
|
case "tools describe":
|
|
3256
3825
|
return renderToolDetail(colors, data, flags);
|
|
3826
|
+
case "events ls":
|
|
3827
|
+
return renderEventsListData(colors, data, flags);
|
|
3257
3828
|
case "tools call":
|
|
3258
3829
|
return renderInvokeData(colors, data, flags);
|
|
3259
3830
|
case "sessions revoke":
|