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
package/dist/commands/tools.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `appduct tools` (ARCHITECTURE.md §10): `tools [selector] [--full]` lists tools for a session;
|
|
3
|
-
* `tools [selector] <name>` shows one tool's full schema/annotations.
|
|
4
|
-
*
|
|
5
|
-
* The command table gives both forms a leading optional `[selector]`, which makes a single
|
|
6
|
-
* positional argument inherently ambiguous (is it the selector, or the tool name in `tools <name>`
|
|
7
|
-
* with the selector omitted?). This resolves it the same way a human reading the table would:
|
|
8
|
-
* first try the arg as a tool name in the implicit-selector session's registry; if no such tool
|
|
9
|
-
* exists there (or the implicit selector doesn't resolve, e.g. `ambiguous_session`), fall back to
|
|
10
|
-
* treating it as a selector and list that session's tools instead.
|
|
11
|
-
*/
|
|
12
|
-
import { RPC_METHODS } from "@appduct/shared";
|
|
13
|
-
import { usageError } from "../errors.js";
|
|
14
|
-
import { callDaemon, DaemonRpcError } from "../rpc/client.js";
|
|
15
|
-
const listTools = (selector, context) => {
|
|
16
|
-
return callDaemon(RPC_METHODS.toolsList, { selector }, { stateDir: context.stateDir, spawn: context.spawn });
|
|
17
|
-
};
|
|
18
|
-
const findTool = (tools, name) => {
|
|
19
|
-
return tools.find((tool) => tool.name === name);
|
|
20
|
-
};
|
|
21
|
-
export const handleToolsCommand = async (options, context) => {
|
|
22
|
-
if (options.selector !== undefined && options.name !== undefined) {
|
|
23
|
-
const tools = await listTools(options.selector, context);
|
|
24
|
-
const tool = findTool(tools, options.name);
|
|
25
|
-
if (!tool) {
|
|
26
|
-
throw usageError(`Tool "${options.name}" is not registered on session "${options.selector}".`, { available: tools.map((entry) => entry.name) });
|
|
27
|
-
}
|
|
28
|
-
return { ok: true, data: tool };
|
|
29
|
-
}
|
|
30
|
-
if (options.selector !== undefined) {
|
|
31
|
-
// A single positional arg: try it as the implicit session's tool name first.
|
|
32
|
-
let implicitTools;
|
|
33
|
-
try {
|
|
34
|
-
implicitTools = await listTools(undefined, context);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (!(error instanceof DaemonRpcError)) {
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (implicitTools) {
|
|
42
|
-
const tool = findTool(implicitTools, options.selector);
|
|
43
|
-
if (tool) {
|
|
44
|
-
return { ok: true, data: tool };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Not a tool name on the implicit session (or there is no implicit session): treat the arg as
|
|
48
|
-
// a selector and list that session's tools instead.
|
|
49
|
-
return { ok: true, data: await listTools(options.selector, context) };
|
|
50
|
-
}
|
|
51
|
-
return { ok: true, data: await listTools(undefined, context) };
|
|
52
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/daemon/address.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qFAAqF;AACrF,eAAO,MAAM,4BAA4B,GAAI,SAAS,MAAM,KAAG,OAE9D,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,OAWpD,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,SAAS,MAAM,KAAG,OAEvD,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAS,8BAAmC,KAC3C,iBA+CF,CAAC"}
|
package/dist/daemon/address.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Advertised-address detection (ARCHITECTURE.md §2, §8 bootstrap endpoint). Prefers a
|
|
3
|
-
* non-internal private IPv4 address, falling back to IPv6, then loopback. Fixes v1's
|
|
4
|
-
* `detectHostIp`, which happily returned a Docker bridge address (`172.17.x.x`, inside the RFC
|
|
5
|
-
* 1918 172.16/12 block v1 treated as "private") — that address is unreachable from a physical
|
|
6
|
-
* device and made the printed deep link useless. Also excludes the IPv4 CGNAT range
|
|
7
|
-
* (`100.64.0.0/10`), which is routable-looking but not device-reachable in the common case.
|
|
8
|
-
*/
|
|
9
|
-
import { networkInterfaces } from "node:os";
|
|
10
|
-
import { isLinkLocalIpv6Address, isPrivateIpv4Address, isUniqueLocalIpv6Address, } from "@appduct/shared";
|
|
11
|
-
/** Docker's default bridge network (`docker0`), the exact defect v1 shipped with. */
|
|
12
|
-
export const isDockerDefaultBridgeAddress = (address) => {
|
|
13
|
-
return address.startsWith("172.17.");
|
|
14
|
-
};
|
|
15
|
-
/** Carrier-grade NAT shared address space (RFC 6598): `100.64.0.0/10`. */
|
|
16
|
-
export const isCgnatIpv4Address = (address) => {
|
|
17
|
-
const parts = address.split(".");
|
|
18
|
-
if (parts.length !== 4) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
const first = Number(parts[0]);
|
|
22
|
-
const second = Number(parts[1]);
|
|
23
|
-
return first === 100 && Number.isInteger(second) && second >= 64 && second <= 127;
|
|
24
|
-
};
|
|
25
|
-
export const isExcludedIpv4Address = (address) => {
|
|
26
|
-
return isDockerDefaultBridgeAddress(address) || isCgnatIpv4Address(address);
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Enumerates non-internal interface addresses and picks the best advertisable candidate: a
|
|
30
|
-
* private (RFC 1918) IPv4 address first (excluding Docker/CGNAT), then any other non-internal
|
|
31
|
-
* IPv4, then a unique-local (`fd00::/8`) IPv6 address, then any other non-internal, non-link-local
|
|
32
|
-
* IPv6 address, finally falling back to loopback if nothing else is available.
|
|
33
|
-
*/
|
|
34
|
-
export const detectAdvertisedAddress = (options = {}) => {
|
|
35
|
-
if (options.override) {
|
|
36
|
-
return {
|
|
37
|
-
family: options.override.includes(":") ? 6 : 4,
|
|
38
|
-
address: options.override,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
const ipv4Candidates = [];
|
|
42
|
-
const ipv6Candidates = [];
|
|
43
|
-
for (const entries of Object.values(networkInterfaces())) {
|
|
44
|
-
for (const entry of entries ?? []) {
|
|
45
|
-
if (entry.internal) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (entry.family === "IPv4") {
|
|
49
|
-
if (isExcludedIpv4Address(entry.address)) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
ipv4Candidates.push(entry.address);
|
|
53
|
-
}
|
|
54
|
-
else if (entry.family === "IPv6") {
|
|
55
|
-
if (isLinkLocalIpv6Address(entry.address)) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
ipv6Candidates.push(entry.address);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const preferredIpv4 = ipv4Candidates.find((address) => isPrivateIpv4Address(address)) ?? ipv4Candidates[0];
|
|
63
|
-
if (preferredIpv4) {
|
|
64
|
-
return { family: 4, address: preferredIpv4 };
|
|
65
|
-
}
|
|
66
|
-
const preferredIpv6 = ipv6Candidates.find((address) => isUniqueLocalIpv6Address(address)) ?? ipv6Candidates[0];
|
|
67
|
-
if (preferredIpv6) {
|
|
68
|
-
return { family: 6, address: preferredIpv6 };
|
|
69
|
-
}
|
|
70
|
-
return { family: 4, address: "127.0.0.1" };
|
|
71
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/daemon/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAG7C,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AACnE,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG;IACxB,gBAAgB;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,oBAAoB,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;CACpC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,sGAAsG;IACtG,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB;uFACmF;IACnF,MAAM,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;IACjD,2EAA2E;IAC3E,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B;;;;OAIG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACvC;sCACkC;IAClC,KAAK,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACjC,uFAAuF;IACvF,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;+CAC2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8FAA8F;IAC9F,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAiBF,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAEjE,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAE1D,CAAC;AA8DF,eAAO,MAAM,iBAAiB,GAAI,SAAS,kBAAkB,KAAG,WA4K/D,CAAC"}
|
package/dist/daemon/audit.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Append-only per-day audit log (ARCHITECTURE.md §3, §12): one JSONL line per `tools.call`
|
|
3
|
-
* attempt (CLI and MCP alike), regardless of outcome. Raw args are **never** written — only a hex
|
|
4
|
-
* sha256 digest of their canonical JSON, so an operator can correlate repeated/identical calls
|
|
5
|
-
* without the log becoming a second copy of whatever secrets a tool's arguments carried.
|
|
6
|
-
*
|
|
7
|
-
* Writes are serialized through an internal promise queue so a slow disk never blocks the
|
|
8
|
-
* `tools.call` response path: {@link AuditLogger.record} enqueues and returns immediately. A write
|
|
9
|
-
* failure never throws back into the call path — it is logged to stderr and counted in
|
|
10
|
-
* {@link AuditLogger.failedWrites} (surfaced via `daemon.status`, ARCHITECTURE.md §12 item 4).
|
|
11
|
-
* {@link AuditLogger.flush} lets the daemon's shutdown teardown wait for the queue to drain.
|
|
12
|
-
*
|
|
13
|
-
* Retention (ARCHITECTURE.md §3): day files older than `config.auditRetentionDays` are deleted by
|
|
14
|
-
* {@link AuditLogger.prune}, which the daemon runs at startup and once a day thereafter. Pruning
|
|
15
|
-
* runs on the same queue as the writes, so it can never race an in-flight append, and its failures
|
|
16
|
-
* are counted in {@link AuditLogger.failedPrunes} and warned rather than thrown — exactly like a
|
|
17
|
-
* failed write.
|
|
18
|
-
*/
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
|
-
import { chmod, mkdir, open, readdir, rm, stat } from "node:fs/promises";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
import { DEFAULT_AUDIT_RETENTION_DAYS } from "./config.js";
|
|
23
|
-
/** Recursively sorts object keys so semantically-identical args always hash identically,
|
|
24
|
-
* regardless of the property insertion order the caller happened to use. */
|
|
25
|
-
const canonicalize = (value) => {
|
|
26
|
-
if (Array.isArray(value)) {
|
|
27
|
-
return value.map(canonicalize);
|
|
28
|
-
}
|
|
29
|
-
if (value !== null && typeof value === "object") {
|
|
30
|
-
const record = value;
|
|
31
|
-
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalize(record[key])]));
|
|
32
|
-
}
|
|
33
|
-
return value;
|
|
34
|
-
};
|
|
35
|
-
export const canonicalArgsJson = (args) => {
|
|
36
|
-
return JSON.stringify(canonicalize(args));
|
|
37
|
-
};
|
|
38
|
-
export const argsSha256 = (args) => {
|
|
39
|
-
return createHash("sha256").update(canonicalArgsJson(args)).digest("hex");
|
|
40
|
-
};
|
|
41
|
-
/** The UTC day stamp a record written at `date` lands in. UTC (not local time) because that is
|
|
42
|
-
* exactly what the file names have always been derived from — see {@link auditFilePathForDate}. */
|
|
43
|
-
const dayStamp = (date) => {
|
|
44
|
-
return date.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
45
|
-
};
|
|
46
|
-
/** Matches only the day files this module writes. Anything else in `audit/` — an operator's
|
|
47
|
-
* notes, a partial copy, a subdirectory — is never a pruning candidate. */
|
|
48
|
-
const AUDIT_DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl$/u;
|
|
49
|
-
/** The captured day stamp when `name` is one of our day files *and* the stamp is a real calendar
|
|
50
|
-
* date (so a plausible-looking `2026-13-45.jsonl` is left alone rather than compared as a string). */
|
|
51
|
-
const dayStampOfAuditFile = (name) => {
|
|
52
|
-
const match = AUDIT_DAY_FILE.exec(name);
|
|
53
|
-
if (!match) {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
const stamp = match[1];
|
|
57
|
-
const parsed = new Date(`${stamp}T00:00:00.000Z`);
|
|
58
|
-
return Number.isNaN(parsed.getTime()) || dayStamp(parsed) !== stamp ? undefined : stamp;
|
|
59
|
-
};
|
|
60
|
-
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
61
|
-
/**
|
|
62
|
-
* The oldest day stamp retention keeps: `retentionDays` UTC days before the day containing `now`.
|
|
63
|
-
* A day file is pruned when its stamp sorts strictly before this (ISO stamps are zero-padded, so
|
|
64
|
-
* lexicographic order is chronological order), i.e. once it is *more* than `retentionDays` days
|
|
65
|
-
* behind today. Today's file therefore can never be a candidate for any `retentionDays >= 1`.
|
|
66
|
-
*/
|
|
67
|
-
const oldestRetainedStamp = (now, retentionDays) => {
|
|
68
|
-
const todayUtcMidnight = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
69
|
-
const cutoff = new Date(todayUtcMidnight - retentionDays * MS_PER_DAY);
|
|
70
|
-
// `auditRetentionDays` is validated as a positive integer, not a *small* one: a window wide
|
|
71
|
-
// enough to run off the end of the `Date` range would make `toISOString` throw. That config
|
|
72
|
-
// means "keep everything", so answer with a stamp no real day file can sort before.
|
|
73
|
-
return Number.isNaN(cutoff.getTime()) ? "0000-00-00" : dayStamp(cutoff);
|
|
74
|
-
};
|
|
75
|
-
const auditFilePathForDate = (auditDir, date) => {
|
|
76
|
-
return join(auditDir, `${dayStamp(date)}.jsonl`);
|
|
77
|
-
};
|
|
78
|
-
const appendLine = async (filePath, line) => {
|
|
79
|
-
const handle = await open(filePath, "a", 0o600);
|
|
80
|
-
try {
|
|
81
|
-
await handle.appendFile(line);
|
|
82
|
-
// `open`'s mode only applies at creation and is further subject to umask; chmod explicitly so
|
|
83
|
-
// an existing (or umask-widened) file always ends up at the ARCHITECTURE.md §3 mode.
|
|
84
|
-
await chmod(filePath, 0o600);
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
await handle.close();
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
export const createAuditLogger = (options) => {
|
|
91
|
-
const clock = options.clock ?? { now: () => new Date() };
|
|
92
|
-
const warn = options.warn ??
|
|
93
|
-
((message) => {
|
|
94
|
-
process.stderr.write(`${message}\n`);
|
|
95
|
-
});
|
|
96
|
-
const retentionDays = options.retentionDays ?? DEFAULT_AUDIT_RETENTION_DAYS;
|
|
97
|
-
let failedWrites = 0;
|
|
98
|
-
let failedPrunes = 0;
|
|
99
|
-
let queue = Promise.resolve();
|
|
100
|
-
/**
|
|
101
|
-
* Lists the day files in the audit directory. A *missing* directory is genuinely empty — the
|
|
102
|
-
* daemon creates it lazily — and reports `failed: false`. Any other error (EACCES on a
|
|
103
|
-
* tightened-up directory, ENOTDIR where a file now sits, EIO on a failing disk) is a refusal to
|
|
104
|
-
* answer, not an answer of zero, and is reported as such: callers must not turn "we could not
|
|
105
|
-
* look" into "there is nothing there".
|
|
106
|
-
*/
|
|
107
|
-
const listDayFiles = async () => {
|
|
108
|
-
let entries;
|
|
109
|
-
try {
|
|
110
|
-
entries = await readdir(options.auditDir, { withFileTypes: true });
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
if (error.code === "ENOENT") {
|
|
114
|
-
return { files: [], failed: false };
|
|
115
|
-
}
|
|
116
|
-
return { files: [], failed: true, error: error };
|
|
117
|
-
}
|
|
118
|
-
const files = [];
|
|
119
|
-
for (const entry of entries) {
|
|
120
|
-
if (!entry.isFile()) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
const stamp = dayStampOfAuditFile(entry.name);
|
|
124
|
-
if (stamp !== undefined) {
|
|
125
|
-
files.push({ name: entry.name, stamp });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return { files, failed: false };
|
|
129
|
-
};
|
|
130
|
-
const pruneOnce = async () => {
|
|
131
|
-
const today = dayStamp(clock.now());
|
|
132
|
-
const cutoff = oldestRetainedStamp(clock.now(), retentionDays);
|
|
133
|
-
let deleted = 0;
|
|
134
|
-
let failed = 0;
|
|
135
|
-
const listing = await listDayFiles();
|
|
136
|
-
if (listing.failed) {
|
|
137
|
-
// A sweep that could not read the directory did not prune anything and cannot know whether
|
|
138
|
-
// it needed to — counted and warned like any other prune failure, so a permanently
|
|
139
|
-
// unreadable `audit/` shows up in `daemon.status` instead of looking like a tidy one.
|
|
140
|
-
failedPrunes += 1;
|
|
141
|
-
warn(`appduct daemon: failed to read the audit directory to prune it: ${listing.error?.message}`);
|
|
142
|
-
return { deleted: 0, failed: 1 };
|
|
143
|
-
}
|
|
144
|
-
for (const file of listing.files) {
|
|
145
|
-
// Belt and braces: `cutoff` is already strictly behind `today` for every valid
|
|
146
|
-
// `retentionDays`, but today's file is the one file that must never disappear from under an
|
|
147
|
-
// open append, so it is excluded explicitly (issue #32).
|
|
148
|
-
if (file.stamp === today || file.stamp >= cutoff) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
await rm(join(options.auditDir, file.name), { force: true });
|
|
153
|
-
deleted += 1;
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
failed += 1;
|
|
157
|
-
failedPrunes += 1;
|
|
158
|
-
warn(`appduct daemon: failed to prune the audit record "${file.name}": ${error.message}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return { deleted, failed };
|
|
162
|
-
};
|
|
163
|
-
const record = (entry) => {
|
|
164
|
-
const now = clock.now();
|
|
165
|
-
const full = { ts: now.toISOString(), ...entry };
|
|
166
|
-
const line = `${JSON.stringify(full)}\n`;
|
|
167
|
-
const filePath = auditFilePathForDate(options.auditDir, now);
|
|
168
|
-
// Chained onto the existing queue tail so writes for the same (or different) day never race
|
|
169
|
-
// each other; a failure is swallowed here (never rejects the chain) so one bad write can't
|
|
170
|
-
// poison every audit record after it.
|
|
171
|
-
queue = queue.then(async () => {
|
|
172
|
-
try {
|
|
173
|
-
await mkdir(options.auditDir, { recursive: true });
|
|
174
|
-
await appendLine(filePath, line);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
failedWrites += 1;
|
|
178
|
-
warn(`appduct daemon: failed to write an audit record: ${error.message}`);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* Chained onto the write queue for the same reason the writes are chained onto each other: a
|
|
184
|
-
* prune must never interleave with the append it would otherwise delete out from under. The
|
|
185
|
-
* returned promise resolves with this prune's own result while the queue itself stays a
|
|
186
|
-
* never-rejecting `Promise<void>` that `flush` can keep awaiting.
|
|
187
|
-
*/
|
|
188
|
-
const prune = () => {
|
|
189
|
-
let settle;
|
|
190
|
-
const result = new Promise((resolve) => {
|
|
191
|
-
settle = resolve;
|
|
192
|
-
});
|
|
193
|
-
queue = queue.then(async () => {
|
|
194
|
-
try {
|
|
195
|
-
settle(await pruneOnce());
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
// `pruneOnce` handles per-file failures itself; this only catches a failure of the listing
|
|
199
|
-
// step, which must still never reject the queue.
|
|
200
|
-
failedPrunes += 1;
|
|
201
|
-
warn(`appduct daemon: failed to prune the audit log: ${error.message}`);
|
|
202
|
-
settle({ deleted: 0, failed: 1 });
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
return result;
|
|
206
|
-
};
|
|
207
|
-
const stats = async () => {
|
|
208
|
-
const listing = await listDayFiles();
|
|
209
|
-
if (listing.failed) {
|
|
210
|
-
// Reported as "not measured", never as an empty directory: `daemon.status` would otherwise
|
|
211
|
-
// tell an operator that `audit/` holds nothing at exactly the moment it has become
|
|
212
|
-
// unreadable — the moment they most need to know otherwise.
|
|
213
|
-
return {};
|
|
214
|
-
}
|
|
215
|
-
let files = 0;
|
|
216
|
-
let bytes = 0;
|
|
217
|
-
for (const file of listing.files) {
|
|
218
|
-
try {
|
|
219
|
-
bytes += (await stat(join(options.auditDir, file.name))).size;
|
|
220
|
-
files += 1;
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
// Raced with a prune (ours or an operator's `rm`); an audit record that is gone by the
|
|
224
|
-
// time it is measured simply does not count toward the reported footprint.
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return { files, bytes };
|
|
228
|
-
};
|
|
229
|
-
return {
|
|
230
|
-
record,
|
|
231
|
-
failedWrites: () => failedWrites,
|
|
232
|
-
failedPrunes: () => failedPrunes,
|
|
233
|
-
prune,
|
|
234
|
-
stats,
|
|
235
|
-
flush: () => queue,
|
|
236
|
-
};
|
|
237
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"calls.d.ts","sourceRoot":"","sources":["../../src/daemon/calls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAgB,KAAK,QAAQ,EAAoB,MAAM,aAAa,CAAC;AAE5E;kFACkF;AAClF,eAAO,MAAM,uBAAuB,QAA0B,CAAC;AAC/D,eAAO,MAAM,mBAAmB,OAAsB,CAAC;AACvD,eAAO,MAAM,mBAAmB,SAAsB,CAAC;AAEvD;iFACiF;AACjF,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;oGAEoG;AACpG,MAAM,MAAM,YAAY,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC;AAEpF,4FAA4F;AAC5F,MAAM,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC;AAExF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,YAAY,CAAC;IACnB,UAAU,EAAE,cAAc,CAAC;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,CAAC,EAAE,QAAQ,CAAC;CACnB,CAAC;AAEF;;wEAEwE;AACxE,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB;;;;uCAImC;IACnC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,WAAW,CAAC;IAC7G,mFAAmF;IACnF,gBAAgB,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACvD,kFAAkF;IAClF,eAAe,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACrD;wCACoC;IACpC,sBAAsB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACnE;;;;sEAIkE;IAClE,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACvE,gGAAgG;IAChG,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,GAAG,iBAAiB,KAAK,IAAI,CAAC;IAC/F,oFAAoF;IACpF,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAWF;;4EAE4E;AAC5E,eAAO,MAAM,+BAA+B,OAAQ,CAAC;AAErD,8FAA8F;AAC9F,eAAO,MAAM,YAAY,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,MAM5D,CAAC;AAEF;;kFAEkF;AAClF,eAAO,MAAM,4BAA4B,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,MAE5E,CAAC;AAYF,eAAO,MAAM,kBAAkB,GAAI,SAAS,mBAAmB,KAAG,YAoJjE,CAAC"}
|
package/dist/daemon/calls.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-session tool-invocation manager (ARCHITECTURE.md §5 `tools.call`, §7 `tool_call` /
|
|
3
|
-
* `tool_result` / `tool_error` / `tool_call_progress`). This is the v2 replacement for v1's
|
|
4
|
-
* `pendingCalls` map: the correlation pattern (`call_<random>` ids, resolve on `tool_result`,
|
|
5
|
-
* reject on `tool_error`) was sound and is kept. v1's defect was error flattening — the app's
|
|
6
|
-
* `error.type` got re-wrapped twice and ended up buried two `details` levels deep. Here the type is
|
|
7
|
-
* preserved verbatim: {@link RpcApplicationError.type} is set directly from the app's
|
|
8
|
-
* `tool_error.error.type`, never a generic wrapper.
|
|
9
|
-
*
|
|
10
|
-
* This module knows nothing about sockets or the session state machine — `sessions.ts` (via the
|
|
11
|
-
* composition root in `daemon.ts`) supplies a `send` function and routes incoming
|
|
12
|
-
* `tool_result`/`tool_error`/`tool_call_progress` frames and suspend/revoke/expiry transitions back
|
|
13
|
-
* in. That keeps a single seam (`call`) that a policy layer can wrap.
|
|
14
|
-
*/
|
|
15
|
-
import { randomBytes } from "node:crypto";
|
|
16
|
-
import { clampToolTimeoutMs, DEFAULT_TOOL_TIMEOUT_MS, MAX_TOOL_TIMEOUT_MS, MIN_TOOL_TIMEOUT_MS, } from "@appduct/shared";
|
|
17
|
-
import { RpcApplicationError } from "./rpc-server.js";
|
|
18
|
-
import { systemTimers } from "./timers.js";
|
|
19
|
-
/** Re-exported under this module's historical names; the values live in `@appduct/shared` so the
|
|
20
|
-
* RN SDK's app-side abort timer and this timer cannot drift apart (issue #25). */
|
|
21
|
-
export const DEFAULT_CALL_TIMEOUT_MS = DEFAULT_TOOL_TIMEOUT_MS;
|
|
22
|
-
export const MIN_CALL_TIMEOUT_MS = MIN_TOOL_TIMEOUT_MS;
|
|
23
|
-
export const MAX_CALL_TIMEOUT_MS = MAX_TOOL_TIMEOUT_MS;
|
|
24
|
-
const generateCallId = () => `call_${randomBytes(16).toString("base64url")}`;
|
|
25
|
-
/** Extra headroom added on top of the daemon-side deadline so a caller's transport timeout never
|
|
26
|
-
* beats the daemon's own `tool_timeout` — mirrored (deliberately duplicated) in
|
|
27
|
-
* `client/app-client.ts`, which must not import daemon-internal modules. */
|
|
28
|
-
export const CALL_TRANSPORT_TIMEOUT_SLACK_MS = 5_000;
|
|
29
|
-
/** The deadline the daemon will actually enforce for a `tools.call` with this `timeoutMs`. */
|
|
30
|
-
export const clampTimeout = (timeoutMs) => {
|
|
31
|
-
if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
|
|
32
|
-
return DEFAULT_CALL_TIMEOUT_MS;
|
|
33
|
-
}
|
|
34
|
-
return clampToolTimeoutMs(timeoutMs);
|
|
35
|
-
};
|
|
36
|
-
/** Transport timeout a caller should use for a `tools.call` that will be given `timeoutMs`
|
|
37
|
-
* daemon-side. Always clamps first: adding slack to an unclamped value could overflow Node's
|
|
38
|
-
* 32-bit timer range (~24.8 days), which silently fires the timer immediately. */
|
|
39
|
-
export const deriveCallTransportTimeoutMs = (timeoutMs) => {
|
|
40
|
-
return clampTimeout(timeoutMs) + CALL_TRANSPORT_TIMEOUT_SLACK_MS;
|
|
41
|
-
};
|
|
42
|
-
/** Event kinds that terminate every pending call for a session (ARCHITECTURE.md §6/§5: "On
|
|
43
|
-
* suspend/revoke/expiry, every pending call rejects immediately with session_suspended (or
|
|
44
|
-
* unknown_session on revoke)"). Expiry only ever follows suspend (pending calls are already gone
|
|
45
|
-
* by then), but it is handled defensively with the same `unknown_session` type as revoke. */
|
|
46
|
-
const TERMINATING_KIND_ERROR_TYPES = {
|
|
47
|
-
session_suspended: "session_suspended",
|
|
48
|
-
session_revoked: "unknown_session",
|
|
49
|
-
session_expired: "unknown_session",
|
|
50
|
-
};
|
|
51
|
-
export const createCallsManager = (options) => {
|
|
52
|
-
const timers = options.timers ?? systemTimers;
|
|
53
|
-
// sessionId -> callId -> PendingCall. Concurrent calls to the same session get independent
|
|
54
|
-
// entries here, keyed by their own call id, so out-of-order tool_result/tool_error frames still
|
|
55
|
-
// resolve the right caller.
|
|
56
|
-
const pendingBySession = new Map();
|
|
57
|
-
const takePending = (sessionId, callId) => {
|
|
58
|
-
const sessionMap = pendingBySession.get(sessionId);
|
|
59
|
-
const pending = sessionMap?.get(callId);
|
|
60
|
-
if (!sessionMap || !pending) {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
sessionMap.delete(callId);
|
|
64
|
-
if (sessionMap.size === 0) {
|
|
65
|
-
pendingBySession.delete(sessionId);
|
|
66
|
-
}
|
|
67
|
-
timers.clearTimeout(pending.timer);
|
|
68
|
-
return pending;
|
|
69
|
-
};
|
|
70
|
-
const call = (session, name, args, timeoutMs) => {
|
|
71
|
-
const callId = generateCallId();
|
|
72
|
-
const result = new Promise((resolve, reject) => {
|
|
73
|
-
const effectiveTimeoutMs = clampTimeout(timeoutMs);
|
|
74
|
-
const timer = timers.setTimeout(() => {
|
|
75
|
-
const pending = takePending(session.sessionId, callId);
|
|
76
|
-
pending?.reject(new RpcApplicationError("tool_timeout", `Tool call "${name}" timed out after ${effectiveTimeoutMs}ms.`));
|
|
77
|
-
}, effectiveTimeoutMs);
|
|
78
|
-
let sessionMap = pendingBySession.get(session.sessionId);
|
|
79
|
-
if (!sessionMap) {
|
|
80
|
-
sessionMap = new Map();
|
|
81
|
-
pendingBySession.set(session.sessionId, sessionMap);
|
|
82
|
-
}
|
|
83
|
-
sessionMap.set(callId, { alias: session.alias, resolve, reject, timer });
|
|
84
|
-
const message = {
|
|
85
|
-
type: "tool_call",
|
|
86
|
-
session_id: session.sessionId,
|
|
87
|
-
id: callId,
|
|
88
|
-
name,
|
|
89
|
-
args,
|
|
90
|
-
};
|
|
91
|
-
const sent = options.send(session.sessionId, message);
|
|
92
|
-
if (!sent) {
|
|
93
|
-
const pending = takePending(session.sessionId, callId);
|
|
94
|
-
pending?.reject(new RpcApplicationError("session_not_active", "Session has no active socket."));
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
return { callId, result };
|
|
98
|
-
};
|
|
99
|
-
const handleToolResult = (message) => {
|
|
100
|
-
const pending = takePending(message.session_id, message.id);
|
|
101
|
-
pending?.resolve(message.result);
|
|
102
|
-
};
|
|
103
|
-
const handleToolError = (message) => {
|
|
104
|
-
const pending = takePending(message.session_id, message.id);
|
|
105
|
-
// Verbatim: `error.type` comes straight from the app's frame, never re-wrapped (v1's defect).
|
|
106
|
-
pending?.reject(new RpcApplicationError(message.error.type, message.error.message, -32000, message.error.details));
|
|
107
|
-
};
|
|
108
|
-
const handleToolCallProgress = (message) => {
|
|
109
|
-
const sessionMap = pendingBySession.get(message.session_id);
|
|
110
|
-
const pending = sessionMap?.get(message.id);
|
|
111
|
-
if (!pending) {
|
|
112
|
-
// Unknown correlation id: dropped (ARCHITECTURE.md task notes say "log at debug level"; a
|
|
113
|
-
// full logging seam doesn't exist yet in the daemon, so this is a silent, intentional no-op).
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
options.eventBus.emit({
|
|
117
|
-
// Its own EventKind (ARCHITECTURE.md §5), distinct from `tool_call_started`/
|
|
118
|
-
// `tool_call_finished` — subscribers correlate all three by `callId` in `data` without
|
|
119
|
-
// needing to sniff which kind carries a `progress`/`message` field.
|
|
120
|
-
kind: "tool_call_progress",
|
|
121
|
-
sessionId: message.session_id,
|
|
122
|
-
alias: pending.alias,
|
|
123
|
-
data: { callId: message.id, progress: message.progress, message: message.message },
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
const cancel = (sessionId, callId, reason) => {
|
|
127
|
-
const pending = pendingBySession.get(sessionId)?.get(callId);
|
|
128
|
-
if (!pending) {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
const message = { type: "tool_cancel", session_id: sessionId, id: callId, reason };
|
|
132
|
-
return options.sendCancel(sessionId, message);
|
|
133
|
-
};
|
|
134
|
-
const rejectSession = (sessionId, errorType) => {
|
|
135
|
-
const sessionMap = pendingBySession.get(sessionId);
|
|
136
|
-
if (!sessionMap) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
pendingBySession.delete(sessionId);
|
|
140
|
-
for (const pending of sessionMap.values()) {
|
|
141
|
-
timers.clearTimeout(pending.timer);
|
|
142
|
-
pending.reject(new RpcApplicationError(errorType, `Session transitioned while the call was pending.`));
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
const unsubscribeFromEventBus = options.eventBus.subscribe((event) => {
|
|
146
|
-
const errorType = TERMINATING_KIND_ERROR_TYPES[event.kind];
|
|
147
|
-
if (errorType && event.sessionId) {
|
|
148
|
-
rejectSession(event.sessionId, errorType);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
const disposeAll = () => {
|
|
152
|
-
unsubscribeFromEventBus();
|
|
153
|
-
for (const sessionId of Array.from(pendingBySession.keys())) {
|
|
154
|
-
rejectSession(sessionId, "unknown_session");
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
return { call, handleToolResult, handleToolError, handleToolCallProgress, cancel, rejectSession, disposeAll };
|
|
158
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/daemon/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,wBAAwB,EAAE,MAAM,CAAC;IACjC;yCACqC;IACrC,eAAe,EAAE,MAAM,CAAC;IACxB;kFAC8E;IAC9E,kBAAkB,EAAE,MAAM,CAAC;IAC3B;kDAC8C;IAC9C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,mBAAmB,CAAC;IAC5B;;;;;OAKG;IACH,8BAA8B,EAAE,OAAO,CAAC;IACxC,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAsBF,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,4BAA4B,QAAmB,CAAC;AAE7D,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM;CAKlB;AAsCD,eAAO,MAAM,aAAa,GAAI,OAAO,aAAa,KAAG,aAgBpD,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAErD,kGAAkG;AAClG,eAAO,MAAM,UAAU,GACrB,OAAO,aAAa,EACpB,UAAS;IAAE,IAAI,CAAC,EAAE,YAAY,CAAA;CAAO,KACpC,OAAO,CAAC,aAAa,CA4HvB,CAAC"}
|