appduct 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -52
- package/dist/artifact-inspect.d.ts +0 -1
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +1250 -2
- package/dist/cli/command-options.d.ts +8 -1
- package/dist/cli/create-cli.d.ts +0 -1
- package/dist/cli/dispatch.d.ts +10 -1
- package/dist/cli/envelope.d.ts +19 -0
- package/dist/cli/global-flags.d.ts +30 -0
- package/dist/cli/open-target.d.ts +25 -8
- package/dist/cli/result-types.d.ts +31 -5
- package/dist/cli/router.d.ts +78 -0
- package/dist/cli/routes/daemon/index.d.ts +9 -0
- package/dist/cli/routes/daemon/index.js +848 -0
- package/dist/cli/routes/daemon/run.d.ts +3 -0
- package/dist/cli/routes/daemon/run.js +3246 -0
- package/dist/cli/routes/daemon/start.d.ts +3 -0
- package/dist/cli/routes/daemon/start.js +1677 -0
- package/dist/cli/routes/daemon/status.d.ts +4 -0
- package/dist/cli/routes/daemon/status.js +1685 -0
- package/dist/cli/routes/daemon/stop.d.ts +4 -0
- package/dist/cli/routes/daemon/stop.js +1655 -0
- package/dist/cli/routes/doctor.d.ts +3 -0
- package/dist/cli/routes/doctor.js +1075 -0
- package/dist/cli/routes/events.d.ts +3 -0
- package/dist/cli/routes/events.js +1915 -0
- package/dist/cli/routes/init.d.ts +3 -0
- package/dist/cli/routes/init.js +1486 -0
- package/dist/cli/routes/invoke.d.ts +3 -0
- package/dist/cli/routes/invoke.js +1893 -0
- package/dist/cli/routes/keygen.d.ts +3 -0
- package/dist/cli/routes/keygen.js +928 -0
- package/dist/cli/routes/link.d.ts +3 -0
- package/dist/cli/routes/link.js +2714 -0
- package/dist/cli/routes/ls.d.ts +3 -0
- package/dist/cli/routes/ls.js +1668 -0
- package/dist/cli/routes/mcp.d.ts +5 -0
- package/dist/cli/routes/mcp.js +3929 -0
- package/dist/cli/routes/revoke.d.ts +3 -0
- package/dist/cli/routes/revoke.js +1684 -0
- package/dist/cli/routes/tools.d.ts +3 -0
- package/dist/cli/routes/tools.js +1860 -0
- package/dist/cli/runner.d.ts +3 -11
- package/dist/cli/types.d.ts +9 -6
- package/dist/cli/version-guard.d.ts +20 -0
- package/dist/cli.d.ts +0 -1
- package/dist/client/app-client.d.ts +4 -4
- package/dist/client/bootstrap.d.ts +4 -4
- package/dist/client/connect.d.ts +0 -1
- package/dist/client/errors.d.ts +0 -1
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.js +2389 -16
- package/dist/commands/daemon/context.d.ts +14 -0
- package/dist/commands/daemon/run.d.ts +11 -0
- package/dist/commands/daemon/start.d.ts +5 -0
- package/dist/commands/daemon/status.d.ts +5 -0
- package/dist/commands/daemon/stop.d.ts +5 -0
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/events.d.ts +0 -1
- package/dist/commands/init.d.ts +17 -4
- package/dist/commands/invoke.d.ts +0 -1
- package/dist/commands/keygen.d.ts +0 -1
- package/dist/commands/link.d.ts +1 -2
- package/dist/commands/ls.d.ts +0 -1
- package/dist/commands/mcp.d.ts +2 -4
- package/dist/commands/revoke.d.ts +0 -1
- package/dist/commands/tools.d.ts +19 -3
- package/dist/daemon/address.d.ts +0 -1
- package/dist/daemon/audit.d.ts +6 -11
- package/dist/daemon/calls.d.ts +0 -1
- package/dist/daemon/config.d.ts +13 -16
- package/dist/daemon/daemon.d.ts +0 -1
- package/dist/daemon/event-bus.d.ts +0 -1
- package/dist/daemon/links.d.ts +0 -1
- package/dist/daemon/listener.d.ts +0 -1
- package/dist/daemon/log-rotation.d.ts +0 -1
- package/dist/daemon/pidfile.d.ts +14 -8
- package/dist/daemon/policy.d.ts +0 -1
- package/dist/daemon/registry.d.ts +0 -1
- package/dist/daemon/rpc-errors.d.ts +16 -0
- package/dist/daemon/rpc-server.d.ts +0 -11
- package/dist/daemon/sessions.d.ts +0 -1
- package/dist/daemon/socket-probe.d.ts +0 -1
- package/dist/daemon/state-dir.d.ts +0 -1
- package/dist/daemon/timers.d.ts +0 -1
- package/dist/daemon/tls.d.ts +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/host-certificate.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1307 -3
- package/dist/key-material.d.ts +0 -1
- package/dist/link.d.ts +8 -7
- package/dist/mcp/app-tools.d.ts +153 -0
- package/dist/mcp/connect-tool.d.ts +14 -8
- package/dist/mcp/daemon-tools.d.ts +2 -11
- package/dist/mcp/events-tool.d.ts +0 -1
- package/dist/mcp/server.d.ts +33 -14
- package/dist/native-scheme.d.ts +0 -1
- package/dist/output.d.ts +8 -11
- package/dist/package-root.d.ts +11 -0
- package/dist/package-version.d.ts +0 -1
- package/dist/qr-terminal.d.ts +0 -1
- package/dist/rpc/client.d.ts +3 -3
- package/dist/scheme.d.ts +60 -2
- package/dist/spki-pin.d.ts +0 -1
- package/package.json +6 -3
- package/dist/artifact-inspect.d.ts.map +0 -1
- package/dist/artifact-inspect.js +0 -334
- package/dist/bin.d.ts.map +0 -1
- package/dist/cli/command-options.d.ts.map +0 -1
- package/dist/cli/command-options.js +0 -95
- package/dist/cli/create-cli.d.ts.map +0 -1
- package/dist/cli/create-cli.js +0 -56
- package/dist/cli/dispatch.d.ts.map +0 -1
- package/dist/cli/dispatch.js +0 -275
- package/dist/cli/open-target.d.ts.map +0 -1
- package/dist/cli/open-target.js +0 -438
- package/dist/cli/result-types.d.ts.map +0 -1
- package/dist/cli/result-types.js +0 -6
- package/dist/cli/runner.d.ts.map +0 -1
- package/dist/cli/runner.js +0 -111
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/types.js +0 -10
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client/app-client.d.ts.map +0 -1
- package/dist/client/app-client.js +0 -223
- package/dist/client/bootstrap.d.ts.map +0 -1
- package/dist/client/bootstrap.js +0 -108
- package/dist/client/connect.d.ts.map +0 -1
- package/dist/client/connect.js +0 -38
- package/dist/client/errors.d.ts.map +0 -1
- package/dist/client/errors.js +0 -40
- package/dist/client/index.d.ts.map +0 -1
- package/dist/commands/daemon.d.ts +0 -22
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/daemon.js +0 -128
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -42
- package/dist/commands/events.d.ts.map +0 -1
- package/dist/commands/events.js +0 -76
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -231
- package/dist/commands/invoke.d.ts.map +0 -1
- package/dist/commands/invoke.js +0 -54
- package/dist/commands/keygen.d.ts.map +0 -1
- package/dist/commands/keygen.js +0 -46
- package/dist/commands/link.d.ts.map +0 -1
- package/dist/commands/link.js +0 -43
- package/dist/commands/ls.d.ts.map +0 -1
- package/dist/commands/ls.js +0 -7
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/mcp.js +0 -98
- package/dist/commands/revoke.d.ts.map +0 -1
- package/dist/commands/revoke.js +0 -7
- package/dist/commands/tools.d.ts.map +0 -1
- package/dist/commands/tools.js +0 -52
- package/dist/daemon/address.d.ts.map +0 -1
- package/dist/daemon/address.js +0 -71
- package/dist/daemon/audit.d.ts.map +0 -1
- package/dist/daemon/audit.js +0 -237
- package/dist/daemon/calls.d.ts.map +0 -1
- package/dist/daemon/calls.js +0 -158
- package/dist/daemon/config.d.ts.map +0 -1
- package/dist/daemon/config.js +0 -170
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js +0 -560
- package/dist/daemon/event-bus.d.ts.map +0 -1
- package/dist/daemon/event-bus.js +0 -111
- package/dist/daemon/links.d.ts.map +0 -1
- package/dist/daemon/links.js +0 -118
- package/dist/daemon/listener.d.ts.map +0 -1
- package/dist/daemon/listener.js +0 -132
- package/dist/daemon/log-rotation.d.ts.map +0 -1
- package/dist/daemon/log-rotation.js +0 -136
- package/dist/daemon/pidfile.d.ts.map +0 -1
- package/dist/daemon/pidfile.js +0 -90
- package/dist/daemon/policy.d.ts.map +0 -1
- package/dist/daemon/policy.js +0 -24
- package/dist/daemon/registry.d.ts.map +0 -1
- package/dist/daemon/registry.js +0 -31
- package/dist/daemon/rpc-server.d.ts.map +0 -1
- package/dist/daemon/rpc-server.js +0 -203
- package/dist/daemon/sessions.d.ts.map +0 -1
- package/dist/daemon/sessions.js +0 -429
- package/dist/daemon/socket-probe.d.ts.map +0 -1
- package/dist/daemon/socket-probe.js +0 -28
- package/dist/daemon/state-dir.d.ts.map +0 -1
- package/dist/daemon/state-dir.js +0 -43
- package/dist/daemon/timers.d.ts.map +0 -1
- package/dist/daemon/timers.js +0 -12
- package/dist/daemon/tls.d.ts.map +0 -1
- package/dist/daemon/tls.js +0 -79
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -162
- package/dist/host-certificate.d.ts.map +0 -1
- package/dist/host-certificate.js +0 -111
- package/dist/index.d.ts.map +0 -1
- package/dist/key-material.d.ts.map +0 -1
- package/dist/key-material.js +0 -42
- package/dist/link.d.ts.map +0 -1
- package/dist/link.js +0 -117
- package/dist/mcp/connect-tool.d.ts.map +0 -1
- package/dist/mcp/connect-tool.js +0 -396
- package/dist/mcp/daemon-tools.d.ts.map +0 -1
- package/dist/mcp/daemon-tools.js +0 -29
- package/dist/mcp/events-tool.d.ts.map +0 -1
- package/dist/mcp/events-tool.js +0 -322
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -538
- package/dist/mcp/tool-mapping.d.ts +0 -58
- package/dist/mcp/tool-mapping.d.ts.map +0 -1
- package/dist/mcp/tool-mapping.js +0 -143
- package/dist/mcp/tool-namespace.d.ts +0 -40
- package/dist/mcp/tool-namespace.d.ts.map +0 -1
- package/dist/mcp/tool-namespace.js +0 -65
- package/dist/native-scheme.d.ts.map +0 -1
- package/dist/native-scheme.js +0 -465
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -342
- package/dist/package-version.d.ts.map +0 -1
- package/dist/package-version.js +0 -47
- package/dist/qr-terminal.d.ts.map +0 -1
- package/dist/qr-terminal.js +0 -45
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -819
- package/dist/scheme.d.ts.map +0 -1
- package/dist/scheme.js +0 -321
- package/dist/spki-pin.d.ts.map +0 -1
- package/dist/spki-pin.js +0 -14
|
@@ -0,0 +1,3246 @@
|
|
|
1
|
+
// src/daemon/daemon.ts
|
|
2
|
+
import { rm as rm5 } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
MAX_TOOLS_FILTER_LENGTH,
|
|
5
|
+
RPC_METHODS,
|
|
6
|
+
isValidToolGroup,
|
|
7
|
+
summarizeToolGroups,
|
|
8
|
+
toolGroupMatches,
|
|
9
|
+
EVENT_KINDS
|
|
10
|
+
} from "@appduct/shared";
|
|
11
|
+
|
|
12
|
+
// src/package-version.ts
|
|
13
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
14
|
+
import { join as join2 } from "node:path";
|
|
15
|
+
|
|
16
|
+
// src/package-root.ts
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
var PACKAGE_NAME = "appduct";
|
|
21
|
+
var cached;
|
|
22
|
+
var getPackageRoot = () => {
|
|
23
|
+
if (cached !== void 0) {
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
for (; ; ) {
|
|
28
|
+
const candidate = join(dir, "package.json");
|
|
29
|
+
if (existsSync(candidate)) {
|
|
30
|
+
let name;
|
|
31
|
+
try {
|
|
32
|
+
name = JSON.parse(readFileSync(candidate, "utf8")).name;
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
if (name === PACKAGE_NAME) {
|
|
36
|
+
cached = dir;
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const parent = dirname(dir);
|
|
41
|
+
if (parent === dir) {
|
|
42
|
+
throw new Error(`Could not locate the "${PACKAGE_NAME}" package root above ${import.meta.url}.`);
|
|
43
|
+
}
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/package-version.ts
|
|
49
|
+
var DAEMON_VERSION_OVERRIDE_ENV = "APPDUCT_DAEMON_VERSION_OVERRIDE";
|
|
50
|
+
var isTestRuntime = (env) => {
|
|
51
|
+
return Boolean(env.VITEST) || env.NODE_ENV === "test";
|
|
52
|
+
};
|
|
53
|
+
var cached2;
|
|
54
|
+
var getPackageVersion = () => {
|
|
55
|
+
cached2 ??= JSON.parse(readFileSync2(join2(getPackageRoot(), "package.json"), "utf8")).version;
|
|
56
|
+
return cached2;
|
|
57
|
+
};
|
|
58
|
+
var getDaemonReportedVersion = (env = process.env) => {
|
|
59
|
+
if (!isTestRuntime(env)) {
|
|
60
|
+
return getPackageVersion();
|
|
61
|
+
}
|
|
62
|
+
const override = env[DAEMON_VERSION_OVERRIDE_ENV];
|
|
63
|
+
return override !== void 0 && override.length > 0 ? override : getPackageVersion();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/daemon/address.ts
|
|
67
|
+
import { networkInterfaces } from "node:os";
|
|
68
|
+
import {
|
|
69
|
+
isLinkLocalIpv6Address,
|
|
70
|
+
isPrivateIpv4Address,
|
|
71
|
+
isUniqueLocalIpv6Address
|
|
72
|
+
} from "@appduct/shared";
|
|
73
|
+
var isDockerDefaultBridgeAddress = (address) => {
|
|
74
|
+
return address.startsWith("172.17.");
|
|
75
|
+
};
|
|
76
|
+
var isCgnatIpv4Address = (address) => {
|
|
77
|
+
const parts = address.split(".");
|
|
78
|
+
if (parts.length !== 4) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const first = Number(parts[0]);
|
|
82
|
+
const second = Number(parts[1]);
|
|
83
|
+
return first === 100 && Number.isInteger(second) && second >= 64 && second <= 127;
|
|
84
|
+
};
|
|
85
|
+
var isExcludedIpv4Address = (address) => {
|
|
86
|
+
return isDockerDefaultBridgeAddress(address) || isCgnatIpv4Address(address);
|
|
87
|
+
};
|
|
88
|
+
var detectAdvertisedAddress = (options = {}) => {
|
|
89
|
+
if (options.override) {
|
|
90
|
+
return {
|
|
91
|
+
family: options.override.includes(":") ? 6 : 4,
|
|
92
|
+
address: options.override
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const ipv4Candidates = [];
|
|
96
|
+
const ipv6Candidates = [];
|
|
97
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
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
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const preferredIpv4 = ipv4Candidates.find((address) => isPrivateIpv4Address(address)) ?? ipv4Candidates[0];
|
|
116
|
+
if (preferredIpv4) {
|
|
117
|
+
return { family: 4, address: preferredIpv4 };
|
|
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" };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/daemon/audit.ts
|
|
127
|
+
import { createHash } from "node:crypto";
|
|
128
|
+
import { chmod, mkdir, open, readdir, rm, stat } from "node:fs/promises";
|
|
129
|
+
import { join as join3 } from "node:path";
|
|
130
|
+
|
|
131
|
+
// src/daemon/config.ts
|
|
132
|
+
import { readFile } from "node:fs/promises";
|
|
133
|
+
var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
134
|
+
"wssPort",
|
|
135
|
+
"keyPath",
|
|
136
|
+
"graceSeconds",
|
|
137
|
+
"linkTtlSeconds",
|
|
138
|
+
"keepaliveIntervalSeconds",
|
|
139
|
+
"eventBufferSize",
|
|
140
|
+
"auditRetentionDays",
|
|
141
|
+
"daemonLogMaxBytes",
|
|
142
|
+
"policy",
|
|
143
|
+
"advertisedIp",
|
|
144
|
+
"scheme",
|
|
145
|
+
"restartDaemonOnVersionMismatch"
|
|
146
|
+
]);
|
|
147
|
+
var KNOWN_POLICY_KEYS = /* @__PURE__ */ new Set(["default", "destructive", "tools"]);
|
|
148
|
+
var POLICY_DECISIONS = /* @__PURE__ */ new Set(["allow", "deny", "prompt"]);
|
|
149
|
+
var DEFAULT_AUDIT_RETENTION_DAYS = 30;
|
|
150
|
+
var DEFAULT_DAEMON_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
151
|
+
var AppductConfigError = class extends Error {
|
|
152
|
+
constructor(key, message) {
|
|
153
|
+
super(message);
|
|
154
|
+
this.key = key;
|
|
155
|
+
this.name = "AppductConfigError";
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var configError = (key, message) => {
|
|
159
|
+
return new AppductConfigError(key, `Invalid Appduct config value for "${key}": ${message}`);
|
|
160
|
+
};
|
|
161
|
+
var requireWssPort = (value, key) => {
|
|
162
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65535) {
|
|
163
|
+
throw configError(key, "must be a port number between 0 and 65535 (0 binds an OS-assigned port).");
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
};
|
|
167
|
+
var requirePositiveInteger = (value, key) => {
|
|
168
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
169
|
+
throw configError(key, "must be a positive integer.");
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
};
|
|
173
|
+
var requireNonEmptyString = (value, key) => {
|
|
174
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
175
|
+
throw configError(key, "must be a non-empty string.");
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
};
|
|
179
|
+
var requireBoolean = (value, key) => {
|
|
180
|
+
if (typeof value !== "boolean") {
|
|
181
|
+
throw configError(key, "must be a boolean.");
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
};
|
|
185
|
+
var requirePolicyDecision = (value, key) => {
|
|
186
|
+
if (typeof value !== "string" || !POLICY_DECISIONS.has(value)) {
|
|
187
|
+
throw configError(key, 'must be "allow", "deny", or "prompt".');
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
};
|
|
191
|
+
var defaultConfig = (paths) => {
|
|
192
|
+
return {
|
|
193
|
+
wssPort: 8443,
|
|
194
|
+
keyPath: paths.keyPath,
|
|
195
|
+
graceSeconds: 600,
|
|
196
|
+
linkTtlSeconds: 300,
|
|
197
|
+
keepaliveIntervalSeconds: 15,
|
|
198
|
+
eventBufferSize: 256,
|
|
199
|
+
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
200
|
+
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
201
|
+
restartDaemonOnVersionMismatch: false,
|
|
202
|
+
policy: {
|
|
203
|
+
default: "allow",
|
|
204
|
+
destructive: "allow"
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
var loadConfig = async (paths, options = {}) => {
|
|
209
|
+
const warn = options.warn ?? (() => {
|
|
210
|
+
});
|
|
211
|
+
const config = defaultConfig(paths);
|
|
212
|
+
let raw;
|
|
213
|
+
try {
|
|
214
|
+
raw = JSON.parse(await readFile(paths.configPath, "utf8"));
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error.code === "ENOENT") {
|
|
217
|
+
return config;
|
|
218
|
+
}
|
|
219
|
+
throw configError("config.json", `could not be read/parsed (${error.message}).`);
|
|
220
|
+
}
|
|
221
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
222
|
+
throw configError("config.json", "must be a JSON object.");
|
|
223
|
+
}
|
|
224
|
+
const parsed = raw;
|
|
225
|
+
for (const key of Object.keys(parsed)) {
|
|
226
|
+
if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
|
|
227
|
+
warn(`Unknown Appduct config key "${key}" is ignored.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (parsed.wssPort !== void 0) {
|
|
231
|
+
config.wssPort = requireWssPort(parsed.wssPort, "wssPort");
|
|
232
|
+
}
|
|
233
|
+
if (parsed.keyPath !== void 0) {
|
|
234
|
+
config.keyPath = requireNonEmptyString(parsed.keyPath, "keyPath");
|
|
235
|
+
}
|
|
236
|
+
if (parsed.advertisedIp !== void 0) {
|
|
237
|
+
config.advertisedIp = requireNonEmptyString(parsed.advertisedIp, "advertisedIp");
|
|
238
|
+
}
|
|
239
|
+
if (parsed.scheme !== void 0) {
|
|
240
|
+
config.scheme = requireNonEmptyString(parsed.scheme, "scheme");
|
|
241
|
+
}
|
|
242
|
+
if (parsed.graceSeconds !== void 0) {
|
|
243
|
+
config.graceSeconds = requirePositiveInteger(parsed.graceSeconds, "graceSeconds");
|
|
244
|
+
}
|
|
245
|
+
if (parsed.linkTtlSeconds !== void 0) {
|
|
246
|
+
config.linkTtlSeconds = requirePositiveInteger(parsed.linkTtlSeconds, "linkTtlSeconds");
|
|
247
|
+
}
|
|
248
|
+
if (parsed.keepaliveIntervalSeconds !== void 0) {
|
|
249
|
+
config.keepaliveIntervalSeconds = requirePositiveInteger(
|
|
250
|
+
parsed.keepaliveIntervalSeconds,
|
|
251
|
+
"keepaliveIntervalSeconds"
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (parsed.eventBufferSize !== void 0) {
|
|
255
|
+
config.eventBufferSize = requirePositiveInteger(parsed.eventBufferSize, "eventBufferSize");
|
|
256
|
+
}
|
|
257
|
+
if (parsed.auditRetentionDays !== void 0) {
|
|
258
|
+
config.auditRetentionDays = requirePositiveInteger(parsed.auditRetentionDays, "auditRetentionDays");
|
|
259
|
+
}
|
|
260
|
+
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
261
|
+
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
262
|
+
}
|
|
263
|
+
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
264
|
+
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
265
|
+
parsed.restartDaemonOnVersionMismatch,
|
|
266
|
+
"restartDaemonOnVersionMismatch"
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (parsed.policy !== void 0) {
|
|
270
|
+
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
271
|
+
throw configError("policy", "must be an object.");
|
|
272
|
+
}
|
|
273
|
+
const policy = parsed.policy;
|
|
274
|
+
for (const key of Object.keys(policy)) {
|
|
275
|
+
if (!KNOWN_POLICY_KEYS.has(key)) {
|
|
276
|
+
warn(`Unknown Appduct config key "policy.${key}" is ignored.`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (policy.default !== void 0) {
|
|
280
|
+
config.policy.default = requirePolicyDecision(policy.default, "policy.default");
|
|
281
|
+
}
|
|
282
|
+
if (policy.destructive !== void 0) {
|
|
283
|
+
config.policy.destructive = requirePolicyDecision(policy.destructive, "policy.destructive");
|
|
284
|
+
}
|
|
285
|
+
if (policy.tools !== void 0) {
|
|
286
|
+
if (typeof policy.tools !== "object" || policy.tools === null || Array.isArray(policy.tools)) {
|
|
287
|
+
throw configError("policy.tools", "must be an object.");
|
|
288
|
+
}
|
|
289
|
+
const tools = {};
|
|
290
|
+
for (const [toolKey, toolValue] of Object.entries(policy.tools)) {
|
|
291
|
+
tools[toolKey] = requirePolicyDecision(toolValue, `policy.tools["${toolKey}"]`);
|
|
292
|
+
}
|
|
293
|
+
config.policy.tools = tools;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return config;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/daemon/audit.ts
|
|
300
|
+
var canonicalize = (value) => {
|
|
301
|
+
if (Array.isArray(value)) {
|
|
302
|
+
return value.map(canonicalize);
|
|
303
|
+
}
|
|
304
|
+
if (value !== null && typeof value === "object") {
|
|
305
|
+
const record = value;
|
|
306
|
+
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalize(record[key])]));
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
};
|
|
310
|
+
var canonicalArgsJson = (args) => {
|
|
311
|
+
return JSON.stringify(canonicalize(args));
|
|
312
|
+
};
|
|
313
|
+
var argsSha256 = (args) => {
|
|
314
|
+
return createHash("sha256").update(canonicalArgsJson(args)).digest("hex");
|
|
315
|
+
};
|
|
316
|
+
var dayStamp = (date) => {
|
|
317
|
+
return date.toISOString().slice(0, 10);
|
|
318
|
+
};
|
|
319
|
+
var AUDIT_DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl$/u;
|
|
320
|
+
var dayStampOfAuditFile = (name) => {
|
|
321
|
+
const match = AUDIT_DAY_FILE.exec(name);
|
|
322
|
+
if (!match) {
|
|
323
|
+
return void 0;
|
|
324
|
+
}
|
|
325
|
+
const stamp = match[1];
|
|
326
|
+
const parsed = /* @__PURE__ */ new Date(`${stamp}T00:00:00.000Z`);
|
|
327
|
+
return Number.isNaN(parsed.getTime()) || dayStamp(parsed) !== stamp ? void 0 : stamp;
|
|
328
|
+
};
|
|
329
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
330
|
+
var oldestRetainedStamp = (now, retentionDays) => {
|
|
331
|
+
const todayUtcMidnight = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
332
|
+
const cutoff = new Date(todayUtcMidnight - retentionDays * MS_PER_DAY);
|
|
333
|
+
return Number.isNaN(cutoff.getTime()) ? "0000-00-00" : dayStamp(cutoff);
|
|
334
|
+
};
|
|
335
|
+
var auditFilePathForDate = (auditDir, date) => {
|
|
336
|
+
return join3(auditDir, `${dayStamp(date)}.jsonl`);
|
|
337
|
+
};
|
|
338
|
+
var appendLine = async (filePath, line) => {
|
|
339
|
+
const handle = await open(filePath, "a", 384);
|
|
340
|
+
try {
|
|
341
|
+
await handle.appendFile(line);
|
|
342
|
+
await chmod(filePath, 384);
|
|
343
|
+
} finally {
|
|
344
|
+
await handle.close();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var createAuditLogger = (options) => {
|
|
348
|
+
const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
349
|
+
const warn = options.warn ?? ((message) => {
|
|
350
|
+
process.stderr.write(`${message}
|
|
351
|
+
`);
|
|
352
|
+
});
|
|
353
|
+
const retentionDays = options.retentionDays ?? DEFAULT_AUDIT_RETENTION_DAYS;
|
|
354
|
+
let failedWrites = 0;
|
|
355
|
+
let failedPrunes = 0;
|
|
356
|
+
let queue = Promise.resolve();
|
|
357
|
+
const listDayFiles = async () => {
|
|
358
|
+
let entries;
|
|
359
|
+
try {
|
|
360
|
+
entries = await readdir(options.auditDir, { withFileTypes: true });
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error.code === "ENOENT") {
|
|
363
|
+
return { files: [], failed: false };
|
|
364
|
+
}
|
|
365
|
+
return { files: [], failed: true, error };
|
|
366
|
+
}
|
|
367
|
+
const files = [];
|
|
368
|
+
for (const entry of entries) {
|
|
369
|
+
if (!entry.isFile()) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const stamp = dayStampOfAuditFile(entry.name);
|
|
373
|
+
if (stamp !== void 0) {
|
|
374
|
+
files.push({ name: entry.name, stamp });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { files, failed: false };
|
|
378
|
+
};
|
|
379
|
+
const pruneOnce = async () => {
|
|
380
|
+
const today = dayStamp(clock.now());
|
|
381
|
+
const cutoff = oldestRetainedStamp(clock.now(), retentionDays);
|
|
382
|
+
let deleted = 0;
|
|
383
|
+
let failed = 0;
|
|
384
|
+
const listing = await listDayFiles();
|
|
385
|
+
if (listing.failed) {
|
|
386
|
+
failedPrunes += 1;
|
|
387
|
+
warn(`appduct daemon: failed to read the audit directory to prune it: ${listing.error?.message}`);
|
|
388
|
+
return { deleted: 0, failed: 1 };
|
|
389
|
+
}
|
|
390
|
+
for (const file of listing.files) {
|
|
391
|
+
if (file.stamp === today || file.stamp >= cutoff) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
await rm(join3(options.auditDir, file.name), { force: true });
|
|
396
|
+
deleted += 1;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
failed += 1;
|
|
399
|
+
failedPrunes += 1;
|
|
400
|
+
warn(`appduct daemon: failed to prune the audit record "${file.name}": ${error.message}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { deleted, failed };
|
|
404
|
+
};
|
|
405
|
+
const record = (entry) => {
|
|
406
|
+
const now = clock.now();
|
|
407
|
+
const full = { ts: now.toISOString(), ...entry };
|
|
408
|
+
const line = `${JSON.stringify(full)}
|
|
409
|
+
`;
|
|
410
|
+
const filePath = auditFilePathForDate(options.auditDir, now);
|
|
411
|
+
queue = queue.then(async () => {
|
|
412
|
+
try {
|
|
413
|
+
await mkdir(options.auditDir, { recursive: true });
|
|
414
|
+
await appendLine(filePath, line);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
failedWrites += 1;
|
|
417
|
+
warn(`appduct daemon: failed to write an audit record: ${error.message}`);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
const prune = () => {
|
|
422
|
+
let settle;
|
|
423
|
+
const result = new Promise((resolve) => {
|
|
424
|
+
settle = resolve;
|
|
425
|
+
});
|
|
426
|
+
queue = queue.then(async () => {
|
|
427
|
+
try {
|
|
428
|
+
settle(await pruneOnce());
|
|
429
|
+
} catch (error) {
|
|
430
|
+
failedPrunes += 1;
|
|
431
|
+
warn(`appduct daemon: failed to prune the audit log: ${error.message}`);
|
|
432
|
+
settle({ deleted: 0, failed: 1 });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
return result;
|
|
436
|
+
};
|
|
437
|
+
const stats = async () => {
|
|
438
|
+
const listing = await listDayFiles();
|
|
439
|
+
if (listing.failed) {
|
|
440
|
+
return {};
|
|
441
|
+
}
|
|
442
|
+
let files = 0;
|
|
443
|
+
let bytes = 0;
|
|
444
|
+
for (const file of listing.files) {
|
|
445
|
+
try {
|
|
446
|
+
bytes += (await stat(join3(options.auditDir, file.name))).size;
|
|
447
|
+
files += 1;
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { files, bytes };
|
|
452
|
+
};
|
|
453
|
+
return {
|
|
454
|
+
record,
|
|
455
|
+
failedWrites: () => failedWrites,
|
|
456
|
+
failedPrunes: () => failedPrunes,
|
|
457
|
+
prune,
|
|
458
|
+
stats,
|
|
459
|
+
flush: () => queue
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/daemon/calls.ts
|
|
464
|
+
import { randomBytes } from "node:crypto";
|
|
465
|
+
import {
|
|
466
|
+
clampToolTimeoutMs,
|
|
467
|
+
DEFAULT_TOOL_TIMEOUT_MS,
|
|
468
|
+
MAX_TOOL_TIMEOUT_MS,
|
|
469
|
+
MIN_TOOL_TIMEOUT_MS
|
|
470
|
+
} from "@appduct/shared";
|
|
471
|
+
|
|
472
|
+
// src/daemon/rpc-errors.ts
|
|
473
|
+
var RpcApplicationError = class extends Error {
|
|
474
|
+
constructor(type, message, code = -32e3, details) {
|
|
475
|
+
super(message);
|
|
476
|
+
this.type = type;
|
|
477
|
+
this.code = code;
|
|
478
|
+
this.details = details;
|
|
479
|
+
this.name = "RpcApplicationError";
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// src/daemon/timers.ts
|
|
484
|
+
var systemTimers = {
|
|
485
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
486
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
487
|
+
setInterval: (callback, ms) => setInterval(callback, ms),
|
|
488
|
+
clearInterval: (handle) => clearInterval(handle)
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/daemon/calls.ts
|
|
492
|
+
var DEFAULT_CALL_TIMEOUT_MS = DEFAULT_TOOL_TIMEOUT_MS;
|
|
493
|
+
var generateCallId = () => `call_${randomBytes(16).toString("base64url")}`;
|
|
494
|
+
var clampTimeout = (timeoutMs) => {
|
|
495
|
+
if (timeoutMs === void 0 || !Number.isFinite(timeoutMs)) {
|
|
496
|
+
return DEFAULT_CALL_TIMEOUT_MS;
|
|
497
|
+
}
|
|
498
|
+
return clampToolTimeoutMs(timeoutMs);
|
|
499
|
+
};
|
|
500
|
+
var TERMINATING_KIND_ERROR_TYPES = {
|
|
501
|
+
session_suspended: "session_suspended",
|
|
502
|
+
session_revoked: "unknown_session",
|
|
503
|
+
session_expired: "unknown_session"
|
|
504
|
+
};
|
|
505
|
+
var createCallsManager = (options) => {
|
|
506
|
+
const timers = options.timers ?? systemTimers;
|
|
507
|
+
const pendingBySession = /* @__PURE__ */ new Map();
|
|
508
|
+
const takePending = (sessionId, callId) => {
|
|
509
|
+
const sessionMap = pendingBySession.get(sessionId);
|
|
510
|
+
const pending = sessionMap?.get(callId);
|
|
511
|
+
if (!sessionMap || !pending) {
|
|
512
|
+
return void 0;
|
|
513
|
+
}
|
|
514
|
+
sessionMap.delete(callId);
|
|
515
|
+
if (sessionMap.size === 0) {
|
|
516
|
+
pendingBySession.delete(sessionId);
|
|
517
|
+
}
|
|
518
|
+
timers.clearTimeout(pending.timer);
|
|
519
|
+
return pending;
|
|
520
|
+
};
|
|
521
|
+
const call = (session, name, args, timeoutMs) => {
|
|
522
|
+
const callId = generateCallId();
|
|
523
|
+
const result = new Promise((resolve, reject) => {
|
|
524
|
+
const effectiveTimeoutMs = clampTimeout(timeoutMs);
|
|
525
|
+
const timer = timers.setTimeout(() => {
|
|
526
|
+
const pending = takePending(session.sessionId, callId);
|
|
527
|
+
pending?.reject(
|
|
528
|
+
new RpcApplicationError("tool_timeout", `Tool call "${name}" timed out after ${effectiveTimeoutMs}ms.`)
|
|
529
|
+
);
|
|
530
|
+
}, effectiveTimeoutMs);
|
|
531
|
+
let sessionMap = pendingBySession.get(session.sessionId);
|
|
532
|
+
if (!sessionMap) {
|
|
533
|
+
sessionMap = /* @__PURE__ */ new Map();
|
|
534
|
+
pendingBySession.set(session.sessionId, sessionMap);
|
|
535
|
+
}
|
|
536
|
+
sessionMap.set(callId, { alias: session.alias, resolve, reject, timer });
|
|
537
|
+
const message = {
|
|
538
|
+
type: "tool_call",
|
|
539
|
+
session_id: session.sessionId,
|
|
540
|
+
id: callId,
|
|
541
|
+
name,
|
|
542
|
+
args
|
|
543
|
+
};
|
|
544
|
+
const sent = options.send(session.sessionId, message);
|
|
545
|
+
if (!sent) {
|
|
546
|
+
const pending = takePending(session.sessionId, callId);
|
|
547
|
+
pending?.reject(new RpcApplicationError("session_not_active", "Session has no active socket."));
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
return { callId, result };
|
|
551
|
+
};
|
|
552
|
+
const handleToolResult = (message) => {
|
|
553
|
+
const pending = takePending(message.session_id, message.id);
|
|
554
|
+
pending?.resolve(message.result);
|
|
555
|
+
};
|
|
556
|
+
const handleToolError = (message) => {
|
|
557
|
+
const pending = takePending(message.session_id, message.id);
|
|
558
|
+
pending?.reject(new RpcApplicationError(message.error.type, message.error.message, -32e3, message.error.details));
|
|
559
|
+
};
|
|
560
|
+
const handleToolCallProgress = (message) => {
|
|
561
|
+
const sessionMap = pendingBySession.get(message.session_id);
|
|
562
|
+
const pending = sessionMap?.get(message.id);
|
|
563
|
+
if (!pending) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
options.eventBus.emit({
|
|
567
|
+
// Its own EventKind (ARCHITECTURE.md §5), distinct from `tool_call_started`/
|
|
568
|
+
// `tool_call_finished` — subscribers correlate all three by `callId` in `data` without
|
|
569
|
+
// needing to sniff which kind carries a `progress`/`message` field.
|
|
570
|
+
kind: "tool_call_progress",
|
|
571
|
+
sessionId: message.session_id,
|
|
572
|
+
alias: pending.alias,
|
|
573
|
+
data: { callId: message.id, progress: message.progress, message: message.message }
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
const cancel = (sessionId, callId, reason) => {
|
|
577
|
+
const pending = pendingBySession.get(sessionId)?.get(callId);
|
|
578
|
+
if (!pending) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
const message = { type: "tool_cancel", session_id: sessionId, id: callId, reason };
|
|
582
|
+
return options.sendCancel(sessionId, message);
|
|
583
|
+
};
|
|
584
|
+
const rejectSession = (sessionId, errorType) => {
|
|
585
|
+
const sessionMap = pendingBySession.get(sessionId);
|
|
586
|
+
if (!sessionMap) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
pendingBySession.delete(sessionId);
|
|
590
|
+
for (const pending of sessionMap.values()) {
|
|
591
|
+
timers.clearTimeout(pending.timer);
|
|
592
|
+
pending.reject(new RpcApplicationError(errorType, `Session transitioned while the call was pending.`));
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const unsubscribeFromEventBus = options.eventBus.subscribe((event) => {
|
|
596
|
+
const errorType = TERMINATING_KIND_ERROR_TYPES[event.kind];
|
|
597
|
+
if (errorType && event.sessionId) {
|
|
598
|
+
rejectSession(event.sessionId, errorType);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
const disposeAll = () => {
|
|
602
|
+
unsubscribeFromEventBus();
|
|
603
|
+
for (const sessionId of Array.from(pendingBySession.keys())) {
|
|
604
|
+
rejectSession(sessionId, "unknown_session");
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
return { call, handleToolResult, handleToolError, handleToolCallProgress, cancel, rejectSession, disposeAll };
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// src/daemon/event-bus.ts
|
|
611
|
+
var TERMINAL_EVENT_KINDS = /* @__PURE__ */ new Set(["session_expired", "session_revoked"]);
|
|
612
|
+
var EXCLUDED_FROM_RETENTION = /* @__PURE__ */ new Set(["tool_call_progress"]);
|
|
613
|
+
var createEventBus = (options = {}) => {
|
|
614
|
+
const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
615
|
+
const bufferSize = options.bufferSize ?? 256;
|
|
616
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
617
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
618
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
619
|
+
const appendToBuffer = (sessionId, notification) => {
|
|
620
|
+
const buffer = buffers.get(sessionId) ?? [];
|
|
621
|
+
buffer.push(notification);
|
|
622
|
+
if (buffer.length > bufferSize) {
|
|
623
|
+
buffer.splice(0, buffer.length - bufferSize);
|
|
624
|
+
}
|
|
625
|
+
buffers.set(sessionId, buffer);
|
|
626
|
+
};
|
|
627
|
+
return {
|
|
628
|
+
emit: (event) => {
|
|
629
|
+
const sessionId = event.sessionId;
|
|
630
|
+
const seq = sessionId !== void 0 ? (cursors.get(sessionId) ?? 0) + 1 : 0;
|
|
631
|
+
if (sessionId !== void 0) {
|
|
632
|
+
cursors.set(sessionId, seq);
|
|
633
|
+
}
|
|
634
|
+
const notification = {
|
|
635
|
+
...event,
|
|
636
|
+
ts: event.ts ?? clock.now().getTime(),
|
|
637
|
+
seq
|
|
638
|
+
};
|
|
639
|
+
if (sessionId !== void 0) {
|
|
640
|
+
if (TERMINAL_EVENT_KINDS.has(notification.kind)) {
|
|
641
|
+
buffers.delete(sessionId);
|
|
642
|
+
cursors.delete(sessionId);
|
|
643
|
+
} else if (!EXCLUDED_FROM_RETENTION.has(notification.kind)) {
|
|
644
|
+
appendToBuffer(sessionId, notification);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
for (const listener of listeners) {
|
|
648
|
+
try {
|
|
649
|
+
listener(notification);
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
subscribe: (listener) => {
|
|
655
|
+
listeners.add(listener);
|
|
656
|
+
return () => {
|
|
657
|
+
listeners.delete(listener);
|
|
658
|
+
};
|
|
659
|
+
},
|
|
660
|
+
since: (sessionId, query = {}) => {
|
|
661
|
+
const buffer = buffers.get(sessionId) ?? [];
|
|
662
|
+
const sessionCursor = cursors.get(sessionId) ?? 0;
|
|
663
|
+
let events = buffer.slice();
|
|
664
|
+
if (query.since !== void 0) {
|
|
665
|
+
events = events.filter((event) => event.seq > query.since);
|
|
666
|
+
}
|
|
667
|
+
if (query.kinds !== void 0) {
|
|
668
|
+
const kinds = new Set(query.kinds);
|
|
669
|
+
events = events.filter((event) => kinds.has(event.kind));
|
|
670
|
+
}
|
|
671
|
+
if (query.limit !== void 0 && events.length > query.limit) {
|
|
672
|
+
events = events.slice(0, query.limit);
|
|
673
|
+
}
|
|
674
|
+
const cursor = events.length > 0 ? events[events.length - 1].seq : sessionCursor;
|
|
675
|
+
return { events, cursor };
|
|
676
|
+
},
|
|
677
|
+
drop: (sessionId) => {
|
|
678
|
+
buffers.delete(sessionId);
|
|
679
|
+
cursors.delete(sessionId);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/daemon/listener.ts
|
|
685
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
686
|
+
import { isSessionClaimMessage, isSessionResumeMessage, isSessionBoundMessage } from "@appduct/shared";
|
|
687
|
+
import { WebSocketServer } from "ws";
|
|
688
|
+
|
|
689
|
+
// src/daemon/sessions.ts
|
|
690
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
691
|
+
import {
|
|
692
|
+
isEventMessage,
|
|
693
|
+
isToolCallProgressMessage,
|
|
694
|
+
isToolErrorMessage,
|
|
695
|
+
isToolRegistryDeltaMessage,
|
|
696
|
+
isToolRegistrySnapshotMessage,
|
|
697
|
+
isToolResultMessage,
|
|
698
|
+
parseSessionClaimDeviceFields
|
|
699
|
+
} from "@appduct/shared";
|
|
700
|
+
|
|
701
|
+
// src/daemon/links.ts
|
|
702
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
703
|
+
import { encodeBootstrap } from "@appduct/shared";
|
|
704
|
+
var SESSION_ID_BYTES = 12;
|
|
705
|
+
var TOKEN_BYTES = 32;
|
|
706
|
+
var generateSessionId = () => {
|
|
707
|
+
let candidate = randomBytes2(SESSION_ID_BYTES).toString("base64url");
|
|
708
|
+
while (candidate.startsWith("-")) {
|
|
709
|
+
candidate = randomBytes2(SESSION_ID_BYTES).toString("base64url");
|
|
710
|
+
}
|
|
711
|
+
return candidate;
|
|
712
|
+
};
|
|
713
|
+
var createPendingLinkRegistry = (options) => {
|
|
714
|
+
const records = /* @__PURE__ */ new Map();
|
|
715
|
+
const discard = (sessionId) => {
|
|
716
|
+
const record = records.get(sessionId);
|
|
717
|
+
if (!record) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
options.timers.clearTimeout(record.timer);
|
|
721
|
+
records.delete(sessionId);
|
|
722
|
+
options.eventBus.drop(sessionId);
|
|
723
|
+
};
|
|
724
|
+
return {
|
|
725
|
+
create: (ttlSeconds, addressOverride) => {
|
|
726
|
+
const sessionId = generateSessionId();
|
|
727
|
+
const token = randomBytes2(TOKEN_BYTES);
|
|
728
|
+
const createdAt = options.clock.now();
|
|
729
|
+
const expiresAt = new Date(createdAt.getTime() + ttlSeconds * 1e3);
|
|
730
|
+
const detectedEndpoint = options.getEndpoint();
|
|
731
|
+
const endpoint = addressOverride ? {
|
|
732
|
+
family: addressOverride.includes(":") ? 6 : 4,
|
|
733
|
+
address: addressOverride,
|
|
734
|
+
port: detectedEndpoint.port
|
|
735
|
+
} : detectedEndpoint;
|
|
736
|
+
const timer = options.timers.setTimeout(() => {
|
|
737
|
+
options.eventBus.emit({ kind: "link_expired", sessionId, data: { reason: "link_ttl_expired" } });
|
|
738
|
+
const record = records.get(sessionId);
|
|
739
|
+
if (record) {
|
|
740
|
+
record.timer = options.timers.setTimeout(() => discard(sessionId), ttlSeconds * 1e3);
|
|
741
|
+
}
|
|
742
|
+
}, ttlSeconds * 1e3);
|
|
743
|
+
const link = { sessionId, token, createdAt, expiresAt };
|
|
744
|
+
records.set(sessionId, { link, attempts: 0, timer });
|
|
745
|
+
const deepLinkPayload = encodeBootstrap({
|
|
746
|
+
...endpoint,
|
|
747
|
+
sessionId,
|
|
748
|
+
token: token.toString("base64url"),
|
|
749
|
+
expiresAt: Math.floor(expiresAt.getTime() / 1e3)
|
|
750
|
+
});
|
|
751
|
+
options.eventBus.emit({ kind: "link_created", sessionId, data: { expiresAt: link.expiresAt.toISOString() } });
|
|
752
|
+
return { link, deepLinkPayload, endpoint };
|
|
753
|
+
},
|
|
754
|
+
get: (sessionId) => records.get(sessionId)?.link,
|
|
755
|
+
consume: (sessionId) => {
|
|
756
|
+
const record = records.get(sessionId);
|
|
757
|
+
if (!record) {
|
|
758
|
+
return void 0;
|
|
759
|
+
}
|
|
760
|
+
options.timers.clearTimeout(record.timer);
|
|
761
|
+
records.delete(sessionId);
|
|
762
|
+
return record.link;
|
|
763
|
+
},
|
|
764
|
+
recordFailedAttempt: (sessionId) => {
|
|
765
|
+
const record = records.get(sessionId);
|
|
766
|
+
if (!record) {
|
|
767
|
+
return 0;
|
|
768
|
+
}
|
|
769
|
+
record.attempts += 1;
|
|
770
|
+
return record.attempts;
|
|
771
|
+
},
|
|
772
|
+
discard,
|
|
773
|
+
claimableCount: () => {
|
|
774
|
+
const now = options.clock.now().getTime();
|
|
775
|
+
let count = 0;
|
|
776
|
+
for (const record of records.values()) {
|
|
777
|
+
if (record.link.expiresAt.getTime() > now) {
|
|
778
|
+
count += 1;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return count;
|
|
782
|
+
},
|
|
783
|
+
disposeAll: () => {
|
|
784
|
+
for (const sessionId of Array.from(records.keys())) {
|
|
785
|
+
discard(sessionId);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/daemon/registry.ts
|
|
792
|
+
var createToolRegistry = (onChange) => {
|
|
793
|
+
let tools = /* @__PURE__ */ new Map();
|
|
794
|
+
const notify = () => {
|
|
795
|
+
onChange?.();
|
|
796
|
+
};
|
|
797
|
+
return {
|
|
798
|
+
snapshot: (incoming) => {
|
|
799
|
+
tools = new Map(incoming.map((tool) => [tool.name, tool]));
|
|
800
|
+
notify();
|
|
801
|
+
},
|
|
802
|
+
upsert: (tool) => {
|
|
803
|
+
tools.set(tool.name, tool);
|
|
804
|
+
notify();
|
|
805
|
+
},
|
|
806
|
+
remove: (name) => {
|
|
807
|
+
if (tools.delete(name)) {
|
|
808
|
+
notify();
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
list: () => Array.from(tools.values()),
|
|
812
|
+
get: (name) => tools.get(name),
|
|
813
|
+
count: () => tools.size
|
|
814
|
+
};
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// src/daemon/sessions.ts
|
|
818
|
+
var RESUME_TOKEN_BYTES = 32;
|
|
819
|
+
var POST_CLAIM_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
820
|
+
"tool_registry_snapshot",
|
|
821
|
+
"tool_registry_delta",
|
|
822
|
+
"tool_result",
|
|
823
|
+
"tool_error",
|
|
824
|
+
"tool_call_progress",
|
|
825
|
+
"event"
|
|
826
|
+
]);
|
|
827
|
+
var isPostClaimMessageType = (type) => {
|
|
828
|
+
return typeof type === "string" && POST_CLAIM_MESSAGE_TYPES.has(type);
|
|
829
|
+
};
|
|
830
|
+
var slugifyDeviceModel = (model) => {
|
|
831
|
+
if (!model) {
|
|
832
|
+
return "device";
|
|
833
|
+
}
|
|
834
|
+
const slug = model.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "");
|
|
835
|
+
return slug.length > 0 ? slug : "device";
|
|
836
|
+
};
|
|
837
|
+
var dedupeAlias = (base, existingAliases) => {
|
|
838
|
+
if (!existingAliases.has(base)) {
|
|
839
|
+
return base;
|
|
840
|
+
}
|
|
841
|
+
let suffix = 2;
|
|
842
|
+
while (existingAliases.has(`${base}-${suffix}`)) {
|
|
843
|
+
suffix += 1;
|
|
844
|
+
}
|
|
845
|
+
return `${base}-${suffix}`;
|
|
846
|
+
};
|
|
847
|
+
var decodeFixedLengthToken = (base64url, expectedLength) => {
|
|
848
|
+
let decoded;
|
|
849
|
+
try {
|
|
850
|
+
decoded = Buffer.from(base64url, "base64url");
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
return decoded.length === expectedLength ? decoded : null;
|
|
855
|
+
};
|
|
856
|
+
var tokensMatch = (candidate, expected) => {
|
|
857
|
+
if (!candidate || candidate.length !== expected.length) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
return timingSafeEqual(candidate, expected);
|
|
861
|
+
};
|
|
862
|
+
var MAX_FAILED_CLAIM_ATTEMPTS = 5;
|
|
863
|
+
var closeSocket = (socket, code, reason) => {
|
|
864
|
+
try {
|
|
865
|
+
socket.close(code, reason);
|
|
866
|
+
} catch {
|
|
867
|
+
socket.terminate();
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
var createSessionManager = (options) => {
|
|
871
|
+
const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
872
|
+
const timers = options.timers ?? systemTimers;
|
|
873
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
874
|
+
const pendingLinks = createPendingLinkRegistry({
|
|
875
|
+
getEndpoint: options.getEndpoint,
|
|
876
|
+
clock,
|
|
877
|
+
timers,
|
|
878
|
+
eventBus: options.eventBus
|
|
879
|
+
});
|
|
880
|
+
const emit = (kind, sessionId, alias, data) => {
|
|
881
|
+
options.eventBus.emit({ kind, sessionId, alias, data });
|
|
882
|
+
};
|
|
883
|
+
const toSummary = (session) => {
|
|
884
|
+
return {
|
|
885
|
+
sessionId: session.sessionId,
|
|
886
|
+
alias: session.alias,
|
|
887
|
+
state: session.state,
|
|
888
|
+
device: session.device,
|
|
889
|
+
createdAt: session.createdAt.toISOString(),
|
|
890
|
+
claimedAt: session.claimedAt.toISOString(),
|
|
891
|
+
...session.suspendedAt ? { suspendedAt: session.suspendedAt.toISOString() } : {},
|
|
892
|
+
toolCount: session.registry.count()
|
|
893
|
+
};
|
|
894
|
+
};
|
|
895
|
+
const resolveSession = (selector) => {
|
|
896
|
+
const live = Array.from(sessions.values());
|
|
897
|
+
if (selector !== void 0) {
|
|
898
|
+
const match = live.find((session) => session.sessionId === selector || session.alias === selector);
|
|
899
|
+
if (!match) {
|
|
900
|
+
throw new RpcApplicationError("unknown_session", `No session matches "${selector}".`);
|
|
901
|
+
}
|
|
902
|
+
return match;
|
|
903
|
+
}
|
|
904
|
+
if (live.length === 0) {
|
|
905
|
+
throw new RpcApplicationError("no_session", "No active or suspended session, and none was specified.");
|
|
906
|
+
}
|
|
907
|
+
if (live.length > 1) {
|
|
908
|
+
throw new RpcApplicationError(
|
|
909
|
+
"ambiguous_session",
|
|
910
|
+
`Multiple sessions are live (${live.map((session) => session.alias).join(", ")}); specify a selector.`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
return live[0];
|
|
914
|
+
};
|
|
915
|
+
const assignAlias = (model) => {
|
|
916
|
+
const base = slugifyDeviceModel(model);
|
|
917
|
+
const existing = new Set(Array.from(sessions.values(), (session) => session.alias));
|
|
918
|
+
return dedupeAlias(base, existing);
|
|
919
|
+
};
|
|
920
|
+
const sendAck = (socket, session) => {
|
|
921
|
+
const ack = {
|
|
922
|
+
type: "session_ack",
|
|
923
|
+
session_id: session.sessionId,
|
|
924
|
+
status: "ok",
|
|
925
|
+
alias: session.alias,
|
|
926
|
+
resume_token: session.resumeToken.toString("base64url"),
|
|
927
|
+
keepalive_interval_s: options.keepaliveIntervalSeconds,
|
|
928
|
+
grace_s: options.graceSeconds
|
|
929
|
+
};
|
|
930
|
+
socket.send(JSON.stringify(ack), (error) => {
|
|
931
|
+
if (error) {
|
|
932
|
+
closeSocket(socket, 1011, "send_failed");
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
};
|
|
936
|
+
const suspend = (sessionId) => {
|
|
937
|
+
const session = sessions.get(sessionId);
|
|
938
|
+
if (!session || session.state !== "active") {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
session.state = "suspended";
|
|
942
|
+
session.suspendedAt = clock.now();
|
|
943
|
+
session.socket = void 0;
|
|
944
|
+
session.missedPongs = 0;
|
|
945
|
+
session.graceTimer = timers.setTimeout(() => expire(sessionId), options.graceSeconds * 1e3);
|
|
946
|
+
emit("session_suspended", session.sessionId, session.alias, {});
|
|
947
|
+
};
|
|
948
|
+
const expire = (sessionId) => {
|
|
949
|
+
const session = sessions.get(sessionId);
|
|
950
|
+
if (!session || session.state !== "suspended") {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
sessions.delete(sessionId);
|
|
954
|
+
emit("session_expired", session.sessionId, session.alias, {});
|
|
955
|
+
};
|
|
956
|
+
const attachSocketHandlers = (session) => {
|
|
957
|
+
const socket = session.socket;
|
|
958
|
+
socket.on("error", () => {
|
|
959
|
+
});
|
|
960
|
+
socket.on("pong", () => {
|
|
961
|
+
session.missedPongs = 0;
|
|
962
|
+
});
|
|
963
|
+
socket.on("close", () => {
|
|
964
|
+
if (session.socket === socket) {
|
|
965
|
+
suspend(session.sessionId);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
};
|
|
969
|
+
const keepaliveInterval = timers.setInterval(() => {
|
|
970
|
+
for (const session of sessions.values()) {
|
|
971
|
+
if (session.state !== "active" || !session.socket) {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (session.missedPongs >= 2) {
|
|
975
|
+
const staleSocket = session.socket;
|
|
976
|
+
session.socket = void 0;
|
|
977
|
+
suspend(session.sessionId);
|
|
978
|
+
staleSocket.terminate();
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
session.missedPongs += 1;
|
|
982
|
+
try {
|
|
983
|
+
session.socket.ping();
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}, options.keepaliveIntervalSeconds * 1e3);
|
|
988
|
+
const createLink = (ttlSeconds, addressOverride) => {
|
|
989
|
+
const { link, deepLinkPayload, endpoint } = pendingLinks.create(
|
|
990
|
+
ttlSeconds ?? options.linkTtlSeconds,
|
|
991
|
+
addressOverride
|
|
992
|
+
);
|
|
993
|
+
return {
|
|
994
|
+
sessionId: link.sessionId,
|
|
995
|
+
deepLinkPayload,
|
|
996
|
+
endpoint,
|
|
997
|
+
expiresAt: Math.floor(link.expiresAt.getTime() / 1e3)
|
|
998
|
+
};
|
|
999
|
+
};
|
|
1000
|
+
const handleClaim = (socket, message) => {
|
|
1001
|
+
const pending = pendingLinks.get(message.session_id);
|
|
1002
|
+
if (!pending) {
|
|
1003
|
+
if (sessions.has(message.session_id)) {
|
|
1004
|
+
closeSocket(socket, 1008, "already_claimed");
|
|
1005
|
+
} else {
|
|
1006
|
+
closeSocket(socket, 1008, "unknown_session");
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
const nowMs = clock.now().getTime();
|
|
1011
|
+
if (pending.expiresAt.getTime() <= nowMs) {
|
|
1012
|
+
pendingLinks.discard(pending.sessionId);
|
|
1013
|
+
closeSocket(socket, 1008, "link_expired");
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const candidateToken = decodeFixedLengthToken(message.token, pending.token.length);
|
|
1017
|
+
if (!tokensMatch(candidateToken, pending.token)) {
|
|
1018
|
+
const attempts = pendingLinks.recordFailedAttempt(pending.sessionId);
|
|
1019
|
+
if (attempts >= MAX_FAILED_CLAIM_ATTEMPTS) {
|
|
1020
|
+
pendingLinks.discard(pending.sessionId);
|
|
1021
|
+
closeSocket(socket, 1008, "claim_attempts_exceeded");
|
|
1022
|
+
} else {
|
|
1023
|
+
closeSocket(socket, 1008, "invalid_token");
|
|
1024
|
+
}
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
pendingLinks.consume(pending.sessionId);
|
|
1028
|
+
const deviceFields = parseSessionClaimDeviceFields(message);
|
|
1029
|
+
const device = deviceFields && deviceFields !== "invalid" ? deviceFields : {};
|
|
1030
|
+
const alias = assignAlias(device.model);
|
|
1031
|
+
const sessionId = pending.sessionId;
|
|
1032
|
+
const session = {
|
|
1033
|
+
sessionId,
|
|
1034
|
+
alias,
|
|
1035
|
+
state: "active",
|
|
1036
|
+
device,
|
|
1037
|
+
createdAt: pending.createdAt,
|
|
1038
|
+
claimedAt: clock.now(),
|
|
1039
|
+
resumeToken: randomBytes3(RESUME_TOKEN_BYTES),
|
|
1040
|
+
socket,
|
|
1041
|
+
missedPongs: 0,
|
|
1042
|
+
registry: createToolRegistry(() => {
|
|
1043
|
+
emit("tools_changed", sessionId, alias, { toolCount: session.registry.count() });
|
|
1044
|
+
})
|
|
1045
|
+
};
|
|
1046
|
+
sessions.set(sessionId, session);
|
|
1047
|
+
attachSocketHandlers(session);
|
|
1048
|
+
sendAck(socket, session);
|
|
1049
|
+
emit("session_claimed", sessionId, alias, { device });
|
|
1050
|
+
return sessionId;
|
|
1051
|
+
};
|
|
1052
|
+
const handleResume = (socket, message) => {
|
|
1053
|
+
const session = sessions.get(message.session_id);
|
|
1054
|
+
if (!session) {
|
|
1055
|
+
closeSocket(socket, 1008, "unknown_session");
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
const candidateToken = decodeFixedLengthToken(message.resume_token, session.resumeToken.length);
|
|
1059
|
+
if (!tokensMatch(candidateToken, session.resumeToken)) {
|
|
1060
|
+
closeSocket(socket, 1008, "invalid_resume_token");
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
if (session.state === "active" && session.socket && session.socket !== socket) {
|
|
1064
|
+
const staleSocket = session.socket;
|
|
1065
|
+
session.socket = void 0;
|
|
1066
|
+
closeSocket(staleSocket, 1e3, "session_replaced");
|
|
1067
|
+
}
|
|
1068
|
+
if (session.graceTimer) {
|
|
1069
|
+
timers.clearTimeout(session.graceTimer);
|
|
1070
|
+
session.graceTimer = void 0;
|
|
1071
|
+
}
|
|
1072
|
+
session.resumeToken = randomBytes3(RESUME_TOKEN_BYTES);
|
|
1073
|
+
session.state = "active";
|
|
1074
|
+
session.suspendedAt = void 0;
|
|
1075
|
+
session.socket = socket;
|
|
1076
|
+
session.missedPongs = 0;
|
|
1077
|
+
attachSocketHandlers(session);
|
|
1078
|
+
sendAck(socket, session);
|
|
1079
|
+
emit("session_resumed", session.sessionId, session.alias, {});
|
|
1080
|
+
return session.sessionId;
|
|
1081
|
+
};
|
|
1082
|
+
const handlePostClaimMessage = (sessionId, socket, message) => {
|
|
1083
|
+
const session = sessions.get(sessionId);
|
|
1084
|
+
if (!session) {
|
|
1085
|
+
closeSocket(socket, 1008, "unknown_session");
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
switch (message.type) {
|
|
1089
|
+
case "tool_registry_snapshot": {
|
|
1090
|
+
if (!isToolRegistrySnapshotMessage(message)) {
|
|
1091
|
+
closeSocket(socket, 1008, "invalid_registry");
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
session.registry.snapshot(message.tools);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
case "tool_registry_delta": {
|
|
1098
|
+
if (!isToolRegistryDeltaMessage(message)) {
|
|
1099
|
+
closeSocket(socket, 1008, "invalid_registry");
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (message.operation === "upsert") {
|
|
1103
|
+
session.registry.upsert(message.tool);
|
|
1104
|
+
} else {
|
|
1105
|
+
session.registry.remove(message.name);
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
case "tool_result": {
|
|
1110
|
+
if (!isToolResultMessage(message)) {
|
|
1111
|
+
closeSocket(socket, 1008, "invalid_message");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
options.onToolFrame?.(message);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
case "tool_error": {
|
|
1118
|
+
if (!isToolErrorMessage(message)) {
|
|
1119
|
+
closeSocket(socket, 1008, "invalid_message");
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
options.onToolFrame?.(message);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
case "tool_call_progress": {
|
|
1126
|
+
if (!isToolCallProgressMessage(message)) {
|
|
1127
|
+
closeSocket(socket, 1008, "invalid_message");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
options.onToolFrame?.(message);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
case "event": {
|
|
1134
|
+
if (!isEventMessage(message)) {
|
|
1135
|
+
closeSocket(socket, 1008, "invalid_message");
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
options.onAppEvent?.(sessionId, session.alias, message);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
default:
|
|
1142
|
+
closeSocket(socket, 1008, "unknown_message_type");
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
const revoke = (selector) => {
|
|
1146
|
+
if (selector !== void 0 && !sessions.has(selector) && pendingLinks.get(selector)) {
|
|
1147
|
+
pendingLinks.discard(selector);
|
|
1148
|
+
emit("session_revoked", selector, void 0, {});
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const session = resolveSession(selector);
|
|
1152
|
+
if (session.graceTimer) {
|
|
1153
|
+
timers.clearTimeout(session.graceTimer);
|
|
1154
|
+
}
|
|
1155
|
+
if (session.socket) {
|
|
1156
|
+
closeSocket(session.socket, 1e3, "revoked");
|
|
1157
|
+
}
|
|
1158
|
+
sessions.delete(session.sessionId);
|
|
1159
|
+
emit("session_revoked", session.sessionId, session.alias, {});
|
|
1160
|
+
};
|
|
1161
|
+
const disposeAll = (code, reason) => {
|
|
1162
|
+
timers.clearInterval(keepaliveInterval);
|
|
1163
|
+
pendingLinks.disposeAll();
|
|
1164
|
+
for (const session of sessions.values()) {
|
|
1165
|
+
if (session.graceTimer) {
|
|
1166
|
+
timers.clearTimeout(session.graceTimer);
|
|
1167
|
+
}
|
|
1168
|
+
if (session.socket) {
|
|
1169
|
+
closeSocket(session.socket, code, reason);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
sessions.clear();
|
|
1173
|
+
};
|
|
1174
|
+
const resolveForTools = (selector) => {
|
|
1175
|
+
const session = resolveSession(selector);
|
|
1176
|
+
return { sessionId: session.sessionId, alias: session.alias, state: session.state, registry: session.registry };
|
|
1177
|
+
};
|
|
1178
|
+
const sendToApp = (sessionId, message) => {
|
|
1179
|
+
const session = sessions.get(sessionId);
|
|
1180
|
+
if (!session || session.state !== "active" || !session.socket) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
const socket = session.socket;
|
|
1184
|
+
socket.send(JSON.stringify(message), (error) => {
|
|
1185
|
+
if (error) {
|
|
1186
|
+
closeSocket(socket, 1011, "send_failed");
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
return true;
|
|
1190
|
+
};
|
|
1191
|
+
return {
|
|
1192
|
+
createLink,
|
|
1193
|
+
handleClaim,
|
|
1194
|
+
handleResume,
|
|
1195
|
+
handlePostClaimMessage,
|
|
1196
|
+
list: () => Array.from(sessions.values(), toSummary),
|
|
1197
|
+
pendingLinkCount: () => pendingLinks.claimableCount(),
|
|
1198
|
+
describe: (selector) => toSummary(resolveSession(selector)),
|
|
1199
|
+
revoke,
|
|
1200
|
+
resolveForTools,
|
|
1201
|
+
sendToolCall: sendToApp,
|
|
1202
|
+
sendToolCancel: sendToApp,
|
|
1203
|
+
disposeAll
|
|
1204
|
+
};
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// src/daemon/listener.ts
|
|
1208
|
+
var DEFAULT_PRE_CLAIM_TIMEOUT_MS = 1e4;
|
|
1209
|
+
var MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
1210
|
+
var isRecord = (value) => {
|
|
1211
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1212
|
+
};
|
|
1213
|
+
var startListener = async (options) => {
|
|
1214
|
+
const timers = options.timers ?? systemTimers;
|
|
1215
|
+
const preClaimTimeoutMs = options.preClaimTimeoutMs ?? DEFAULT_PRE_CLAIM_TIMEOUT_MS;
|
|
1216
|
+
const material = options.tls.current();
|
|
1217
|
+
const httpsServer = createHttpsServer({ key: material.keyPem, cert: material.certPem });
|
|
1218
|
+
httpsServer.on("error", () => {
|
|
1219
|
+
});
|
|
1220
|
+
const wss = new WebSocketServer({ server: httpsServer, maxPayload: MAX_PAYLOAD_BYTES });
|
|
1221
|
+
wss.on("error", () => {
|
|
1222
|
+
});
|
|
1223
|
+
wss.on("connection", (socket) => {
|
|
1224
|
+
socket.on("error", () => {
|
|
1225
|
+
});
|
|
1226
|
+
let claimedSessionId = null;
|
|
1227
|
+
const preClaimTimer = timers.setTimeout(() => {
|
|
1228
|
+
if (!claimedSessionId) {
|
|
1229
|
+
socket.close(1008, "pre_claim_timeout");
|
|
1230
|
+
}
|
|
1231
|
+
}, preClaimTimeoutMs);
|
|
1232
|
+
socket.once("close", () => {
|
|
1233
|
+
timers.clearTimeout(preClaimTimer);
|
|
1234
|
+
});
|
|
1235
|
+
socket.on("message", (data, isBinary) => {
|
|
1236
|
+
if (isBinary) {
|
|
1237
|
+
socket.close(1003, "binary_frame_not_supported");
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
let parsed;
|
|
1241
|
+
try {
|
|
1242
|
+
parsed = JSON.parse(data.toString("utf8"));
|
|
1243
|
+
} catch {
|
|
1244
|
+
socket.close(1008, "invalid_json");
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (!isRecord(parsed)) {
|
|
1248
|
+
socket.close(1008, "invalid_json");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (claimedSessionId === null) {
|
|
1252
|
+
if (isSessionClaimMessage(parsed)) {
|
|
1253
|
+
const result = options.sessionManager.handleClaim(socket, parsed);
|
|
1254
|
+
if (result) {
|
|
1255
|
+
claimedSessionId = result;
|
|
1256
|
+
timers.clearTimeout(preClaimTimer);
|
|
1257
|
+
}
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (isSessionResumeMessage(parsed)) {
|
|
1261
|
+
const result = options.sessionManager.handleResume(socket, parsed);
|
|
1262
|
+
if (result) {
|
|
1263
|
+
claimedSessionId = result;
|
|
1264
|
+
timers.clearTimeout(preClaimTimer);
|
|
1265
|
+
}
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
socket.close(1008, "expected_claim_or_resume");
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (!isPostClaimMessageType(parsed.type)) {
|
|
1272
|
+
socket.close(1008, "unknown_message_type");
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (!isSessionBoundMessage(parsed) || parsed.session_id !== claimedSessionId) {
|
|
1276
|
+
socket.close(1008, "session_mismatch");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
options.sessionManager.handlePostClaimMessage(claimedSessionId, socket, parsed);
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
await new Promise((resolve, reject) => {
|
|
1283
|
+
const onError = (error) => {
|
|
1284
|
+
httpsServer.off("listening", onListening);
|
|
1285
|
+
reject(error);
|
|
1286
|
+
};
|
|
1287
|
+
const onListening = () => {
|
|
1288
|
+
httpsServer.off("error", onError);
|
|
1289
|
+
resolve();
|
|
1290
|
+
};
|
|
1291
|
+
httpsServer.once("error", onError);
|
|
1292
|
+
httpsServer.once("listening", onListening);
|
|
1293
|
+
httpsServer.listen(options.port);
|
|
1294
|
+
});
|
|
1295
|
+
return {
|
|
1296
|
+
httpsServer,
|
|
1297
|
+
wss,
|
|
1298
|
+
port: () => {
|
|
1299
|
+
const address = httpsServer.address();
|
|
1300
|
+
return address && typeof address !== "string" ? address.port : void 0;
|
|
1301
|
+
},
|
|
1302
|
+
applyTls: (nextMaterial) => {
|
|
1303
|
+
const server = httpsServer;
|
|
1304
|
+
server.setSecureContext?.({ key: nextMaterial.keyPem, cert: nextMaterial.certPem });
|
|
1305
|
+
},
|
|
1306
|
+
close: () => new Promise((resolve) => {
|
|
1307
|
+
for (const client of wss.clients) {
|
|
1308
|
+
client.terminate();
|
|
1309
|
+
}
|
|
1310
|
+
wss.close(() => {
|
|
1311
|
+
httpsServer.closeAllConnections();
|
|
1312
|
+
httpsServer.close(() => resolve());
|
|
1313
|
+
});
|
|
1314
|
+
})
|
|
1315
|
+
};
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
// src/daemon/policy.ts
|
|
1319
|
+
var policyOverrideKey = (alias, toolName) => `${alias}/${toolName}`;
|
|
1320
|
+
var evaluate = (descriptor, session, policy) => {
|
|
1321
|
+
const override = policy.tools?.[policyOverrideKey(session.alias, descriptor.name)];
|
|
1322
|
+
if (override !== void 0) {
|
|
1323
|
+
return override;
|
|
1324
|
+
}
|
|
1325
|
+
if (descriptor.annotations?.destructiveHint === true) {
|
|
1326
|
+
return policy.destructive;
|
|
1327
|
+
}
|
|
1328
|
+
return policy.default;
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// src/daemon/pidfile.ts
|
|
1332
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1333
|
+
import { open as open2, readFile as readFile2, rm as rm2 } from "node:fs/promises";
|
|
1334
|
+
var DaemonAlreadyRunningError = class extends Error {
|
|
1335
|
+
constructor(pid) {
|
|
1336
|
+
super(`Appduct daemon is already running (pid ${pid}).`);
|
|
1337
|
+
this.pid = pid;
|
|
1338
|
+
this.name = "DaemonAlreadyRunningError";
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
var readProcStatus = (pid) => {
|
|
1342
|
+
try {
|
|
1343
|
+
return readFileSync3(`/proc/${pid}/status`, "utf8");
|
|
1344
|
+
} catch {
|
|
1345
|
+
return void 0;
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
var isZombie = (pid, readStatus) => {
|
|
1349
|
+
const status = readStatus(pid);
|
|
1350
|
+
if (status === void 0) {
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
return /^State:\s*Z\b/mu.test(status);
|
|
1354
|
+
};
|
|
1355
|
+
var isProcessAlive = (pid, readStatus = readProcStatus) => {
|
|
1356
|
+
try {
|
|
1357
|
+
process.kill(pid, 0);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
return error.code === "EPERM";
|
|
1360
|
+
}
|
|
1361
|
+
return !isZombie(pid, readStatus);
|
|
1362
|
+
};
|
|
1363
|
+
var readPidFromFile = async (pidFilePath) => {
|
|
1364
|
+
let contents;
|
|
1365
|
+
try {
|
|
1366
|
+
contents = await readFile2(pidFilePath, "utf8");
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (error.code === "ENOENT") {
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1371
|
+
throw error;
|
|
1372
|
+
}
|
|
1373
|
+
const pid = Number.parseInt(contents.trim(), 10);
|
|
1374
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
1375
|
+
};
|
|
1376
|
+
var acquirePidfile = async (pidFilePath, options = {}) => {
|
|
1377
|
+
const pid = options.pid ?? process.pid;
|
|
1378
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
1379
|
+
try {
|
|
1380
|
+
const handle = await open2(pidFilePath, "wx", 384);
|
|
1381
|
+
try {
|
|
1382
|
+
await handle.writeFile(String(pid), "utf8");
|
|
1383
|
+
} finally {
|
|
1384
|
+
await handle.close();
|
|
1385
|
+
}
|
|
1386
|
+
let released = false;
|
|
1387
|
+
return {
|
|
1388
|
+
pid,
|
|
1389
|
+
release: async () => {
|
|
1390
|
+
if (released) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
released = true;
|
|
1394
|
+
await rm2(pidFilePath, { force: true });
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
if (error.code !== "EEXIST") {
|
|
1399
|
+
throw error;
|
|
1400
|
+
}
|
|
1401
|
+
const existingPid = await readPidFromFile(pidFilePath);
|
|
1402
|
+
if (existingPid !== void 0 && isProcessAlive(existingPid)) {
|
|
1403
|
+
throw new DaemonAlreadyRunningError(existingPid);
|
|
1404
|
+
}
|
|
1405
|
+
await rm2(pidFilePath, { force: true });
|
|
1406
|
+
await options.onStaleTakeover?.();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
throw new Error(`Failed to acquire the Appduct daemon pidfile at "${pidFilePath}".`);
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// src/daemon/rpc-server.ts
|
|
1413
|
+
import { createServer } from "node:net";
|
|
1414
|
+
import { chmod as chmod2, rm as rm3 } from "node:fs/promises";
|
|
1415
|
+
var MAX_LINE_BYTES = 1024 * 1024;
|
|
1416
|
+
var MAX_NOTIFY_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
1417
|
+
var isPlainObject = (value) => {
|
|
1418
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1419
|
+
};
|
|
1420
|
+
var hasRequestId = (value) => {
|
|
1421
|
+
return "id" in value;
|
|
1422
|
+
};
|
|
1423
|
+
var writeErrorLine = (socket, id, code, message, data) => {
|
|
1424
|
+
writeLine(socket, {
|
|
1425
|
+
jsonrpc: "2.0",
|
|
1426
|
+
id,
|
|
1427
|
+
error: { code, message, ...data ? { data } : {} }
|
|
1428
|
+
});
|
|
1429
|
+
};
|
|
1430
|
+
var writeLine = (socket, payload, onFlushed) => {
|
|
1431
|
+
if (socket.destroyed) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
socket.write(`${JSON.stringify(payload)}
|
|
1435
|
+
`, (error) => {
|
|
1436
|
+
if (!error) {
|
|
1437
|
+
onFlushed?.();
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
};
|
|
1441
|
+
var handleLine = async (line, connection, dispatch, registerOnClose) => {
|
|
1442
|
+
const { socket } = connection;
|
|
1443
|
+
let request;
|
|
1444
|
+
try {
|
|
1445
|
+
const parsed = JSON.parse(line);
|
|
1446
|
+
if (!isPlainObject(parsed)) {
|
|
1447
|
+
writeErrorLine(socket, null, -32600, "Invalid Request: expected a JSON object.");
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
request = parsed;
|
|
1451
|
+
} catch {
|
|
1452
|
+
writeErrorLine(socket, null, -32700, "Parse error: invalid JSON.");
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const id = hasRequestId(request) ? request.id : null;
|
|
1456
|
+
if (typeof request.method !== "string" || request.method.length === 0) {
|
|
1457
|
+
writeErrorLine(socket, id, -32600, 'Invalid Request: "method" must be a non-empty string.');
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const handler = dispatch[request.method];
|
|
1461
|
+
if (!handler) {
|
|
1462
|
+
writeErrorLine(socket, id, -32601, `Method not found: "${request.method}".`);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const afterSendCallbacks = [];
|
|
1466
|
+
const context = {
|
|
1467
|
+
connection,
|
|
1468
|
+
afterSend: (callback) => {
|
|
1469
|
+
afterSendCallbacks.push(callback);
|
|
1470
|
+
},
|
|
1471
|
+
onClose: registerOnClose
|
|
1472
|
+
};
|
|
1473
|
+
try {
|
|
1474
|
+
const result = await handler(request.params, context);
|
|
1475
|
+
writeLine(socket, { jsonrpc: "2.0", id, result: result ?? null }, () => {
|
|
1476
|
+
for (const callback of afterSendCallbacks) {
|
|
1477
|
+
callback();
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
if (error instanceof RpcApplicationError) {
|
|
1482
|
+
writeErrorLine(socket, id, error.code, error.message, {
|
|
1483
|
+
type: error.type,
|
|
1484
|
+
...error.details !== void 0 ? { details: error.details } : {}
|
|
1485
|
+
});
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const message = error instanceof Error ? error.message : "Internal error.";
|
|
1489
|
+
writeErrorLine(socket, id, -32603, message, { type: "invalid_request" });
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
var startRpcServer = async (options) => {
|
|
1493
|
+
const maxLineBytes = options.maxLineBytes ?? MAX_LINE_BYTES;
|
|
1494
|
+
const connections = /* @__PURE__ */ new Map();
|
|
1495
|
+
let nextConnectionId = 1;
|
|
1496
|
+
await rm3(options.socketPath, { force: true });
|
|
1497
|
+
const server = createServer((socket) => {
|
|
1498
|
+
socket.on("error", () => {
|
|
1499
|
+
});
|
|
1500
|
+
const id = nextConnectionId;
|
|
1501
|
+
nextConnectionId += 1;
|
|
1502
|
+
const connection = { id, socket, state: {} };
|
|
1503
|
+
connections.set(id, connection);
|
|
1504
|
+
const closeCallbacks = /* @__PURE__ */ new Set();
|
|
1505
|
+
const registerOnClose = (callback) => {
|
|
1506
|
+
closeCallbacks.add(callback);
|
|
1507
|
+
return () => {
|
|
1508
|
+
closeCallbacks.delete(callback);
|
|
1509
|
+
};
|
|
1510
|
+
};
|
|
1511
|
+
let buffer = "";
|
|
1512
|
+
socket.on("data", (chunk) => {
|
|
1513
|
+
buffer += chunk.toString("utf8");
|
|
1514
|
+
if (Buffer.byteLength(buffer, "utf8") > maxLineBytes && !buffer.includes("\n")) {
|
|
1515
|
+
socket.destroy();
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
1519
|
+
while (newlineIndex !== -1) {
|
|
1520
|
+
const line = buffer.slice(0, newlineIndex).replace(/\r$/u, "");
|
|
1521
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
1522
|
+
if (line.length > 0) {
|
|
1523
|
+
if (Buffer.byteLength(line, "utf8") > maxLineBytes) {
|
|
1524
|
+
writeErrorLine(
|
|
1525
|
+
socket,
|
|
1526
|
+
null,
|
|
1527
|
+
-32600,
|
|
1528
|
+
`Invalid Request: line exceeds the ${maxLineBytes}-byte limit.`
|
|
1529
|
+
);
|
|
1530
|
+
socket.destroy();
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
void handleLine(line, connection, options.dispatch, registerOnClose);
|
|
1534
|
+
}
|
|
1535
|
+
newlineIndex = buffer.indexOf("\n");
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
socket.on("close", () => {
|
|
1539
|
+
connections.delete(id);
|
|
1540
|
+
const callbacks = Array.from(closeCallbacks);
|
|
1541
|
+
closeCallbacks.clear();
|
|
1542
|
+
for (const callback of callbacks) {
|
|
1543
|
+
try {
|
|
1544
|
+
callback();
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
server.on("error", () => {
|
|
1551
|
+
});
|
|
1552
|
+
await new Promise((resolve, reject) => {
|
|
1553
|
+
const onError = (error) => {
|
|
1554
|
+
server.off("listening", onListening);
|
|
1555
|
+
reject(error);
|
|
1556
|
+
};
|
|
1557
|
+
const onListening = () => {
|
|
1558
|
+
server.off("error", onError);
|
|
1559
|
+
resolve();
|
|
1560
|
+
};
|
|
1561
|
+
server.once("error", onError);
|
|
1562
|
+
server.once("listening", onListening);
|
|
1563
|
+
server.listen(options.socketPath);
|
|
1564
|
+
});
|
|
1565
|
+
await chmod2(options.socketPath, 384);
|
|
1566
|
+
return {
|
|
1567
|
+
server,
|
|
1568
|
+
notify: (connection, payload) => {
|
|
1569
|
+
if (connection.socket.destroyed) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (connection.socket.writableLength > MAX_NOTIFY_BUFFERED_BYTES) {
|
|
1573
|
+
connection.socket.destroy();
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
writeLine(connection.socket, { jsonrpc: "2.0", method: "event", params: payload });
|
|
1577
|
+
},
|
|
1578
|
+
connections: () => Array.from(connections.values()),
|
|
1579
|
+
close: () => new Promise((resolve) => {
|
|
1580
|
+
for (const connection of connections.values()) {
|
|
1581
|
+
connection.socket.destroy();
|
|
1582
|
+
}
|
|
1583
|
+
server.close(() => resolve());
|
|
1584
|
+
})
|
|
1585
|
+
};
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
// src/daemon/state-dir.ts
|
|
1589
|
+
import { join as join4 } from "node:path";
|
|
1590
|
+
import { mkdir as mkdir2, chmod as chmod3 } from "node:fs/promises";
|
|
1591
|
+
var getStateDirPaths = (stateDir) => {
|
|
1592
|
+
return {
|
|
1593
|
+
root: stateDir,
|
|
1594
|
+
socketPath: join4(stateDir, "daemon.sock"),
|
|
1595
|
+
pidFilePath: join4(stateDir, "daemon.pid"),
|
|
1596
|
+
logFilePath: join4(stateDir, "daemon.log"),
|
|
1597
|
+
keyPath: join4(stateDir, "key.pem"),
|
|
1598
|
+
configPath: join4(stateDir, "config.json"),
|
|
1599
|
+
auditDir: join4(stateDir, "audit"),
|
|
1600
|
+
spawnLockPath: join4(stateDir, "daemon.spawn.lock")
|
|
1601
|
+
};
|
|
1602
|
+
};
|
|
1603
|
+
var getSocketPath = (paths) => {
|
|
1604
|
+
return paths.socketPath;
|
|
1605
|
+
};
|
|
1606
|
+
var ensureStateDir = async (stateDir) => {
|
|
1607
|
+
const paths = getStateDirPaths(stateDir);
|
|
1608
|
+
await mkdir2(paths.auditDir, { recursive: true });
|
|
1609
|
+
await chmod3(paths.root, 448);
|
|
1610
|
+
await chmod3(paths.auditDir, 448);
|
|
1611
|
+
return paths;
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
// src/daemon/tls.ts
|
|
1615
|
+
import { stat as stat2, readFile as readFile3 } from "node:fs/promises";
|
|
1616
|
+
|
|
1617
|
+
// src/host-certificate.ts
|
|
1618
|
+
import "reflect-metadata";
|
|
1619
|
+
import {
|
|
1620
|
+
createPrivateKey as createPrivateKey2,
|
|
1621
|
+
createPublicKey as createPublicKey2,
|
|
1622
|
+
webcrypto
|
|
1623
|
+
} from "node:crypto";
|
|
1624
|
+
import {
|
|
1625
|
+
BasicConstraintsExtension,
|
|
1626
|
+
DNS,
|
|
1627
|
+
ExtendedKeyUsage,
|
|
1628
|
+
ExtendedKeyUsageExtension,
|
|
1629
|
+
IP,
|
|
1630
|
+
KeyUsageFlags,
|
|
1631
|
+
KeyUsagesExtension,
|
|
1632
|
+
SubjectAlternativeNameExtension,
|
|
1633
|
+
SubjectKeyIdentifierExtension,
|
|
1634
|
+
X509CertificateGenerator
|
|
1635
|
+
} from "@peculiar/x509";
|
|
1636
|
+
|
|
1637
|
+
// src/spki-pin.ts
|
|
1638
|
+
import { createHash as createHash2, createPrivateKey, createPublicKey } from "node:crypto";
|
|
1639
|
+
var createSpkiPin = (spkiDer) => {
|
|
1640
|
+
const digest = createHash2("sha256").update(spkiDer).digest("base64");
|
|
1641
|
+
return `sha256/${digest}`;
|
|
1642
|
+
};
|
|
1643
|
+
var getSpkiPinFromPrivateKeyPem = (keyPem) => {
|
|
1644
|
+
const privateKey = createPrivateKey(keyPem);
|
|
1645
|
+
const publicKey = createPublicKey(privateKey);
|
|
1646
|
+
const spkiDer = Buffer.from(
|
|
1647
|
+
publicKey.export({
|
|
1648
|
+
type: "spki",
|
|
1649
|
+
format: "der"
|
|
1650
|
+
})
|
|
1651
|
+
);
|
|
1652
|
+
return createSpkiPin(spkiDer);
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
// src/host-certificate.ts
|
|
1656
|
+
var CERT_VALIDITY_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1657
|
+
var CERT_CLOCK_SKEW_MS = 5 * 60 * 1e3;
|
|
1658
|
+
var toArrayBuffer = (view) => {
|
|
1659
|
+
return Uint8Array.from(view).buffer;
|
|
1660
|
+
};
|
|
1661
|
+
var decodeBase64Url = (value) => {
|
|
1662
|
+
return Uint8Array.from(Buffer.from(value, "base64url"));
|
|
1663
|
+
};
|
|
1664
|
+
var resolveEcHash = (curve) => {
|
|
1665
|
+
switch (curve) {
|
|
1666
|
+
case "P-256":
|
|
1667
|
+
return "SHA-256";
|
|
1668
|
+
case "P-384":
|
|
1669
|
+
return "SHA-384";
|
|
1670
|
+
case "P-521":
|
|
1671
|
+
return "SHA-512";
|
|
1672
|
+
default:
|
|
1673
|
+
throw new Error(`Unsupported EC named curve "${curve}" for Appduct host certificates.`);
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
var importSigningKeyPair = async (keyPem) => {
|
|
1677
|
+
const privateKeyObject = createPrivateKey2(keyPem);
|
|
1678
|
+
const publicKeyObject = createPublicKey2(privateKeyObject);
|
|
1679
|
+
const privateKeyDer = Buffer.from(privateKeyObject.export({ format: "der", type: "pkcs8" }));
|
|
1680
|
+
const publicKeyDer = Buffer.from(publicKeyObject.export({ format: "der", type: "spki" }));
|
|
1681
|
+
const jwk = privateKeyObject.export({ format: "jwk" });
|
|
1682
|
+
if (jwk.kty === "RSA") {
|
|
1683
|
+
const modulusLength = privateKeyObject.asymmetricKeyDetails?.modulusLength;
|
|
1684
|
+
if (typeof modulusLength !== "number" || typeof jwk.e !== "string") {
|
|
1685
|
+
throw new Error("Appduct could not determine RSA key details for certificate generation.");
|
|
1686
|
+
}
|
|
1687
|
+
const publicExponent = decodeBase64Url(jwk.e);
|
|
1688
|
+
const importAlgorithm = {
|
|
1689
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
1690
|
+
hash: "SHA-256",
|
|
1691
|
+
modulusLength,
|
|
1692
|
+
publicExponent
|
|
1693
|
+
};
|
|
1694
|
+
return {
|
|
1695
|
+
keyType: "rsa",
|
|
1696
|
+
privateKey: await webcrypto.subtle.importKey(
|
|
1697
|
+
"pkcs8",
|
|
1698
|
+
toArrayBuffer(privateKeyDer),
|
|
1699
|
+
importAlgorithm,
|
|
1700
|
+
false,
|
|
1701
|
+
["sign"]
|
|
1702
|
+
),
|
|
1703
|
+
publicKey: await webcrypto.subtle.importKey(
|
|
1704
|
+
"spki",
|
|
1705
|
+
toArrayBuffer(publicKeyDer),
|
|
1706
|
+
importAlgorithm,
|
|
1707
|
+
true,
|
|
1708
|
+
["verify"]
|
|
1709
|
+
),
|
|
1710
|
+
signingAlgorithm: {
|
|
1711
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
1712
|
+
hash: "SHA-256"
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
if (jwk.kty === "EC") {
|
|
1717
|
+
const namedCurve = jwk.crv;
|
|
1718
|
+
if (typeof namedCurve !== "string") {
|
|
1719
|
+
throw new Error("Appduct could not determine the EC named curve for certificate generation.");
|
|
1720
|
+
}
|
|
1721
|
+
const importAlgorithm = {
|
|
1722
|
+
name: "ECDSA",
|
|
1723
|
+
namedCurve
|
|
1724
|
+
};
|
|
1725
|
+
const signingAlgorithm = {
|
|
1726
|
+
...importAlgorithm,
|
|
1727
|
+
hash: resolveEcHash(namedCurve)
|
|
1728
|
+
};
|
|
1729
|
+
return {
|
|
1730
|
+
keyType: "ec",
|
|
1731
|
+
privateKey: await webcrypto.subtle.importKey(
|
|
1732
|
+
"pkcs8",
|
|
1733
|
+
toArrayBuffer(privateKeyDer),
|
|
1734
|
+
importAlgorithm,
|
|
1735
|
+
false,
|
|
1736
|
+
["sign"]
|
|
1737
|
+
),
|
|
1738
|
+
publicKey: await webcrypto.subtle.importKey(
|
|
1739
|
+
"spki",
|
|
1740
|
+
toArrayBuffer(publicKeyDer),
|
|
1741
|
+
importAlgorithm,
|
|
1742
|
+
true,
|
|
1743
|
+
["verify"]
|
|
1744
|
+
),
|
|
1745
|
+
signingAlgorithm: {
|
|
1746
|
+
name: "ECDSA",
|
|
1747
|
+
hash: resolveEcHash(namedCurve)
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
throw new Error(`Appduct does not support "${jwk.kty ?? privateKeyObject.asymmetricKeyType}" host keys.`);
|
|
1752
|
+
};
|
|
1753
|
+
var generateHostCertificate = async (keyPem, advertisedIp) => {
|
|
1754
|
+
const keys = await importSigningKeyPair(keyPem);
|
|
1755
|
+
const sanEntries = [
|
|
1756
|
+
{ type: DNS, value: "localhost" },
|
|
1757
|
+
{ type: IP, value: "127.0.0.1" }
|
|
1758
|
+
];
|
|
1759
|
+
if (advertisedIp !== "127.0.0.1") {
|
|
1760
|
+
sanEntries.push({ type: IP, value: advertisedIp });
|
|
1761
|
+
}
|
|
1762
|
+
const keyUsage = KeyUsageFlags.digitalSignature | (keys.keyType === "rsa" ? KeyUsageFlags.keyEncipherment : KeyUsageFlags.keyAgreement);
|
|
1763
|
+
const certificate = await X509CertificateGenerator.createSelfSigned(
|
|
1764
|
+
{
|
|
1765
|
+
name: "CN=Appduct Host",
|
|
1766
|
+
notBefore: new Date(Date.now() - CERT_CLOCK_SKEW_MS),
|
|
1767
|
+
notAfter: new Date(Date.now() + CERT_VALIDITY_WINDOW_MS),
|
|
1768
|
+
signingAlgorithm: keys.signingAlgorithm,
|
|
1769
|
+
keys: {
|
|
1770
|
+
privateKey: keys.privateKey,
|
|
1771
|
+
publicKey: keys.publicKey
|
|
1772
|
+
},
|
|
1773
|
+
extensions: [
|
|
1774
|
+
new BasicConstraintsExtension(false, void 0, true),
|
|
1775
|
+
new KeyUsagesExtension(keyUsage, true),
|
|
1776
|
+
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], false),
|
|
1777
|
+
new SubjectAlternativeNameExtension(sanEntries, false),
|
|
1778
|
+
await SubjectKeyIdentifierExtension.create(keys.publicKey)
|
|
1779
|
+
]
|
|
1780
|
+
}
|
|
1781
|
+
);
|
|
1782
|
+
const certPem = certificate.toString("pem");
|
|
1783
|
+
return {
|
|
1784
|
+
certPem,
|
|
1785
|
+
spkiPin: getSpkiPinFromPrivateKeyPem(keyPem)
|
|
1786
|
+
};
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
// src/key-material.ts
|
|
1790
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
1791
|
+
import { dirname as dirname2 } from "node:path";
|
|
1792
|
+
import { mkdir as mkdir3, rename, rm as rm4, writeFile } from "node:fs/promises";
|
|
1793
|
+
var generatePrivateKeyPem = () => {
|
|
1794
|
+
const { privateKey } = generateKeyPairSync("rsa", {
|
|
1795
|
+
modulusLength: 2048,
|
|
1796
|
+
publicExponent: 65537
|
|
1797
|
+
});
|
|
1798
|
+
return privateKey.export({
|
|
1799
|
+
format: "pem",
|
|
1800
|
+
type: "pkcs8"
|
|
1801
|
+
}).toString("utf8");
|
|
1802
|
+
};
|
|
1803
|
+
var writePrivateKeyAtomically = async (path, keyPem) => {
|
|
1804
|
+
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1805
|
+
await mkdir3(dirname2(path), { recursive: true });
|
|
1806
|
+
try {
|
|
1807
|
+
await writeFile(temporaryPath, keyPem, {
|
|
1808
|
+
encoding: "utf8",
|
|
1809
|
+
mode: 384,
|
|
1810
|
+
flag: "wx"
|
|
1811
|
+
});
|
|
1812
|
+
await rename(temporaryPath, path);
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
await rm4(temporaryPath, { force: true }).catch(() => {
|
|
1815
|
+
});
|
|
1816
|
+
throw error;
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
// src/daemon/tls.ts
|
|
1821
|
+
var HostKeyError = class extends Error {
|
|
1822
|
+
constructor(message) {
|
|
1823
|
+
super(message);
|
|
1824
|
+
this.name = "HostKeyError";
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
var loadHostKeyPem = async (keyPath) => {
|
|
1828
|
+
const fileStat = await stat2(keyPath);
|
|
1829
|
+
if ((fileStat.mode & 63) !== 0) {
|
|
1830
|
+
throw new HostKeyError(
|
|
1831
|
+
`Appduct host key at "${keyPath}" is group- or world-readable (mode ${(fileStat.mode & 511).toString(8)}). Refusing to load it \u2014 run "chmod 600 ${keyPath}" or re-run "appduct keygen".`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
return readFile3(keyPath, "utf8");
|
|
1835
|
+
};
|
|
1836
|
+
var loadOrGenerateHostKeyPem = async (keyPath, warn) => {
|
|
1837
|
+
try {
|
|
1838
|
+
return await loadHostKeyPem(keyPath);
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
if (error.code !== "ENOENT") {
|
|
1841
|
+
throw error;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
const keyPem = generatePrivateKeyPem();
|
|
1845
|
+
await writePrivateKeyAtomically(keyPath, keyPem);
|
|
1846
|
+
warn?.(
|
|
1847
|
+
`Appduct: no host key found at "${keyPath}" \u2014 generated one (dev mode). SPKI pin: ${getSpkiPinFromPrivateKeyPem(keyPem)}`
|
|
1848
|
+
);
|
|
1849
|
+
return keyPem;
|
|
1850
|
+
};
|
|
1851
|
+
var createTlsManager = async (options) => {
|
|
1852
|
+
const keyPem = await loadOrGenerateHostKeyPem(options.keyPath, options.warn);
|
|
1853
|
+
const mint = async (address) => {
|
|
1854
|
+
const { certPem, spkiPin } = await generateHostCertificate(keyPem, address.address);
|
|
1855
|
+
return { certPem, keyPem, spkiPin, advertisedAddress: address };
|
|
1856
|
+
};
|
|
1857
|
+
let material = await mint(options.detectAddress());
|
|
1858
|
+
return {
|
|
1859
|
+
current: () => material,
|
|
1860
|
+
refresh: async (address) => {
|
|
1861
|
+
const next = address ?? options.detectAddress();
|
|
1862
|
+
if (next.address !== material.advertisedAddress.address || next.family !== material.advertisedAddress.family) {
|
|
1863
|
+
material = await mint(next);
|
|
1864
|
+
}
|
|
1865
|
+
return material;
|
|
1866
|
+
},
|
|
1867
|
+
pinnedKeys: () => [material.spkiPin]
|
|
1868
|
+
};
|
|
1869
|
+
};
|
|
1870
|
+
var toAgentEndpoint = (address, port) => {
|
|
1871
|
+
return { family: address.family, address: address.address, port };
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
// src/daemon/daemon.ts
|
|
1875
|
+
var KNOWN_EVENT_KINDS = new Set(EVENT_KINDS);
|
|
1876
|
+
var packageVersion = () => getDaemonReportedVersion();
|
|
1877
|
+
var AUDIT_PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1878
|
+
var buildStatusResult = async (config, boundWssPort, startedAt, tls, sessionManager, auditLogger, auditDir) => {
|
|
1879
|
+
const stats = await auditLogger.stats();
|
|
1880
|
+
return {
|
|
1881
|
+
version: packageVersion(),
|
|
1882
|
+
pid: process.pid,
|
|
1883
|
+
startedAt: startedAt.toISOString(),
|
|
1884
|
+
wssPort: boundWssPort,
|
|
1885
|
+
pinnedKeys: tls.pinnedKeys(),
|
|
1886
|
+
sessions: sessionManager.list(),
|
|
1887
|
+
pendingLinks: sessionManager.pendingLinkCount(),
|
|
1888
|
+
policy: config.policy,
|
|
1889
|
+
audit: {
|
|
1890
|
+
path: auditDir,
|
|
1891
|
+
failedWrites: auditLogger.failedWrites(),
|
|
1892
|
+
failedPrunes: auditLogger.failedPrunes(),
|
|
1893
|
+
retentionDays: config.auditRetentionDays,
|
|
1894
|
+
files: stats.files,
|
|
1895
|
+
bytes: stats.bytes
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
};
|
|
1899
|
+
var asSelectorParams = (params) => {
|
|
1900
|
+
if (params === void 0 || params === null) {
|
|
1901
|
+
return {};
|
|
1902
|
+
}
|
|
1903
|
+
if (typeof params !== "object" || Array.isArray(params)) {
|
|
1904
|
+
throw new RpcApplicationError("invalid_request", "Params must be an object.");
|
|
1905
|
+
}
|
|
1906
|
+
const selector = params.selector;
|
|
1907
|
+
if (selector !== void 0 && typeof selector !== "string") {
|
|
1908
|
+
throw new RpcApplicationError("invalid_request", '"selector" must be a string.');
|
|
1909
|
+
}
|
|
1910
|
+
return { selector };
|
|
1911
|
+
};
|
|
1912
|
+
var asToolsListParams = (params) => {
|
|
1913
|
+
const { selector } = asSelectorParams(params);
|
|
1914
|
+
const record = asRecordParams(params);
|
|
1915
|
+
const group = record.group;
|
|
1916
|
+
if (group !== void 0 && !isValidToolGroup(group)) {
|
|
1917
|
+
throw new RpcApplicationError(
|
|
1918
|
+
"invalid_request",
|
|
1919
|
+
'"group" must be one or two "/"-separated segments, each matching [a-zA-Z0-9_-]{1,64}.'
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
const filter = record.filter;
|
|
1923
|
+
if (filter !== void 0 && (typeof filter !== "string" || filter.length > MAX_TOOLS_FILTER_LENGTH)) {
|
|
1924
|
+
throw new RpcApplicationError(
|
|
1925
|
+
"invalid_request",
|
|
1926
|
+
`"filter" must be a string of at most ${MAX_TOOLS_FILTER_LENGTH} characters.`
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
const limit = record.limit;
|
|
1930
|
+
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
|
|
1931
|
+
throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
|
|
1932
|
+
}
|
|
1933
|
+
const offset = record.offset;
|
|
1934
|
+
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) {
|
|
1935
|
+
throw new RpcApplicationError("invalid_request", '"offset" must be a non-negative integer.');
|
|
1936
|
+
}
|
|
1937
|
+
return {
|
|
1938
|
+
selector,
|
|
1939
|
+
group,
|
|
1940
|
+
filter,
|
|
1941
|
+
limit,
|
|
1942
|
+
offset
|
|
1943
|
+
};
|
|
1944
|
+
};
|
|
1945
|
+
var asRecordParams = (params) => {
|
|
1946
|
+
if (params === void 0 || params === null) {
|
|
1947
|
+
return {};
|
|
1948
|
+
}
|
|
1949
|
+
if (typeof params !== "object" || Array.isArray(params)) {
|
|
1950
|
+
throw new RpcApplicationError("invalid_request", "Params must be an object.");
|
|
1951
|
+
}
|
|
1952
|
+
return params;
|
|
1953
|
+
};
|
|
1954
|
+
var asToolsCallParams = (params) => {
|
|
1955
|
+
const record = asRecordParams(params);
|
|
1956
|
+
const selector = record.selector;
|
|
1957
|
+
if (selector !== void 0 && typeof selector !== "string") {
|
|
1958
|
+
throw new RpcApplicationError("invalid_request", '"selector" must be a string.');
|
|
1959
|
+
}
|
|
1960
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
1961
|
+
throw new RpcApplicationError("invalid_request", '"name" must be a non-empty string.');
|
|
1962
|
+
}
|
|
1963
|
+
const args = record.args;
|
|
1964
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
1965
|
+
throw new RpcApplicationError("invalid_request", '"args" must be a JSON object.');
|
|
1966
|
+
}
|
|
1967
|
+
const timeoutMs = record.timeoutMs;
|
|
1968
|
+
if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs))) {
|
|
1969
|
+
throw new RpcApplicationError("invalid_request", '"timeoutMs" must be a number.');
|
|
1970
|
+
}
|
|
1971
|
+
const caller = record.caller;
|
|
1972
|
+
if (caller !== void 0 && caller !== "cli" && caller !== "mcp" && caller !== "client") {
|
|
1973
|
+
throw new RpcApplicationError("invalid_request", '"caller" must be "cli", "mcp", or "client".');
|
|
1974
|
+
}
|
|
1975
|
+
const consent = record.consent;
|
|
1976
|
+
if (consent !== void 0 && consent !== "elicitation" && consent !== "client") {
|
|
1977
|
+
throw new RpcApplicationError("invalid_request", '"consent" must be "elicitation".');
|
|
1978
|
+
}
|
|
1979
|
+
return {
|
|
1980
|
+
selector,
|
|
1981
|
+
name: record.name,
|
|
1982
|
+
args,
|
|
1983
|
+
timeoutMs,
|
|
1984
|
+
caller,
|
|
1985
|
+
consent: consent === "elicitation" ? "elicitation" : void 0
|
|
1986
|
+
};
|
|
1987
|
+
};
|
|
1988
|
+
var asToolsCancelParams = (params) => {
|
|
1989
|
+
const record = asRecordParams(params);
|
|
1990
|
+
const selector = record.selector;
|
|
1991
|
+
if (selector !== void 0 && typeof selector !== "string") {
|
|
1992
|
+
throw new RpcApplicationError("invalid_request", '"selector" must be a string.');
|
|
1993
|
+
}
|
|
1994
|
+
if (typeof record.callId !== "string" || record.callId.length === 0) {
|
|
1995
|
+
throw new RpcApplicationError("invalid_request", '"callId" must be a non-empty string.');
|
|
1996
|
+
}
|
|
1997
|
+
const reason = record.reason;
|
|
1998
|
+
if (reason !== void 0 && typeof reason !== "string") {
|
|
1999
|
+
throw new RpcApplicationError("invalid_request", '"reason" must be a string.');
|
|
2000
|
+
}
|
|
2001
|
+
return {
|
|
2002
|
+
selector,
|
|
2003
|
+
callId: record.callId,
|
|
2004
|
+
reason
|
|
2005
|
+
};
|
|
2006
|
+
};
|
|
2007
|
+
var asEventsSubscribeParams = (params) => {
|
|
2008
|
+
const record = asRecordParams(params);
|
|
2009
|
+
const sessionSelector = record.sessionSelector;
|
|
2010
|
+
if (sessionSelector !== void 0 && typeof sessionSelector !== "string") {
|
|
2011
|
+
throw new RpcApplicationError("invalid_request", '"sessionSelector" must be a string.');
|
|
2012
|
+
}
|
|
2013
|
+
const kindsRaw = record.kinds;
|
|
2014
|
+
let kinds;
|
|
2015
|
+
if (kindsRaw !== void 0) {
|
|
2016
|
+
if (!Array.isArray(kindsRaw) || !kindsRaw.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
|
|
2017
|
+
throw new RpcApplicationError("invalid_request", '"kinds" must be an array of known event kinds.');
|
|
2018
|
+
}
|
|
2019
|
+
kinds = kindsRaw;
|
|
2020
|
+
}
|
|
2021
|
+
return { sessionSelector, kinds };
|
|
2022
|
+
};
|
|
2023
|
+
var asEventsSinceParams = (params) => {
|
|
2024
|
+
const record = asRecordParams(params);
|
|
2025
|
+
const selector = record.selector;
|
|
2026
|
+
if (selector !== void 0 && typeof selector !== "string") {
|
|
2027
|
+
throw new RpcApplicationError("invalid_request", '"selector" must be a string.');
|
|
2028
|
+
}
|
|
2029
|
+
const since = record.since;
|
|
2030
|
+
if (since !== void 0 && (typeof since !== "number" || !Number.isInteger(since) || since < 0)) {
|
|
2031
|
+
throw new RpcApplicationError("invalid_request", '"since" must be a non-negative integer.');
|
|
2032
|
+
}
|
|
2033
|
+
const kindsRaw = record.kinds;
|
|
2034
|
+
let kinds;
|
|
2035
|
+
if (kindsRaw !== void 0) {
|
|
2036
|
+
if (!Array.isArray(kindsRaw) || !kindsRaw.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
|
|
2037
|
+
throw new RpcApplicationError("invalid_request", '"kinds" must be an array of known event kinds.');
|
|
2038
|
+
}
|
|
2039
|
+
kinds = kindsRaw;
|
|
2040
|
+
}
|
|
2041
|
+
const limit = record.limit;
|
|
2042
|
+
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
|
|
2043
|
+
throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
selector,
|
|
2047
|
+
since,
|
|
2048
|
+
kinds,
|
|
2049
|
+
limit
|
|
2050
|
+
};
|
|
2051
|
+
};
|
|
2052
|
+
var asLinkCreateParams = (params) => {
|
|
2053
|
+
if (params === void 0 || params === null) {
|
|
2054
|
+
return {};
|
|
2055
|
+
}
|
|
2056
|
+
if (typeof params !== "object" || Array.isArray(params)) {
|
|
2057
|
+
throw new RpcApplicationError("invalid_request", "Params must be an object.");
|
|
2058
|
+
}
|
|
2059
|
+
const record = params;
|
|
2060
|
+
const ttlSeconds = record.ttlSeconds;
|
|
2061
|
+
if (ttlSeconds !== void 0 && (typeof ttlSeconds !== "number" || !Number.isInteger(ttlSeconds) || ttlSeconds <= 0)) {
|
|
2062
|
+
throw new RpcApplicationError("invalid_request", '"ttlSeconds" must be a positive integer.');
|
|
2063
|
+
}
|
|
2064
|
+
const addressOverride = record.addressOverride;
|
|
2065
|
+
if (addressOverride !== void 0 && (typeof addressOverride !== "string" || addressOverride.length === 0)) {
|
|
2066
|
+
throw new RpcApplicationError("invalid_request", '"addressOverride" must be a non-empty string.');
|
|
2067
|
+
}
|
|
2068
|
+
return { ttlSeconds, addressOverride };
|
|
2069
|
+
};
|
|
2070
|
+
var startDaemon = async (options) => {
|
|
2071
|
+
const clock = options.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
2072
|
+
const timers = options.timers ?? systemTimers;
|
|
2073
|
+
const paths = getStateDirPaths(options.stateDir);
|
|
2074
|
+
await ensureStateDir(options.stateDir);
|
|
2075
|
+
const config = await loadConfig(paths, { warn: options.warn });
|
|
2076
|
+
const startedAt = clock.now();
|
|
2077
|
+
const auditLogger = createAuditLogger({
|
|
2078
|
+
auditDir: paths.auditDir,
|
|
2079
|
+
clock,
|
|
2080
|
+
retentionDays: config.auditRetentionDays,
|
|
2081
|
+
warn: options.warn
|
|
2082
|
+
});
|
|
2083
|
+
let auditPruneInterval;
|
|
2084
|
+
let pidfile;
|
|
2085
|
+
let server;
|
|
2086
|
+
let listener;
|
|
2087
|
+
let sessionManager;
|
|
2088
|
+
let callsManager;
|
|
2089
|
+
let eventBus;
|
|
2090
|
+
let boundWssPort = config.wssPort;
|
|
2091
|
+
let shuttingDown = false;
|
|
2092
|
+
let resolveExited;
|
|
2093
|
+
const exited = new Promise((resolve) => {
|
|
2094
|
+
resolveExited = resolve;
|
|
2095
|
+
});
|
|
2096
|
+
const shutdown = async () => {
|
|
2097
|
+
if (shuttingDown) {
|
|
2098
|
+
return exited;
|
|
2099
|
+
}
|
|
2100
|
+
shuttingDown = true;
|
|
2101
|
+
try {
|
|
2102
|
+
if (auditPruneInterval !== void 0) {
|
|
2103
|
+
timers.clearInterval(auditPruneInterval);
|
|
2104
|
+
auditPruneInterval = void 0;
|
|
2105
|
+
}
|
|
2106
|
+
callsManager?.disposeAll();
|
|
2107
|
+
sessionManager?.disposeAll(1001, "daemon_shutdown");
|
|
2108
|
+
await listener?.close();
|
|
2109
|
+
await server?.close();
|
|
2110
|
+
await pidfile?.release();
|
|
2111
|
+
await rm5(getSocketPath(paths), { force: true });
|
|
2112
|
+
await auditLogger.flush();
|
|
2113
|
+
} finally {
|
|
2114
|
+
resolveExited();
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
try {
|
|
2118
|
+
pidfile = await acquirePidfile(paths.pidFilePath, {
|
|
2119
|
+
onStaleTakeover: async () => {
|
|
2120
|
+
await rm5(getSocketPath(paths), { force: true });
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
eventBus = createEventBus({ clock, bufferSize: config.eventBufferSize });
|
|
2124
|
+
const activeEventBus = eventBus;
|
|
2125
|
+
const detectAddress = options.detectAddress ?? (() => detectAdvertisedAddress({ override: config.advertisedIp }));
|
|
2126
|
+
const tls = await createTlsManager({ keyPath: config.keyPath, detectAddress, warn: options.warn });
|
|
2127
|
+
sessionManager = createSessionManager({
|
|
2128
|
+
graceSeconds: config.graceSeconds,
|
|
2129
|
+
keepaliveIntervalSeconds: config.keepaliveIntervalSeconds,
|
|
2130
|
+
linkTtlSeconds: config.linkTtlSeconds,
|
|
2131
|
+
// `boundWssPort`, not `config.wssPort`: a link minted by a daemon on an OS-assigned port
|
|
2132
|
+
// must advertise the port the app can actually reach, not the `0` that asked for one.
|
|
2133
|
+
getEndpoint: () => toAgentEndpoint(tls.current().advertisedAddress, boundWssPort),
|
|
2134
|
+
eventBus: activeEventBus,
|
|
2135
|
+
clock,
|
|
2136
|
+
onToolFrame: (message) => {
|
|
2137
|
+
switch (message.type) {
|
|
2138
|
+
case "tool_result":
|
|
2139
|
+
callsManager?.handleToolResult(message);
|
|
2140
|
+
return;
|
|
2141
|
+
case "tool_error":
|
|
2142
|
+
callsManager?.handleToolError(message);
|
|
2143
|
+
return;
|
|
2144
|
+
case "tool_call_progress":
|
|
2145
|
+
callsManager?.handleToolCallProgress(message);
|
|
2146
|
+
}
|
|
2147
|
+
},
|
|
2148
|
+
onAppEvent: (sessionId, alias, message) => {
|
|
2149
|
+
activeEventBus.emit({
|
|
2150
|
+
kind: "app_event",
|
|
2151
|
+
sessionId,
|
|
2152
|
+
alias,
|
|
2153
|
+
data: { name: message.name, payload: message.payload, ts: message.ts }
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
const activeSessionManager = sessionManager;
|
|
2158
|
+
callsManager = createCallsManager({
|
|
2159
|
+
send: (sessionId, message) => activeSessionManager.sendToolCall(sessionId, message),
|
|
2160
|
+
sendCancel: (sessionId, message) => activeSessionManager.sendToolCancel(sessionId, message),
|
|
2161
|
+
eventBus: activeEventBus
|
|
2162
|
+
});
|
|
2163
|
+
const activeCallsManager = callsManager;
|
|
2164
|
+
listener = await startListener({
|
|
2165
|
+
port: config.wssPort,
|
|
2166
|
+
tls,
|
|
2167
|
+
sessionManager
|
|
2168
|
+
});
|
|
2169
|
+
const activeListener = listener;
|
|
2170
|
+
boundWssPort = activeListener.port() ?? config.wssPort;
|
|
2171
|
+
activeEventBus.subscribe((event) => {
|
|
2172
|
+
for (const connection of server?.connections() ?? []) {
|
|
2173
|
+
const subscription = connection.state.eventSubscription;
|
|
2174
|
+
if (!subscription) {
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (subscription.kinds && !subscription.kinds.has(event.kind)) {
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
if (subscription.sessionSelector !== void 0 && event.sessionId !== subscription.sessionSelector && event.alias !== subscription.sessionSelector) {
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
server?.notify(connection, event);
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
server = await startRpcServer({
|
|
2187
|
+
socketPath: getSocketPath(paths),
|
|
2188
|
+
dispatch: {
|
|
2189
|
+
[RPC_METHODS.daemonStatus]: async () => {
|
|
2190
|
+
return buildStatusResult(
|
|
2191
|
+
config,
|
|
2192
|
+
boundWssPort,
|
|
2193
|
+
startedAt,
|
|
2194
|
+
tls,
|
|
2195
|
+
activeSessionManager,
|
|
2196
|
+
auditLogger,
|
|
2197
|
+
paths.auditDir
|
|
2198
|
+
);
|
|
2199
|
+
},
|
|
2200
|
+
[RPC_METHODS.daemonShutdown]: (_params, context) => {
|
|
2201
|
+
context.afterSend(() => {
|
|
2202
|
+
void shutdown();
|
|
2203
|
+
});
|
|
2204
|
+
return { ok: true };
|
|
2205
|
+
},
|
|
2206
|
+
[RPC_METHODS.linkCreate]: async (params) => {
|
|
2207
|
+
const { ttlSeconds, addressOverride } = asLinkCreateParams(params);
|
|
2208
|
+
const previousMaterial = tls.current();
|
|
2209
|
+
const nextMaterial = await tls.refresh();
|
|
2210
|
+
if (nextMaterial !== previousMaterial) {
|
|
2211
|
+
activeListener.applyTls(nextMaterial);
|
|
2212
|
+
}
|
|
2213
|
+
return { ...activeSessionManager.createLink(ttlSeconds, addressOverride), pin: tls.pinnedKeys()[0] };
|
|
2214
|
+
},
|
|
2215
|
+
[RPC_METHODS.sessionsList]: () => {
|
|
2216
|
+
return activeSessionManager.list();
|
|
2217
|
+
},
|
|
2218
|
+
[RPC_METHODS.sessionsDescribe]: (params) => {
|
|
2219
|
+
const { selector } = asSelectorParams(params);
|
|
2220
|
+
return activeSessionManager.describe(selector);
|
|
2221
|
+
},
|
|
2222
|
+
[RPC_METHODS.sessionsRevoke]: (params) => {
|
|
2223
|
+
const { selector } = asSelectorParams(params);
|
|
2224
|
+
activeSessionManager.revoke(selector);
|
|
2225
|
+
return { ok: true };
|
|
2226
|
+
},
|
|
2227
|
+
[RPC_METHODS.toolsList]: (params) => {
|
|
2228
|
+
const { selector, group, filter, limit, offset } = asToolsListParams(params);
|
|
2229
|
+
const resolved = activeSessionManager.resolveForTools(selector);
|
|
2230
|
+
const entries = resolved.registry.list().map((descriptor) => ({
|
|
2231
|
+
...descriptor,
|
|
2232
|
+
// Normalised once, here, so every reader of a listing finds the key: an app registers an
|
|
2233
|
+
// ungrouped tool by omitting `group`, a listing reports it as `null` — the same value the
|
|
2234
|
+
// `groups` summary uses for its ungrouped bucket.
|
|
2235
|
+
group: descriptor.group ?? null,
|
|
2236
|
+
policy: evaluate(descriptor, { alias: resolved.alias }, config.policy)
|
|
2237
|
+
}));
|
|
2238
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
2239
|
+
const groups = summarizeToolGroups(entries);
|
|
2240
|
+
const inGroup = group === void 0 ? entries : entries.filter((entry) => toolGroupMatches(entry.group, group));
|
|
2241
|
+
const lowerFilter = filter?.toLowerCase();
|
|
2242
|
+
const matching = lowerFilter ? inGroup.filter(
|
|
2243
|
+
(entry) => entry.name.toLowerCase().includes(lowerFilter) || entry.description.toLowerCase().includes(lowerFilter)
|
|
2244
|
+
) : inGroup;
|
|
2245
|
+
const total = matching.length;
|
|
2246
|
+
const page = offset === void 0 && limit === void 0 ? matching : matching.slice(offset ?? 0, limit === void 0 ? void 0 : (offset ?? 0) + limit);
|
|
2247
|
+
return { tools: page, total, groups };
|
|
2248
|
+
},
|
|
2249
|
+
// This handler is the single seam every `tools.call` passes through: policy (ARCHITECTURE.md
|
|
2250
|
+
// §12) is evaluated once the target tool descriptor is known, and one audit record is
|
|
2251
|
+
// written for every attempt that reaches a resolved session, across ok/error/denied.
|
|
2252
|
+
[RPC_METHODS.toolsCall]: async (params, context) => {
|
|
2253
|
+
const { selector, name, args, timeoutMs, caller, consent } = asToolsCallParams(params);
|
|
2254
|
+
const effectiveCaller = caller ?? "cli";
|
|
2255
|
+
const resolved = activeSessionManager.resolveForTools(selector);
|
|
2256
|
+
const auditStartedAt = clock.now().getTime();
|
|
2257
|
+
const writeAudit = (outcome, errorType, grantedConsent2, deniedReason) => {
|
|
2258
|
+
auditLogger.record({
|
|
2259
|
+
sessionId: resolved.sessionId,
|
|
2260
|
+
alias: resolved.alias,
|
|
2261
|
+
tool: name,
|
|
2262
|
+
argsSha256: argsSha256(args),
|
|
2263
|
+
outcome,
|
|
2264
|
+
errorType,
|
|
2265
|
+
deniedReason,
|
|
2266
|
+
durationMs: clock.now().getTime() - auditStartedAt,
|
|
2267
|
+
caller: effectiveCaller,
|
|
2268
|
+
consent: grantedConsent2
|
|
2269
|
+
});
|
|
2270
|
+
};
|
|
2271
|
+
if (resolved.state !== "active") {
|
|
2272
|
+
const errorType = resolved.state === "suspended" ? "session_suspended" : "session_not_active";
|
|
2273
|
+
writeAudit("error", errorType);
|
|
2274
|
+
throw new RpcApplicationError(errorType, `Session "${resolved.alias}" is not active.`);
|
|
2275
|
+
}
|
|
2276
|
+
const tool = resolved.registry.get(name);
|
|
2277
|
+
if (!tool) {
|
|
2278
|
+
writeAudit("error", "tool_not_found");
|
|
2279
|
+
throw new RpcApplicationError("tool_not_found", `Tool "${name}" is not registered.`);
|
|
2280
|
+
}
|
|
2281
|
+
const policyDecision = evaluate(tool, { alias: resolved.alias }, config.policy);
|
|
2282
|
+
const grantedConsent = policyDecision === "prompt" && consent === "elicitation" ? consent : void 0;
|
|
2283
|
+
if (policyDecision === "deny" || policyDecision === "prompt" && grantedConsent === void 0) {
|
|
2284
|
+
activeEventBus.emit({
|
|
2285
|
+
kind: "tool_call_finished",
|
|
2286
|
+
sessionId: resolved.sessionId,
|
|
2287
|
+
alias: resolved.alias,
|
|
2288
|
+
data: { name, outcome: "denied" }
|
|
2289
|
+
});
|
|
2290
|
+
if (policyDecision === "prompt") {
|
|
2291
|
+
writeAudit("denied", void 0, void 0, "no_consent_channel");
|
|
2292
|
+
throw new RpcApplicationError(
|
|
2293
|
+
"policy_denied",
|
|
2294
|
+
`Policy requires prompt consent to call "${name}" on session "${resolved.alias}", but no consent channel confirmed it.`,
|
|
2295
|
+
-32e3,
|
|
2296
|
+
{
|
|
2297
|
+
reason: "no_consent_channel",
|
|
2298
|
+
hint: `This caller did not confirm human consent for "${name}". Only an MCP client that declares the "elicitation" capability can confirm it, via an elicitation/create prompt; every other caller (including the CLI and MCP clients without elicitation) is denied by design. To change this tool's policy, edit "policy.tools["${resolved.alias}/${name}"]" (or policy.default/policy.destructive) in ${paths.configPath}.`
|
|
2299
|
+
}
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
writeAudit("denied", void 0, void 0, "policy");
|
|
2303
|
+
throw new RpcApplicationError(
|
|
2304
|
+
"policy_denied",
|
|
2305
|
+
`Policy denies calling "${name}" on session "${resolved.alias}".`,
|
|
2306
|
+
-32e3,
|
|
2307
|
+
{ hint: `Edit "policy" in ${paths.configPath} (policy.default/policy.destructive/policy.tools) to change this.` }
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
const startedAt2 = clock.now().getTime();
|
|
2311
|
+
const session = { sessionId: resolved.sessionId, alias: resolved.alias };
|
|
2312
|
+
const { callId, result: callResult } = activeCallsManager.call(
|
|
2313
|
+
session,
|
|
2314
|
+
name,
|
|
2315
|
+
args,
|
|
2316
|
+
// The caller's explicit `timeoutMs` wins; otherwise the tool's own declared deadline is
|
|
2317
|
+
// the default, and only if it declares none does `clampTimeout` fall back to
|
|
2318
|
+
// DEFAULT_CALL_TIMEOUT_MS (issue #25).
|
|
2319
|
+
timeoutMs ?? tool.timeout_ms
|
|
2320
|
+
);
|
|
2321
|
+
const unregisterOnClose = context.onClose(() => {
|
|
2322
|
+
activeCallsManager.cancel(resolved.sessionId, callId, "connection_closed");
|
|
2323
|
+
});
|
|
2324
|
+
activeEventBus.emit({
|
|
2325
|
+
kind: "tool_call_started",
|
|
2326
|
+
sessionId: resolved.sessionId,
|
|
2327
|
+
alias: resolved.alias,
|
|
2328
|
+
data: { name, callId }
|
|
2329
|
+
});
|
|
2330
|
+
try {
|
|
2331
|
+
const result = await callResult;
|
|
2332
|
+
activeEventBus.emit({
|
|
2333
|
+
kind: "tool_call_finished",
|
|
2334
|
+
sessionId: resolved.sessionId,
|
|
2335
|
+
alias: resolved.alias,
|
|
2336
|
+
data: { name, callId, durationMs: clock.now().getTime() - startedAt2, outcome: "ok" }
|
|
2337
|
+
});
|
|
2338
|
+
writeAudit("ok", void 0, grantedConsent);
|
|
2339
|
+
return { result, callId };
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
const errorType = error instanceof RpcApplicationError ? error.type : "tool_execution_error";
|
|
2342
|
+
const outcome = errorType === "tool_cancelled" ? "cancelled" : "error";
|
|
2343
|
+
activeEventBus.emit({
|
|
2344
|
+
kind: "tool_call_finished",
|
|
2345
|
+
sessionId: resolved.sessionId,
|
|
2346
|
+
alias: resolved.alias,
|
|
2347
|
+
data: { name, callId, durationMs: clock.now().getTime() - startedAt2, outcome, errorType }
|
|
2348
|
+
});
|
|
2349
|
+
writeAudit(outcome, errorType, grantedConsent);
|
|
2350
|
+
throw error;
|
|
2351
|
+
} finally {
|
|
2352
|
+
unregisterOnClose();
|
|
2353
|
+
}
|
|
2354
|
+
},
|
|
2355
|
+
[RPC_METHODS.toolsCancel]: (params) => {
|
|
2356
|
+
const { selector, callId, reason } = asToolsCancelParams(params);
|
|
2357
|
+
const resolved = activeSessionManager.resolveForTools(selector);
|
|
2358
|
+
const cancelled = activeCallsManager.cancel(resolved.sessionId, callId, reason ?? "client_cancelled");
|
|
2359
|
+
return { cancelled };
|
|
2360
|
+
},
|
|
2361
|
+
[RPC_METHODS.eventsSubscribe]: (params, context) => {
|
|
2362
|
+
const { sessionSelector, kinds } = asEventsSubscribeParams(params);
|
|
2363
|
+
context.connection.state.eventSubscription = {
|
|
2364
|
+
sessionSelector,
|
|
2365
|
+
kinds: kinds ? new Set(kinds) : void 0
|
|
2366
|
+
};
|
|
2367
|
+
return { ok: true };
|
|
2368
|
+
},
|
|
2369
|
+
[RPC_METHODS.eventsSince]: (params) => {
|
|
2370
|
+
const { selector, since, kinds, limit } = asEventsSinceParams(params);
|
|
2371
|
+
const resolved = activeSessionManager.describe(selector);
|
|
2372
|
+
return activeEventBus.since(resolved.sessionId, { since, kinds, limit });
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
auditPruneInterval = timers.setInterval(() => {
|
|
2377
|
+
void auditLogger.prune();
|
|
2378
|
+
}, AUDIT_PRUNE_INTERVAL_MS);
|
|
2379
|
+
void auditLogger.prune();
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
if (auditPruneInterval !== void 0) {
|
|
2382
|
+
timers.clearInterval(auditPruneInterval);
|
|
2383
|
+
auditPruneInterval = void 0;
|
|
2384
|
+
}
|
|
2385
|
+
callsManager?.disposeAll();
|
|
2386
|
+
sessionManager?.disposeAll(1001, "daemon_startup_failed");
|
|
2387
|
+
await listener?.close();
|
|
2388
|
+
await pidfile?.release();
|
|
2389
|
+
throw error;
|
|
2390
|
+
}
|
|
2391
|
+
eventBus.emit({ kind: "daemon_started", data: { version: packageVersion(), pid: process.pid } });
|
|
2392
|
+
return {
|
|
2393
|
+
paths,
|
|
2394
|
+
config,
|
|
2395
|
+
startedAt,
|
|
2396
|
+
server,
|
|
2397
|
+
listener,
|
|
2398
|
+
eventBus,
|
|
2399
|
+
exited,
|
|
2400
|
+
shutdown
|
|
2401
|
+
};
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2404
|
+
// src/rpc/client.ts
|
|
2405
|
+
import { join as join5 } from "node:path";
|
|
2406
|
+
import { RPC_METHODS as RPC_METHODS2 } from "@appduct/shared";
|
|
2407
|
+
var defaultBinPath = join5(getPackageRoot(), "bin.js");
|
|
2408
|
+
var DaemonRpcError = class extends Error {
|
|
2409
|
+
constructor(code, message, data) {
|
|
2410
|
+
super(message);
|
|
2411
|
+
this.code = code;
|
|
2412
|
+
this.data = data;
|
|
2413
|
+
this.name = "DaemonRpcError";
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
var DaemonUnavailableError = class extends Error {
|
|
2417
|
+
constructor(message, reason = "unreachable") {
|
|
2418
|
+
super(message);
|
|
2419
|
+
this.reason = reason;
|
|
2420
|
+
this.name = "DaemonUnavailableError";
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
var describeLiveState = (state) => {
|
|
2424
|
+
const parts = [];
|
|
2425
|
+
if (state.sessions > 0) {
|
|
2426
|
+
parts.push(`${state.sessions} connected session(s)`);
|
|
2427
|
+
}
|
|
2428
|
+
if (state.pendingLinks > 0) {
|
|
2429
|
+
parts.push(`${state.pendingLinks} unclaimed link(s)`);
|
|
2430
|
+
}
|
|
2431
|
+
return parts.join(" and ");
|
|
2432
|
+
};
|
|
2433
|
+
var DaemonVersionMismatchError = class extends Error {
|
|
2434
|
+
constructor(daemonVersion, clientVersion, live, message) {
|
|
2435
|
+
super(
|
|
2436
|
+
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".`
|
|
2437
|
+
);
|
|
2438
|
+
this.daemonVersion = daemonVersion;
|
|
2439
|
+
this.clientVersion = clientVersion;
|
|
2440
|
+
this.name = "DaemonVersionMismatchError";
|
|
2441
|
+
this.sessionCount = live.sessions;
|
|
2442
|
+
this.pendingLinkCount = live.pendingLinks;
|
|
2443
|
+
}
|
|
2444
|
+
sessionCount;
|
|
2445
|
+
pendingLinkCount;
|
|
2446
|
+
};
|
|
2447
|
+
var isConnectionMissingError = (error) => {
|
|
2448
|
+
const code = error.code;
|
|
2449
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
2450
|
+
};
|
|
2451
|
+
var isDaemonUnreachableError = (error) => {
|
|
2452
|
+
return error instanceof DaemonUnavailableError || isConnectionMissingError(error);
|
|
2453
|
+
};
|
|
2454
|
+
|
|
2455
|
+
// src/errors.ts
|
|
2456
|
+
var EXIT_CODE_BY_ERROR_TYPE = {
|
|
2457
|
+
usage_error: 64,
|
|
2458
|
+
validation_error: 65,
|
|
2459
|
+
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
2460
|
+
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
2461
|
+
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
2462
|
+
// assertion failure (docs/tasks/08-appduct-doctor.md).
|
|
2463
|
+
inspection_error: 66,
|
|
2464
|
+
connection_error: 70,
|
|
2465
|
+
session_error: 71,
|
|
2466
|
+
tool_error: 72,
|
|
2467
|
+
permission_error: 77,
|
|
2468
|
+
internal_error: 1,
|
|
2469
|
+
// `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
|
|
2470
|
+
// assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
|
|
2471
|
+
// and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
|
|
2472
|
+
// conflate a normal assertion failure with an unexpected crash.
|
|
2473
|
+
assertion_error: 3
|
|
2474
|
+
};
|
|
2475
|
+
var RPC_ERROR_EXIT_CLASS = {
|
|
2476
|
+
no_session: "session_error",
|
|
2477
|
+
ambiguous_session: "session_error",
|
|
2478
|
+
unknown_session: "session_error",
|
|
2479
|
+
session_not_active: "session_error",
|
|
2480
|
+
session_suspended: "session_error",
|
|
2481
|
+
tool_not_found: "tool_error",
|
|
2482
|
+
tool_input_validation_error: "tool_error",
|
|
2483
|
+
tool_output_validation_error: "tool_error",
|
|
2484
|
+
tool_execution_error: "tool_error",
|
|
2485
|
+
tool_serialization_error: "tool_error",
|
|
2486
|
+
tool_timeout: "tool_error",
|
|
2487
|
+
tool_cancelled: "tool_error",
|
|
2488
|
+
policy_denied: "permission_error",
|
|
2489
|
+
invalid_request: "validation_error"
|
|
2490
|
+
};
|
|
2491
|
+
var AppductCliError = class extends Error {
|
|
2492
|
+
type;
|
|
2493
|
+
details;
|
|
2494
|
+
constructor(type, message, details) {
|
|
2495
|
+
super(message);
|
|
2496
|
+
this.name = "AppductCliError";
|
|
2497
|
+
this.type = type;
|
|
2498
|
+
this.details = details;
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
var connectionError = (message, details) => {
|
|
2502
|
+
return new AppductCliError("connection_error", message, details);
|
|
2503
|
+
};
|
|
2504
|
+
var isAppductCliError = (value) => {
|
|
2505
|
+
return value instanceof AppductCliError;
|
|
2506
|
+
};
|
|
2507
|
+
var rpcErrorClass = (error) => {
|
|
2508
|
+
const type = error.data?.type;
|
|
2509
|
+
return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
|
|
2510
|
+
};
|
|
2511
|
+
var toCliError = (error) => {
|
|
2512
|
+
if (isAppductCliError(error)) {
|
|
2513
|
+
return {
|
|
2514
|
+
type: error.type,
|
|
2515
|
+
message: error.message,
|
|
2516
|
+
details: error.details
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
if (error instanceof DaemonVersionMismatchError) {
|
|
2520
|
+
return {
|
|
2521
|
+
type: "connection_error",
|
|
2522
|
+
message: error.message,
|
|
2523
|
+
details: {
|
|
2524
|
+
daemon_version: error.daemonVersion,
|
|
2525
|
+
client_version: error.clientVersion,
|
|
2526
|
+
session_count: error.sessionCount,
|
|
2527
|
+
pending_link_count: error.pendingLinkCount
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
if (error instanceof DaemonRpcError && error.data?.type) {
|
|
2532
|
+
return {
|
|
2533
|
+
type: error.data.type,
|
|
2534
|
+
message: error.message,
|
|
2535
|
+
details: error.data.details
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
if (isDaemonUnreachableError(error)) {
|
|
2539
|
+
return {
|
|
2540
|
+
type: "connection_error",
|
|
2541
|
+
message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
if (error instanceof Error) {
|
|
2545
|
+
return {
|
|
2546
|
+
type: "internal_error",
|
|
2547
|
+
message: error.message
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
return {
|
|
2551
|
+
type: "internal_error",
|
|
2552
|
+
message: "An unexpected error occurred.",
|
|
2553
|
+
details: error
|
|
2554
|
+
};
|
|
2555
|
+
};
|
|
2556
|
+
var getExitCodeForError = (error) => {
|
|
2557
|
+
if (isAppductCliError(error)) {
|
|
2558
|
+
return EXIT_CODE_BY_ERROR_TYPE[error.type];
|
|
2559
|
+
}
|
|
2560
|
+
if (error instanceof DaemonVersionMismatchError) {
|
|
2561
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
2562
|
+
}
|
|
2563
|
+
if (error instanceof DaemonRpcError) {
|
|
2564
|
+
const cliErrorClass = rpcErrorClass(error);
|
|
2565
|
+
return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
|
|
2566
|
+
}
|
|
2567
|
+
if (isDaemonUnreachableError(error)) {
|
|
2568
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
2569
|
+
}
|
|
2570
|
+
return EXIT_CODE_BY_ERROR_TYPE.internal_error;
|
|
2571
|
+
};
|
|
2572
|
+
|
|
2573
|
+
// src/commands/daemon/run.ts
|
|
2574
|
+
var handleDaemonRunCommand = async (context) => {
|
|
2575
|
+
let daemon;
|
|
2576
|
+
try {
|
|
2577
|
+
daemon = await startDaemon({
|
|
2578
|
+
stateDir: context.stateDir,
|
|
2579
|
+
clock: context.clock,
|
|
2580
|
+
warn: context.warn
|
|
2581
|
+
});
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
if (error instanceof DaemonAlreadyRunningError) {
|
|
2584
|
+
throw connectionError(error.message, { pid: error.pid });
|
|
2585
|
+
}
|
|
2586
|
+
throw error;
|
|
2587
|
+
}
|
|
2588
|
+
return {
|
|
2589
|
+
result: {
|
|
2590
|
+
ok: true,
|
|
2591
|
+
data: {
|
|
2592
|
+
daemon: {
|
|
2593
|
+
pid: process.pid,
|
|
2594
|
+
state_dir: daemon.paths.root,
|
|
2595
|
+
socket_path: daemon.paths.socketPath
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
},
|
|
2599
|
+
completion: daemon.exited,
|
|
2600
|
+
stop: () => {
|
|
2601
|
+
void daemon.shutdown();
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// src/cli/global-flags.ts
|
|
2607
|
+
var formatJson = (value, flags) => {
|
|
2608
|
+
return flags.pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
// src/output.ts
|
|
2612
|
+
import pc from "picocolors";
|
|
2613
|
+
import {
|
|
2614
|
+
formatAgentWebSocketUrl,
|
|
2615
|
+
renderToolSignature,
|
|
2616
|
+
summarizeToolDescription
|
|
2617
|
+
} from "@appduct/shared";
|
|
2618
|
+
|
|
2619
|
+
// src/qr-terminal.ts
|
|
2620
|
+
import { toQR } from "toqr";
|
|
2621
|
+
var HALF_UPPER = "\u2580";
|
|
2622
|
+
var HALF_LOWER = "\u2584";
|
|
2623
|
+
var FULL = "\u2588";
|
|
2624
|
+
var renderQrToTerminal = (content, options) => {
|
|
2625
|
+
const margin = options?.margin ?? 0;
|
|
2626
|
+
const ecLevel = options?.ecLevel ?? 1;
|
|
2627
|
+
const modules = toQR(content, ecLevel);
|
|
2628
|
+
const side = Math.sqrt(modules.length);
|
|
2629
|
+
if (!Number.isInteger(side)) {
|
|
2630
|
+
throw new Error("Invalid QR matrix size");
|
|
2631
|
+
}
|
|
2632
|
+
const dim = side + 2 * margin;
|
|
2633
|
+
const isDark = (row, col) => {
|
|
2634
|
+
if (row < margin || col < margin || row >= margin + side || col >= margin + side) {
|
|
2635
|
+
return false;
|
|
2636
|
+
}
|
|
2637
|
+
const mr = row - margin;
|
|
2638
|
+
const mc = col - margin;
|
|
2639
|
+
return modules[mr * side + mc] === 1;
|
|
2640
|
+
};
|
|
2641
|
+
const lines = [];
|
|
2642
|
+
for (let row = 0; row < dim; row += 2) {
|
|
2643
|
+
let line = "";
|
|
2644
|
+
for (let col = 0; col < dim; col++) {
|
|
2645
|
+
const top = isDark(row, col);
|
|
2646
|
+
const bottom = row + 1 < dim ? isDark(row + 1, col) : false;
|
|
2647
|
+
line += top ? bottom ? FULL : HALF_UPPER : bottom ? HALF_LOWER : " ";
|
|
2648
|
+
}
|
|
2649
|
+
lines.push(line);
|
|
2650
|
+
}
|
|
2651
|
+
return lines.join("\n");
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
// src/output.ts
|
|
2655
|
+
var renderMetaLines = (meta) => {
|
|
2656
|
+
if (!meta) {
|
|
2657
|
+
return [];
|
|
2658
|
+
}
|
|
2659
|
+
return [
|
|
2660
|
+
"Meta",
|
|
2661
|
+
` Command: ${meta.command}`,
|
|
2662
|
+
` Timestamp: ${meta.timestamp}`,
|
|
2663
|
+
...typeof meta.duration_ms === "number" ? [` Duration: ${meta.duration_ms} ms`] : []
|
|
2664
|
+
];
|
|
2665
|
+
};
|
|
2666
|
+
var formatScalar = (value, flags) => {
|
|
2667
|
+
if (typeof value === "string") {
|
|
2668
|
+
return value;
|
|
2669
|
+
}
|
|
2670
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2671
|
+
return String(value);
|
|
2672
|
+
}
|
|
2673
|
+
if (value === null || value === void 0) {
|
|
2674
|
+
return "null";
|
|
2675
|
+
}
|
|
2676
|
+
return formatJson(value, flags);
|
|
2677
|
+
};
|
|
2678
|
+
var renderFields = (title, fields, flags) => {
|
|
2679
|
+
const visibleFields = fields.filter(([, value]) => value !== void 0);
|
|
2680
|
+
if (visibleFields.length === 0) {
|
|
2681
|
+
return [];
|
|
2682
|
+
}
|
|
2683
|
+
const width = Math.max(...visibleFields.map(([label]) => label.length));
|
|
2684
|
+
return [
|
|
2685
|
+
title,
|
|
2686
|
+
...visibleFields.map(([label, value]) => ` ${label.padEnd(width)} ${formatScalar(value, flags)}`)
|
|
2687
|
+
];
|
|
2688
|
+
};
|
|
2689
|
+
var formatAge = (fromIso, toIso) => {
|
|
2690
|
+
const elapsedMs = Math.max(0, new Date(toIso).getTime() - new Date(fromIso).getTime());
|
|
2691
|
+
const elapsedSeconds = Math.floor(elapsedMs / 1e3);
|
|
2692
|
+
if (elapsedSeconds < 60) {
|
|
2693
|
+
return `${elapsedSeconds}s`;
|
|
2694
|
+
}
|
|
2695
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
2696
|
+
if (elapsedMinutes < 60) {
|
|
2697
|
+
return `${elapsedMinutes}m`;
|
|
2698
|
+
}
|
|
2699
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
2700
|
+
if (elapsedHours < 24) {
|
|
2701
|
+
return `${elapsedHours}h`;
|
|
2702
|
+
}
|
|
2703
|
+
return `${Math.floor(elapsedHours / 24)}d`;
|
|
2704
|
+
};
|
|
2705
|
+
var formatDevice = (device) => {
|
|
2706
|
+
const parts = [device.manufacturer, device.model].filter((part) => Boolean(part));
|
|
2707
|
+
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
2708
|
+
return device.os ? `${label} (${device.os})` : label;
|
|
2709
|
+
};
|
|
2710
|
+
var renderLsData = (colors, data, now) => {
|
|
2711
|
+
if (data.length === 0) {
|
|
2712
|
+
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
2713
|
+
}
|
|
2714
|
+
const nowIso = now.toISOString();
|
|
2715
|
+
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
2716
|
+
const rows = data.map((session) => [
|
|
2717
|
+
session.alias,
|
|
2718
|
+
session.state,
|
|
2719
|
+
formatDevice(session.device),
|
|
2720
|
+
String(session.toolCount),
|
|
2721
|
+
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
2722
|
+
]);
|
|
2723
|
+
const widths = headers.map(
|
|
2724
|
+
(header, index) => Math.max(header.length, ...rows.map((row) => row[index].length))
|
|
2725
|
+
);
|
|
2726
|
+
return [
|
|
2727
|
+
colors.green("Sessions"),
|
|
2728
|
+
` ${headers.map((header, index) => header.padEnd(widths[index])).join(" ")}`,
|
|
2729
|
+
...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
|
|
2730
|
+
];
|
|
2731
|
+
};
|
|
2732
|
+
var isToolsListing = (data) => {
|
|
2733
|
+
return typeof data === "object" && data !== null && Array.isArray(data.tools);
|
|
2734
|
+
};
|
|
2735
|
+
var isToolGroupsListing = (data) => {
|
|
2736
|
+
return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
|
|
2737
|
+
};
|
|
2738
|
+
var UNGROUPED_LABEL = "(ungrouped)";
|
|
2739
|
+
var MAX_FOOTER_GROUPS = 10;
|
|
2740
|
+
var hasAnyGroup = (groups) => {
|
|
2741
|
+
return (groups ?? []).some((entry) => entry.group !== null);
|
|
2742
|
+
};
|
|
2743
|
+
var topLevelGroups = (groups) => {
|
|
2744
|
+
return (groups ?? []).filter((entry) => entry.group !== null && !entry.group.includes("/"));
|
|
2745
|
+
};
|
|
2746
|
+
var renderEmptyToolsLine = (data) => {
|
|
2747
|
+
if (data.total > 0) {
|
|
2748
|
+
return ` No tools at offset ${data.offset ?? 0}; ${data.total} matching tool${data.total === 1 ? "" : "s"} in total.`;
|
|
2749
|
+
}
|
|
2750
|
+
if (data.group !== void 0) {
|
|
2751
|
+
const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
|
|
2752
|
+
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
|
|
2753
|
+
}
|
|
2754
|
+
return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
|
|
2755
|
+
};
|
|
2756
|
+
var renderGroupHint = (data) => {
|
|
2757
|
+
if (data.group !== void 0 || !hasAnyGroup(data.groups)) {
|
|
2758
|
+
return void 0;
|
|
2759
|
+
}
|
|
2760
|
+
const top = topLevelGroups(data.groups);
|
|
2761
|
+
const named = top.slice(0, MAX_FOOTER_GROUPS).map((entry) => `${entry.group} ${entry.total}`);
|
|
2762
|
+
const more = top.length > MAX_FOOTER_GROUPS ? `, ... ${top.length - MAX_FOOTER_GROUPS} more; see --groups` : "";
|
|
2763
|
+
return `--group <name> (groups: ${named.join(", ")}${more})`;
|
|
2764
|
+
};
|
|
2765
|
+
var renderTruncationLine = (data) => {
|
|
2766
|
+
if (data.tools.length >= data.total) {
|
|
2767
|
+
return [];
|
|
2768
|
+
}
|
|
2769
|
+
const groupHint = renderGroupHint(data);
|
|
2770
|
+
const narrow = groupHint === void 0 ? "--filter <text> or" : `${groupHint} or --filter <text>, or`;
|
|
2771
|
+
return [
|
|
2772
|
+
"",
|
|
2773
|
+
`Showing ${data.tools.length} of ${data.total} tools (offset ${data.offset ?? 0}). Narrow with ${narrow} page with --offset <n>.`
|
|
2774
|
+
];
|
|
2775
|
+
};
|
|
2776
|
+
var renderToolSummaryLines = (tool, indent) => {
|
|
2777
|
+
const tag = tool.policy === "allow" ? "" : ` [${tool.policy}]`;
|
|
2778
|
+
return [`${indent}${renderToolSignature(tool)}${tag}`, `${indent} ${summarizeToolDescription(tool.description)}`];
|
|
2779
|
+
};
|
|
2780
|
+
var renderGroupedToolLines = (colors, tools) => {
|
|
2781
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
2782
|
+
for (const tool of tools) {
|
|
2783
|
+
const key = tool.group ?? null;
|
|
2784
|
+
const bucket = byGroup.get(key);
|
|
2785
|
+
if (bucket) {
|
|
2786
|
+
bucket.push(tool);
|
|
2787
|
+
} else {
|
|
2788
|
+
byGroup.set(key, [tool]);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const tops = /* @__PURE__ */ new Map();
|
|
2792
|
+
for (const key of byGroup.keys()) {
|
|
2793
|
+
if (key === null) {
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
const slash = key.indexOf("/");
|
|
2797
|
+
const top = slash === -1 ? key : key.slice(0, slash);
|
|
2798
|
+
const subs = tops.get(top) ?? [];
|
|
2799
|
+
if (slash !== -1) {
|
|
2800
|
+
subs.push(key);
|
|
2801
|
+
}
|
|
2802
|
+
tops.set(top, subs);
|
|
2803
|
+
}
|
|
2804
|
+
const byCodePoint = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
2805
|
+
const lines = [];
|
|
2806
|
+
for (const top of [...tops.keys()].sort(byCodePoint)) {
|
|
2807
|
+
lines.push(` ${colors.cyan(top)}`);
|
|
2808
|
+
lines.push(...(byGroup.get(top) ?? []).flatMap((tool) => renderToolSummaryLines(tool, " ")));
|
|
2809
|
+
for (const sub of tops.get(top).sort(byCodePoint)) {
|
|
2810
|
+
lines.push(` ${colors.cyan(sub)}`);
|
|
2811
|
+
lines.push(...byGroup.get(sub).flatMap((tool) => renderToolSummaryLines(tool, " ")));
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
const ungrouped = byGroup.get(null);
|
|
2815
|
+
if (ungrouped) {
|
|
2816
|
+
lines.push(` ${colors.cyan(UNGROUPED_LABEL)}`);
|
|
2817
|
+
lines.push(...ungrouped.flatMap((tool) => renderToolSummaryLines(tool, " ")));
|
|
2818
|
+
}
|
|
2819
|
+
return lines;
|
|
2820
|
+
};
|
|
2821
|
+
var renderToolsTitle = (colors, data) => {
|
|
2822
|
+
return colors.green(data.group === void 0 ? "Tools" : `Tools in group ${data.group}`);
|
|
2823
|
+
};
|
|
2824
|
+
var renderToolSummaryTable = (colors, data) => {
|
|
2825
|
+
if (data.tools.length === 0) {
|
|
2826
|
+
return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
|
|
2827
|
+
}
|
|
2828
|
+
const grouped = data.group === void 0 && hasAnyGroup(data.groups);
|
|
2829
|
+
return [
|
|
2830
|
+
renderToolsTitle(colors, data),
|
|
2831
|
+
...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
|
|
2832
|
+
...renderTruncationLine(data),
|
|
2833
|
+
"",
|
|
2834
|
+
"Run `appduct tools <name>` for a tool's full schema."
|
|
2835
|
+
];
|
|
2836
|
+
};
|
|
2837
|
+
var renderToolDetail = (colors, tool, flags) => {
|
|
2838
|
+
return renderFields(
|
|
2839
|
+
colors.green(`Tool: ${tool.name}`),
|
|
2840
|
+
[
|
|
2841
|
+
["Signature", renderToolSignature(tool)],
|
|
2842
|
+
["Description", tool.description],
|
|
2843
|
+
["Input schema", tool.input_schema],
|
|
2844
|
+
["Output schema", tool.output_schema],
|
|
2845
|
+
// `?? undefined` keeps an ungrouped tool's row hidden rather than printing a bare "null":
|
|
2846
|
+
// `renderFields` drops undefined rows, and the machine form's `null` is for consumers, not
|
|
2847
|
+
// for a human scanning one tool's schema.
|
|
2848
|
+
["Group", tool.group ?? void 0],
|
|
2849
|
+
["Annotations", tool.annotations],
|
|
2850
|
+
// Only rendered for a tool that declares one; `renderFields` drops undefined rows, so a
|
|
2851
|
+
// tool on the daemon's default deadline shows no line at all rather than a misleading
|
|
2852
|
+
// "10000".
|
|
2853
|
+
["Timeout (ms)", tool.timeout_ms]
|
|
2854
|
+
],
|
|
2855
|
+
flags
|
|
2856
|
+
);
|
|
2857
|
+
};
|
|
2858
|
+
var renderToolsFullListing = (colors, data, flags) => {
|
|
2859
|
+
if (data.tools.length === 0) {
|
|
2860
|
+
return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
|
|
2861
|
+
}
|
|
2862
|
+
return [
|
|
2863
|
+
...data.tools.flatMap((tool, index) => [
|
|
2864
|
+
...index > 0 ? [""] : [],
|
|
2865
|
+
...renderToolDetail(colors, tool, flags)
|
|
2866
|
+
]),
|
|
2867
|
+
...renderTruncationLine(data)
|
|
2868
|
+
];
|
|
2869
|
+
};
|
|
2870
|
+
var renderToolGroups = (colors, data) => {
|
|
2871
|
+
if (data.groups.length === 0) {
|
|
2872
|
+
return [colors.green("Groups"), " No tools registered."];
|
|
2873
|
+
}
|
|
2874
|
+
const label = (entry) => {
|
|
2875
|
+
if (entry.group === null) {
|
|
2876
|
+
return UNGROUPED_LABEL;
|
|
2877
|
+
}
|
|
2878
|
+
return entry.group.includes("/") ? ` ${entry.group}` : entry.group;
|
|
2879
|
+
};
|
|
2880
|
+
const width = Math.max(...data.groups.map((entry) => label(entry).length));
|
|
2881
|
+
const countWidth = Math.max(...data.groups.map((entry) => String(entry.total).length));
|
|
2882
|
+
return [
|
|
2883
|
+
colors.green("Groups"),
|
|
2884
|
+
...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
|
|
2885
|
+
"",
|
|
2886
|
+
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
2887
|
+
];
|
|
2888
|
+
};
|
|
2889
|
+
var renderToolsData = (colors, data, flags, full) => {
|
|
2890
|
+
if (isToolGroupsListing(data)) {
|
|
2891
|
+
return renderToolGroups(colors, data);
|
|
2892
|
+
}
|
|
2893
|
+
if (!isToolsListing(data)) {
|
|
2894
|
+
return renderToolDetail(colors, data, flags);
|
|
2895
|
+
}
|
|
2896
|
+
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
2897
|
+
};
|
|
2898
|
+
var renderInvokeData = (colors, data, flags) => {
|
|
2899
|
+
return [colors.green("Result"), formatScalar(data, flags)];
|
|
2900
|
+
};
|
|
2901
|
+
var renderLinkData = (colors, data, flags, qr) => {
|
|
2902
|
+
const lines = [
|
|
2903
|
+
colors.green("Link Created"),
|
|
2904
|
+
...renderFields(
|
|
2905
|
+
"Link",
|
|
2906
|
+
[
|
|
2907
|
+
["Session", data.sessionId],
|
|
2908
|
+
["Deep link", data.deepLink],
|
|
2909
|
+
["Endpoint", formatAgentWebSocketUrl(data.endpoint)],
|
|
2910
|
+
["Pin", data.pin],
|
|
2911
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()],
|
|
2912
|
+
["Delivered", data.delivered ? `yes (${data.target})` : void 0]
|
|
2913
|
+
],
|
|
2914
|
+
flags
|
|
2915
|
+
)
|
|
2916
|
+
];
|
|
2917
|
+
if (qr) {
|
|
2918
|
+
lines.push("", "QR", renderQrToTerminal(data.deepLink));
|
|
2919
|
+
}
|
|
2920
|
+
return lines;
|
|
2921
|
+
};
|
|
2922
|
+
var renderRevokeData = (colors, _data) => {
|
|
2923
|
+
return [colors.green("Session Revoked")];
|
|
2924
|
+
};
|
|
2925
|
+
var renderKeygenData = (colors, data, flags) => {
|
|
2926
|
+
return [
|
|
2927
|
+
colors.green("Key Ready"),
|
|
2928
|
+
...renderFields(
|
|
2929
|
+
"Key",
|
|
2930
|
+
[
|
|
2931
|
+
["Path", data.path],
|
|
2932
|
+
["Fingerprint", data.pin]
|
|
2933
|
+
],
|
|
2934
|
+
flags
|
|
2935
|
+
)
|
|
2936
|
+
];
|
|
2937
|
+
};
|
|
2938
|
+
var renderInitData = (colors, data, flags) => {
|
|
2939
|
+
const snippet = JSON.stringify(
|
|
2940
|
+
{ mcpServers: { appduct: data.mcpServerEntry } },
|
|
2941
|
+
null,
|
|
2942
|
+
2
|
|
2943
|
+
);
|
|
2944
|
+
return [
|
|
2945
|
+
colors.green(data.changed ? "Project Initialized" : "Project Already Initialized"),
|
|
2946
|
+
...renderFields(
|
|
2947
|
+
"Config",
|
|
2948
|
+
[
|
|
2949
|
+
["Path", data.path],
|
|
2950
|
+
["Scheme", data.scheme],
|
|
2951
|
+
["Source", data.source],
|
|
2952
|
+
// Only present for a scheme discovery read off a static project file (app.json, or one of
|
|
2953
|
+
// the native Android/iOS probes) — names the exact file/key, not just which platform.
|
|
2954
|
+
["Read from", data.origin],
|
|
2955
|
+
["Written", data.changed ? data.created ? "created" : "updated" : "unchanged"]
|
|
2956
|
+
],
|
|
2957
|
+
flags
|
|
2958
|
+
),
|
|
2959
|
+
...data.note === void 0 ? [] : ["", colors.yellow(`Note: ${data.note}`)],
|
|
2960
|
+
"",
|
|
2961
|
+
"MCP server entry",
|
|
2962
|
+
...snippet.split("\n").map((line) => ` ${line}`),
|
|
2963
|
+
"",
|
|
2964
|
+
"Next",
|
|
2965
|
+
...data.nextSteps.map((step, index) => ` ${index + 1}. ${step}`)
|
|
2966
|
+
];
|
|
2967
|
+
};
|
|
2968
|
+
var renderDaemonRunData = (colors, data, flags) => {
|
|
2969
|
+
return [
|
|
2970
|
+
colors.green("Daemon Running"),
|
|
2971
|
+
...renderFields(
|
|
2972
|
+
"Daemon",
|
|
2973
|
+
[
|
|
2974
|
+
["PID", data.daemon.pid],
|
|
2975
|
+
["State dir", data.daemon.state_dir],
|
|
2976
|
+
["Socket", data.daemon.socket_path]
|
|
2977
|
+
],
|
|
2978
|
+
flags
|
|
2979
|
+
)
|
|
2980
|
+
];
|
|
2981
|
+
};
|
|
2982
|
+
var renderDaemonStartData = (colors, data, flags) => {
|
|
2983
|
+
return [
|
|
2984
|
+
colors.green("Daemon Started"),
|
|
2985
|
+
...renderFields(
|
|
2986
|
+
"Daemon",
|
|
2987
|
+
[
|
|
2988
|
+
["PID", data.daemon.pid],
|
|
2989
|
+
["WSS port", data.daemon.wss_port],
|
|
2990
|
+
["Started at", data.daemon.started_at]
|
|
2991
|
+
],
|
|
2992
|
+
flags
|
|
2993
|
+
)
|
|
2994
|
+
];
|
|
2995
|
+
};
|
|
2996
|
+
var renderDaemonStopData = (colors, data, flags) => {
|
|
2997
|
+
return [
|
|
2998
|
+
colors.green("Daemon Stopped"),
|
|
2999
|
+
...renderFields("Daemon", [["Method", data.daemon.method]], flags)
|
|
3000
|
+
];
|
|
3001
|
+
};
|
|
3002
|
+
var formatByteSize = (bytes) => {
|
|
3003
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
3004
|
+
let value = bytes;
|
|
3005
|
+
let unit = 0;
|
|
3006
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
3007
|
+
value /= 1024;
|
|
3008
|
+
unit += 1;
|
|
3009
|
+
}
|
|
3010
|
+
return `${unit === 0 ? value : value.toFixed(1)} ${units[unit]}`;
|
|
3011
|
+
};
|
|
3012
|
+
var renderDaemonStatusData = (colors, data, flags) => {
|
|
3013
|
+
return [
|
|
3014
|
+
colors.green("Daemon Status"),
|
|
3015
|
+
...renderFields(
|
|
3016
|
+
"Daemon",
|
|
3017
|
+
[
|
|
3018
|
+
["Version", data.daemon.version],
|
|
3019
|
+
["PID", data.daemon.pid],
|
|
3020
|
+
["Started at", data.daemon.started_at],
|
|
3021
|
+
["WSS port", data.daemon.wss_port],
|
|
3022
|
+
["Pinned keys", data.daemon.pinned_keys.length],
|
|
3023
|
+
["Sessions", data.daemon.session_count]
|
|
3024
|
+
],
|
|
3025
|
+
flags
|
|
3026
|
+
),
|
|
3027
|
+
"",
|
|
3028
|
+
...renderFields(
|
|
3029
|
+
"Policy",
|
|
3030
|
+
[
|
|
3031
|
+
["Default", data.policy.default],
|
|
3032
|
+
["Destructive", data.policy.destructive],
|
|
3033
|
+
["Overrides", data.policy.tools ? Object.keys(data.policy.tools).length : 0]
|
|
3034
|
+
],
|
|
3035
|
+
flags
|
|
3036
|
+
),
|
|
3037
|
+
"",
|
|
3038
|
+
// The retention rows drop out entirely against a daemon that predates them (`renderFields`
|
|
3039
|
+
// skips `undefined`), rather than printing "undefined" or an invented zero.
|
|
3040
|
+
...renderFields(
|
|
3041
|
+
"Audit",
|
|
3042
|
+
[
|
|
3043
|
+
["Path", data.audit.path],
|
|
3044
|
+
["Retention", data.audit.retention_days === void 0 ? void 0 : `${data.audit.retention_days} days`],
|
|
3045
|
+
["Files", data.audit.files],
|
|
3046
|
+
["Size", data.audit.bytes === void 0 ? void 0 : formatByteSize(data.audit.bytes)],
|
|
3047
|
+
["Failed writes", data.audit.failed_writes],
|
|
3048
|
+
["Failed prunes", data.audit.failed_prunes]
|
|
3049
|
+
],
|
|
3050
|
+
flags
|
|
3051
|
+
),
|
|
3052
|
+
// Version drift (issue #30) is the one thing here an operator has to act on, so it goes last
|
|
3053
|
+
// — the line still on screen after the block scrolls — and in yellow, not a quiet field row.
|
|
3054
|
+
...data.warning ? ["", colors.yellow(`Warning: ${data.warning}`)] : []
|
|
3055
|
+
];
|
|
3056
|
+
};
|
|
3057
|
+
var renderDoctorData = (colors, data, flags) => {
|
|
3058
|
+
return [
|
|
3059
|
+
data.present ? colors.green("Appduct Present") : colors.yellow("Appduct Absent"),
|
|
3060
|
+
...renderFields(
|
|
3061
|
+
"Artifact",
|
|
3062
|
+
[
|
|
3063
|
+
["Path", data.artifact],
|
|
3064
|
+
["Platform", data.platform],
|
|
3065
|
+
["Format", data.format],
|
|
3066
|
+
["Present", data.present],
|
|
3067
|
+
["Signals", data.signals.length > 0 ? data.signals.join(", ") : "none"],
|
|
3068
|
+
["Assertion", data.assertion ? `${data.assertion.expected} (holds)` : void 0]
|
|
3069
|
+
],
|
|
3070
|
+
flags
|
|
3071
|
+
)
|
|
3072
|
+
];
|
|
3073
|
+
};
|
|
3074
|
+
var renderSuccessData = (colors, command, data, options) => {
|
|
3075
|
+
const flags = options.flags;
|
|
3076
|
+
switch (command) {
|
|
3077
|
+
case "init":
|
|
3078
|
+
return renderInitData(colors, data, flags);
|
|
3079
|
+
case "keygen":
|
|
3080
|
+
return renderKeygenData(colors, data, flags);
|
|
3081
|
+
case "link":
|
|
3082
|
+
return renderLinkData(colors, data, flags, options.qr);
|
|
3083
|
+
case "ls":
|
|
3084
|
+
return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
|
|
3085
|
+
case "tools":
|
|
3086
|
+
return renderToolsData(colors, data, flags, options.full);
|
|
3087
|
+
case "invoke":
|
|
3088
|
+
return renderInvokeData(colors, data, flags);
|
|
3089
|
+
case "revoke":
|
|
3090
|
+
return renderRevokeData(colors, data);
|
|
3091
|
+
case "daemon run":
|
|
3092
|
+
return renderDaemonRunData(colors, data, flags);
|
|
3093
|
+
case "daemon start":
|
|
3094
|
+
return renderDaemonStartData(colors, data, flags);
|
|
3095
|
+
case "daemon stop":
|
|
3096
|
+
return renderDaemonStopData(colors, data, flags);
|
|
3097
|
+
case "daemon status":
|
|
3098
|
+
return renderDaemonStatusData(colors, data, flags);
|
|
3099
|
+
case "doctor":
|
|
3100
|
+
return renderDoctorData(colors, data, flags);
|
|
3101
|
+
default:
|
|
3102
|
+
return [colors.green("Command Complete"), formatJson(data, flags)];
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
var renderHumanError = (colors, error, flags, meta) => {
|
|
3106
|
+
return [
|
|
3107
|
+
colors.red("Command Failed"),
|
|
3108
|
+
...renderFields(
|
|
3109
|
+
"Error",
|
|
3110
|
+
[
|
|
3111
|
+
["Type", error.type],
|
|
3112
|
+
["Message", error.message],
|
|
3113
|
+
["Details", error.details]
|
|
3114
|
+
],
|
|
3115
|
+
flags
|
|
3116
|
+
),
|
|
3117
|
+
...meta ? ["", ...renderMetaLines(meta)] : []
|
|
3118
|
+
].join("\n");
|
|
3119
|
+
};
|
|
3120
|
+
var renderResult = (result, options) => {
|
|
3121
|
+
if (options.flags.json) {
|
|
3122
|
+
return {
|
|
3123
|
+
stdout: `${formatJson(result, options.flags)}
|
|
3124
|
+
`
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
const colors = pc.createColors(options.flags.color);
|
|
3128
|
+
if (!result.ok) {
|
|
3129
|
+
return {
|
|
3130
|
+
stderr: `${renderHumanError(colors, result.error, options.flags, result.meta)}
|
|
3131
|
+
`
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
const lines = [
|
|
3135
|
+
...renderSuccessData(colors, options.command, result.data, options),
|
|
3136
|
+
"",
|
|
3137
|
+
...renderMetaLines(result.meta)
|
|
3138
|
+
].filter((line, index, collection) => {
|
|
3139
|
+
if (line !== "") {
|
|
3140
|
+
return true;
|
|
3141
|
+
}
|
|
3142
|
+
return index > 0 && collection[index - 1] !== "" && index < collection.length - 1;
|
|
3143
|
+
});
|
|
3144
|
+
return {
|
|
3145
|
+
stdout: `${lines.join("\n")}
|
|
3146
|
+
`
|
|
3147
|
+
};
|
|
3148
|
+
};
|
|
3149
|
+
|
|
3150
|
+
// src/cli/envelope.ts
|
|
3151
|
+
var createCommandMeta = (command, startedAt, finishedAt) => {
|
|
3152
|
+
return {
|
|
3153
|
+
command,
|
|
3154
|
+
timestamp: finishedAt.toISOString(),
|
|
3155
|
+
duration_ms: finishedAt.getTime() - startedAt.getTime()
|
|
3156
|
+
};
|
|
3157
|
+
};
|
|
3158
|
+
var finalizeResult = (result, timing, flags) => {
|
|
3159
|
+
if (!flags.verbose) {
|
|
3160
|
+
return result;
|
|
3161
|
+
}
|
|
3162
|
+
return {
|
|
3163
|
+
...result,
|
|
3164
|
+
meta: createCommandMeta(timing.command, timing.startedAt, timing.finishedAt)
|
|
3165
|
+
};
|
|
3166
|
+
};
|
|
3167
|
+
|
|
3168
|
+
// src/cli/runner.ts
|
|
3169
|
+
var writeRenderedOutput = (rendered, writers) => {
|
|
3170
|
+
if (rendered.stdout) {
|
|
3171
|
+
writers.stdout.write(rendered.stdout);
|
|
3172
|
+
}
|
|
3173
|
+
if (rendered.stderr) {
|
|
3174
|
+
writers.stderr.write(rendered.stderr);
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
var executeHostedCommand = async (command, handler, env, reporter) => {
|
|
3178
|
+
const startedAt = env.clock.now();
|
|
3179
|
+
let renderedSuccess = false;
|
|
3180
|
+
try {
|
|
3181
|
+
const hosted = await handler();
|
|
3182
|
+
const finishedAt = env.clock.now();
|
|
3183
|
+
const finalized = finalizeResult(hosted.result, { command, startedAt, finishedAt }, env.flags);
|
|
3184
|
+
const shouldRenderBootstrap = !reporter || reporter.kind === "plain";
|
|
3185
|
+
if (shouldRenderBootstrap) {
|
|
3186
|
+
writeRenderedOutput(
|
|
3187
|
+
renderResult(finalized, { command, flags: env.flags, now: finishedAt }),
|
|
3188
|
+
env
|
|
3189
|
+
);
|
|
3190
|
+
renderedSuccess = true;
|
|
3191
|
+
}
|
|
3192
|
+
let resolved = false;
|
|
3193
|
+
const stop = () => {
|
|
3194
|
+
if (resolved) {
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
resolved = true;
|
|
3198
|
+
hosted.stop();
|
|
3199
|
+
};
|
|
3200
|
+
process.once("SIGINT", stop);
|
|
3201
|
+
process.once("SIGTERM", stop);
|
|
3202
|
+
try {
|
|
3203
|
+
await hosted.completion;
|
|
3204
|
+
} finally {
|
|
3205
|
+
resolved = true;
|
|
3206
|
+
process.off("SIGINT", stop);
|
|
3207
|
+
process.off("SIGTERM", stop);
|
|
3208
|
+
reporter?.dispose();
|
|
3209
|
+
}
|
|
3210
|
+
return 0;
|
|
3211
|
+
} catch (error) {
|
|
3212
|
+
reporter?.dispose();
|
|
3213
|
+
const finishedAt = env.clock.now();
|
|
3214
|
+
const cliError = toCliError(error);
|
|
3215
|
+
const result = { ok: false, error: cliError };
|
|
3216
|
+
const finalized = finalizeResult(result, { command, startedAt, finishedAt }, env.flags);
|
|
3217
|
+
if (renderedSuccess && env.flags.json) {
|
|
3218
|
+
env.stderr.write(`${formatJson(finalized, env.flags)}
|
|
3219
|
+
`);
|
|
3220
|
+
return getExitCodeForError(error);
|
|
3221
|
+
}
|
|
3222
|
+
writeRenderedOutput(
|
|
3223
|
+
renderResult(finalized, { command, flags: env.flags, now: finishedAt }),
|
|
3224
|
+
env
|
|
3225
|
+
);
|
|
3226
|
+
return getExitCodeForError(error);
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
|
|
3230
|
+
// src/cli/router.ts
|
|
3231
|
+
var commandName = (context) => {
|
|
3232
|
+
return context.path.join(" ");
|
|
3233
|
+
};
|
|
3234
|
+
|
|
3235
|
+
// src/cli/routes/daemon/run.ts
|
|
3236
|
+
var route = async (context) => {
|
|
3237
|
+
const { stateDir, env } = context;
|
|
3238
|
+
return executeHostedCommand(
|
|
3239
|
+
commandName(context),
|
|
3240
|
+
() => handleDaemonRunCommand({ stateDir, clock: env.clock }),
|
|
3241
|
+
env
|
|
3242
|
+
);
|
|
3243
|
+
};
|
|
3244
|
+
export {
|
|
3245
|
+
route
|
|
3246
|
+
};
|