agents-can-communicate 0.7.1 → 0.8.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.
Files changed (105) hide show
  1. package/README.md +3 -1
  2. package/bin/entrypoints/acc-bootstrap.mjs +6 -48
  3. package/bin/entrypoints/acc-claude-channel.mjs +47 -157
  4. package/bin/entrypoints/acc-mcp.mjs +0 -1
  5. package/bin/entrypoints/acc.mjs +0 -1
  6. package/docs/ADAPTER_AUTHORING.md +74 -24
  7. package/docs/ARCHITECTURE.md +31 -18
  8. package/docs/CAPABILITIES.md +38 -25
  9. package/docs/CLI.md +51 -13
  10. package/docs/CONCEPTS.md +5 -4
  11. package/docs/CONFIGURATION.md +18 -11
  12. package/docs/GETTING_STARTED.md +15 -12
  13. package/docs/GLOSSARY.md +2 -1
  14. package/docs/HOW_IT_WORKS.md +8 -4
  15. package/docs/PROTOCOL.md +25 -6
  16. package/docs/RELEASING.md +8 -5
  17. package/docs/SECURITY_MODEL.md +14 -1
  18. package/docs/TROUBLESHOOTING.md +66 -29
  19. package/docs/UPGRADING.md +49 -8
  20. package/docs/WHY_ACC.md +5 -3
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +2 -3
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +26 -30
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +249 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
  38. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -4
  40. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
  41. package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +5 -5
  42. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
  44. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
  45. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
  46. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -0
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  50. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  51. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +2 -2
  54. package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
  55. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
  56. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
  57. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
  58. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +104 -84
  59. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +8 -4
  60. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  61. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
  62. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
  63. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
  64. package/node_modules/@agents-can-communicate/cli/src/main.mjs +24 -3
  65. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
  66. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
  67. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
  68. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
  69. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
  70. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
  71. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  72. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +17 -2
  73. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
  74. package/node_modules/@agents-can-communicate/core/src/status.mjs +31 -1
  75. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  76. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
  77. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +33 -48
  78. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  79. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
  80. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
  81. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +27 -15
  82. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
  83. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +4 -2
  84. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  85. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
  86. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
  87. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +6 -11
  88. package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
  89. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
  90. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
  91. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
  92. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
  93. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  95. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  96. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
  97. package/package.json +1 -1
  98. package/bin/acc-bootstrap.mjs +0 -7
  99. package/bin/acc-claude-channel.mjs +0 -10
  100. package/bin/entrypoints/claude-channel-binding.mjs +0 -46
  101. package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
  102. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
  103. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
  104. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
  105. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
@@ -1,232 +0,0 @@
1
- import { readFile, readdir, stat } from "node:fs/promises";
2
- import net from "node:net";
3
- import path from "node:path";
4
- import { decisionBody } from "@agents-can-communicate/adapter-sdk";
5
-
6
- import { PROTOCOL_CONTRACT, endpointDir, isSocketSafe, readRegistration, routeAck, routeReply }
7
- from "./channel.mjs";
8
-
9
- // The five native-delivery adapter methods for Claude Code. They keep every
10
- // Claude flag, protocol name, and endpoint detail inside this package and hand
11
- // core only opaque facts: a probe verdict, an activation plan, a handshake with
12
- // an opaque endpoint id, and an offer result. offerMessage is the sender-side
13
- // half - it connects to the receiver's session endpoint and delivers one
14
- // envelope; the Channel process itself turns acc_reply into a real ACC answer.
15
-
16
- const MIN_VERSION = "2.1.258";
17
- const CHANNEL_PLUGIN = "plugin:agents-can-communicate@acc-local";
18
- const DEV_CHANNEL_FLAG = "--dangerously-load-development-channels";
19
- // The bytes the installed 2.1.258 Mach-O carries for the Channel MCP protocol;
20
- // a build without Channels has neither. Read-only, bounded.
21
- const PROBE_NEEDLE = Buffer.from("notifications/claude/channel");
22
- const PROBE_MAX_BYTES = 256 * 1024 * 1024;
23
- const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
24
-
25
- function compare(left, right) {
26
- const a = left.split("+")[0].split(".").map(Number);
27
- const b = right.split("+")[0].split(".").map(Number);
28
- for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
29
- return 0;
30
- }
31
-
32
- async function executableHasChannel(realExecutable) {
33
- let handle;
34
- try {
35
- handle = await (await import("node:fs/promises")).open(realExecutable, "r");
36
- } catch {
37
- return false;
38
- }
39
- try {
40
- const chunk = Buffer.alloc(1024 * 1024);
41
- let carry = Buffer.alloc(0);
42
- let read = 0;
43
- let position = 0;
44
- do {
45
- ({ bytesRead: read } = await handle.read(chunk, 0, chunk.length, position));
46
- if (read === 0) break;
47
- const window = Buffer.concat([carry, chunk.subarray(0, read)]);
48
- if (window.includes(PROBE_NEEDLE)) return true;
49
- carry = window.subarray(Math.max(0, window.length - PROBE_NEEDLE.length));
50
- position += read;
51
- } while (position < PROBE_MAX_BYTES);
52
- return false;
53
- } finally {
54
- await handle.close().catch(() => null);
55
- }
56
- }
57
-
58
- // Read-only: a stable version at or above the minimum whose executable carries
59
- // the Channel protocol. Never launches the client.
60
- export async function probeNativeDelivery({ realExecutable, timeoutMs = 750,
61
- hasChannel = executableHasChannel, readVersion = defaultReadVersion } = {}) {
62
- const unsupported = reasonCode => ({ supported: false, clientVersion: null,
63
- protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
64
- if (typeof realExecutable !== "string" || realExecutable === "") return unsupported("feature_probe_failed");
65
- const clientVersion = await withTimeout(readVersion(realExecutable, timeoutMs), timeoutMs)
66
- .catch(() => null);
67
- if (clientVersion === null || !STABLE_VERSION.test(clientVersion)) {
68
- return { ...unsupported("feature_probe_failed"), clientVersion };
69
- }
70
- if (compare(clientVersion, MIN_VERSION) < 0) {
71
- return { ...unsupported("below_minimum_version"), clientVersion };
72
- }
73
- const present = await withTimeout(Promise.resolve(hasChannel(realExecutable)), timeoutMs)
74
- .catch(() => false);
75
- if (!present) return { ...unsupported("protocol_mismatch"), clientVersion };
76
- return { supported: true, clientVersion, protocolContract: PROTOCOL_CONTRACT,
77
- executableFingerprint: null, modes: ["livePush", "idleWake", "busyQueue", "replyRoute"],
78
- reasonCode: null };
79
- }
80
-
81
- function defaultReadVersion(realExecutable, timeoutMs) {
82
- return new Promise(resolve => {
83
- import("node:child_process").then(({ execFile }) => {
84
- execFile(realExecutable, ["--version"], { timeout: timeoutMs, windowsHide: true },
85
- (error, stdout, stderr) => {
86
- if (error !== null) return resolve(null);
87
- resolve(/(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/.exec(`${stdout}${stderr}`)?.[1] ?? null);
88
- });
89
- });
90
- });
91
- }
92
-
93
- function withTimeout(work, ms) {
94
- let timer = null;
95
- const deadline = new Promise((_resolve, reject) => {
96
- timer = setTimeout(() => reject(Object.assign(new Error("native probe timed out"),
97
- { code: "ETIMEDOUT" })), ms);
98
- });
99
- return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
100
- }
101
-
102
- // The shell bootstrap adds only the captured Channel flag; the .mcp.json the
103
- // adapter's own install writes is the native-config artifact. ACC never
104
- // suppresses Claude's experimental warning.
105
- export function planNativeActivation({ detection, livePolicy }) {
106
- if (detection?.realExecutable === undefined || detection.realExecutable === null) {
107
- return { eligible: false, reasonCode: "feature_probe_failed", mechanisms: [] };
108
- }
109
- void livePolicy;
110
- return { eligible: true, reasonCode: null, mechanisms: [
111
- { kind: "native-config", artifactIds: ["claude-channel-mcp"] },
112
- { kind: "shell-bootstrap", command: "claude", realExecutable: detection.realExecutable,
113
- prefixArgs: [DEV_CHANNEL_FLAG, CHANNEL_PLUGIN] },
114
- ] };
115
- }
116
-
117
- async function currentRegistrations(runtimeDir) {
118
- const dir = endpointDir(runtimeDir);
119
- let names;
120
- try { names = await readdir(dir); } catch { return []; }
121
- const registrations = [];
122
- for (const name of names) {
123
- if (!name.endsWith(".json")) continue;
124
- try {
125
- const record = readRegistration(await readFile(path.join(dir, name), "utf8"));
126
- registrations.push(record);
127
- } catch { /* skip a malformed neighbour */ }
128
- }
129
- return registrations;
130
- }
131
-
132
- // Bind the exact Channel the hook-resolved Claude process owns: match clientPid,
133
- // verify the protocol, and return an opaque endpoint id. Never selects the first
134
- // registration; two Claude sessions cannot receive each other's endpoint.
135
- export async function bindNativeSession({ clientPid, clientVersion, runtimeDir, timeoutMs = 750,
136
- intervalMs = 50, now = () => Date.now(),
137
- sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) }) {
138
- const closed = reasonCode => ({ supported: false, clientVersion: clientVersion ?? null,
139
- protocolContract: PROTOCOL_CONTRACT, modes: [], opaqueEndpointRef: null, leaseUntil: null,
140
- reasonCode });
141
- if (!Number.isInteger(clientPid) || clientPid <= 0) return closed("handshake_failed");
142
- // Both sides of this handshake are started by SessionStart: the hook writes
143
- // the session binding and then calls here, while the Channel is still waiting
144
- // for that same binding before it can listen and register. So the endpoint
145
- // reliably appears after this begins, and a single read gave up on a channel
146
- // that was moments from ready - measured, a healthy endpoint landed on disk
147
- // just after the hook had already recorded no binding at all.
148
- //
149
- // The budget was always passed in for this. It stops one interval short of
150
- // the hook's own timer, which races the same budget: an answer that arrives
151
- // together with that timer is a coin toss the session loses.
152
- const deadline = now() + Math.max(0, timeoutMs - intervalMs);
153
- for (;;) {
154
- const matches = (await currentRegistrations(runtimeDir))
155
- .filter(record => record.clientPid === clientPid
156
- && record.protocolContract === PROTOCOL_CONTRACT
157
- && Date.parse(record.leaseUntil) > Date.now());
158
- // Exactly one, still: waiting must not relax the rule that two Claude
159
- // sessions can never be handed each other's endpoint.
160
- if (matches.length === 1 && isSocketSafe(matches[0].socketPath)) {
161
- const [record] = matches;
162
- return { supported: true, clientVersion: clientVersion ?? null,
163
- protocolContract: PROTOCOL_CONTRACT, modes: [...record.modes],
164
- opaqueEndpointRef: record.endpointId, leaseUntil: record.leaseUntil, reasonCode: null };
165
- }
166
- if (now() >= deadline) return closed("handshake_failed");
167
- await sleep(intervalMs);
168
- }
169
- }
170
-
171
- // Sender side: resolve the endpoint id to its registration under this
172
- // workspace's runtime dir, connect to the session-scoped socket, and deliver one
173
- // envelope. Returns only accepted/duplicate plus the bound client version.
174
- export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 2_000,
175
- connect = net.createConnection }) {
176
- const rejected = safeErrorCode => ({ accepted: false, transport: "claude-channel",
177
- clientVersion: binding?.clientVersion ?? null, safeErrorCode });
178
- if (typeof runtimeDir !== "string" || binding?.opaqueEndpointRef === undefined) {
179
- return rejected("recipient_unavailable");
180
- }
181
- const file = path.join(endpointDir(runtimeDir), `${binding.opaqueEndpointRef}.json`);
182
- if (path.dirname(file) !== endpointDir(runtimeDir)) return rejected("recipient_unavailable");
183
- let record;
184
- try {
185
- record = readRegistration(await readFile(file, "utf8"));
186
- } catch {
187
- return rejected("recipient_unavailable");
188
- }
189
- if (record.endpointId !== binding.opaqueEndpointRef
190
- || record.protocolContract !== PROTOCOL_CONTRACT
191
- || Date.parse(record.leaseUntil) <= Date.now()
192
- || !(await stat(record.socketPath).then(s => s.isSocket(), () => false))
193
- || !isSocketSafe(record.socketPath)) {
194
- return rejected("recipient_unavailable");
195
- }
196
- return sendEnvelope({ record, message, binding, connect, timeoutMs, rejected });
197
- }
198
-
199
- function sendEnvelope({ record, message, binding, connect, timeoutMs, rejected }) {
200
- const envelope = { nonce: record.nonce, messageId: message.messageId, kind: message.kind,
201
- subject: message.subject ?? "", body: decisionBody(message),
202
- ...(typeof message.inReplyTo === "string" ? { inReplyTo: message.inReplyTo } : {}) };
203
- return new Promise(resolve => {
204
- const socket = connect(record.socketPath);
205
- let settled = false;
206
- const finish = value => { if (settled) return; settled = true; clearTimeout(timer);
207
- socket.destroy(); resolve(value); };
208
- const timer = setTimeout(() => finish(rejected("transport_error")), timeoutMs);
209
- socket.once("error", error => finish(rejected(
210
- ["EPERM", "EACCES"].includes(error.code)
211
- ? "transport_permission_denied" : "recipient_unavailable")));
212
- socket.once("connect", () => {
213
- socket.write(`${JSON.stringify(envelope)}\n`);
214
- let buffer = "";
215
- socket.on("data", chunk => {
216
- buffer += chunk.toString("utf8");
217
- const newline = buffer.indexOf("\n");
218
- if (newline === -1) return;
219
- let response;
220
- try { response = JSON.parse(buffer.slice(0, newline)); }
221
- catch { return finish(rejected("transport_rejected")); }
222
- if (response.accepted === true) {
223
- return finish({ accepted: true, transport: "claude-channel",
224
- clientVersion: binding.clientVersion });
225
- }
226
- finish(rejected("transport_rejected"));
227
- });
228
- });
229
- });
230
- }
231
-
232
- export { MIN_VERSION, endpointDir, routeAck, routeReply };
@@ -1,150 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { createReadStream } from "node:fs";
4
- import { mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
5
- import path from "node:path";
6
-
7
- import { evaluateNativeEligibility, ownVersion } from "@agents-can-communicate/adapter-sdk";
8
-
9
- // The launch-time check behind an owned shell shim. It answers one closed
10
- // question - may this exact executable receive native delivery? - from the
11
- // static minimum, a read-only adapter probe, and a keyed cache under ACC's own
12
- // data home. It never throws: a shim that cannot decide launches the vendor
13
- // command untouched, and this module is what makes "cannot decide" cheap.
14
- //
15
- // The cache key is the executable's identity - resolved path, symlink target,
16
- // inode, size, mtime, and full sha256 - plus platform and this ACC runtime's
17
- // version. A vendor upgrade, replacement, or changed certification contract is
18
- // therefore a miss. A cached failure is short-lived; a repaired client is
19
- // never disabled for long.
20
-
21
- export const BOOTSTRAP_CACHE_SCHEMA = 1;
22
- export const SUPPORTED_TTL_MS = 6 * 60 * 60 * 1_000;
23
- export const FAILED_TTL_MS = 5 * 60 * 1_000;
24
- const DEFAULT_TIMEOUT_MS = 750;
25
- const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b|$)/;
26
-
27
- export const cachePathFor = (dataHome, adapterId) =>
28
- path.join(dataHome, "acc", "native-bootstrap", `${adapterId}.json`);
29
-
30
- function sha256File(file) {
31
- return new Promise((resolve, reject) => {
32
- const hash = createHash("sha256");
33
- createReadStream(file).on("data", chunk => hash.update(chunk))
34
- .on("error", reject).on("end", () => resolve(`sha256:${hash.digest("hex")}`));
35
- });
36
- }
37
-
38
- async function executableIdentity(realExecutable) {
39
- const target = await realpath(realExecutable);
40
- const facts = await stat(target);
41
- if (!facts.isFile()) throw new Error("not a file");
42
- return { path: realExecutable, target, inode: facts.ino, size: facts.size,
43
- mtimeMs: Math.floor(facts.mtimeMs), executableFingerprint: await sha256File(target) };
44
- }
45
-
46
- function withTimeout(work, ms, label) {
47
- let timer = null;
48
- const deadline = new Promise((_resolve, reject) => {
49
- timer = setTimeout(() => reject(Object.assign(new Error(`${label} timed out`),
50
- { code: "ETIMEDOUT" })), ms);
51
- });
52
- return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
53
- }
54
-
55
- // Read-only: the client's own version command, nothing else.
56
- export function defaultReadVersion({ realExecutable, versionArgs, timeoutMs }) {
57
- return new Promise(resolve => {
58
- execFile(realExecutable, versionArgs, { timeout: timeoutMs, windowsHide: true },
59
- (error, stdout, stderr) => {
60
- if (error !== null) return resolve(null);
61
- resolve(VERSION.exec(`${stdout}${stderr}`)?.[1] ?? null);
62
- });
63
- });
64
- }
65
-
66
- const sameIdentity = (left, right) => left !== undefined && right !== undefined
67
- && ["path", "target", "inode", "size", "mtimeMs", "executableFingerprint"]
68
- .every(key => left[key] === right[key]);
69
-
70
- async function loadCache(file) {
71
- try {
72
- const record = JSON.parse(await readFile(file, "utf8"));
73
- return record?.schemaVersion === BOOTSTRAP_CACHE_SCHEMA ? record : null;
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- async function saveCache(file, record) {
80
- await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
81
- const temporary = `${file}.${process.pid}.tmp`;
82
- await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
83
- await rename(temporary, file);
84
- }
85
-
86
- const closedOutcome = (supported, reasonCode) => Object.freeze({ supported, reasonCode });
87
-
88
- export async function checkNativeBootstrap({ adapter, realExecutable, platform, dataHome,
89
- timeoutMs = DEFAULT_TIMEOUT_MS, clock = { now: () => new Date().toISOString() },
90
- readVersion = defaultReadVersion, accVersion }) {
91
- try {
92
- if (adapter?.nativeDelivery === undefined || typeof adapter.probeNativeDelivery !== "function") {
93
- return closedOutcome(false, "native_delivery_unsupported");
94
- }
95
- const identity = await executableIdentity(realExecutable);
96
- const file = cachePathFor(dataHome, adapter.id);
97
- const now = Date.parse(clock.now());
98
- const runtimeVersion = accVersion === undefined
99
- ? await ownVersion(import.meta.url) : accVersion;
100
- const runtimeVersionKnown = typeof runtimeVersion === "string" && runtimeVersion !== "";
101
- const cached = await loadCache(file);
102
- if (cached !== null && cached.adapterId === adapter.id && cached.platform === platform
103
- && runtimeVersionKnown && cached.accVersion === runtimeVersion
104
- && sameIdentity(cached.identity, identity) && Date.parse(cached.expiresAt) > now) {
105
- return closedOutcome(cached.supported, cached.reasonCode);
106
- }
107
-
108
- const clientVersion = await withTimeout(readVersion({ realExecutable: identity.target,
109
- versionArgs: adapter.client?.versionArgs ?? ["--version"], timeoutMs }), timeoutMs,
110
- "version probe").catch(() => null);
111
- let probe = null;
112
- let reasonCode = null;
113
- if (clientVersion !== null) {
114
- try {
115
- probe = await withTimeout(adapter.probeNativeDelivery({ realExecutable: identity.target,
116
- timeoutMs }), timeoutMs, "native probe");
117
- } catch (error) {
118
- probe = null;
119
- reasonCode = error?.code === "ETIMEDOUT" ? "probe_timeout" : "feature_probe_failed";
120
- }
121
- }
122
- let eligibility;
123
- try {
124
- eligibility = evaluateNativeEligibility(adapter, { clientVersion, platform, probe });
125
- } catch {
126
- // A malformed probe is an adapter bug, and still not a reason to change
127
- // the user's launch: it fails closed like any other probe failure.
128
- eligibility = { eligible: false, reasonCode: "feature_probe_failed" };
129
- }
130
- const supported = eligibility.eligible === true;
131
- // A probe that timed out or threw is reported as such rather than as the
132
- // generic verdict the missing probe would otherwise produce.
133
- const outcomeReason = supported ? null : (reasonCode ?? eligibility.reasonCode
134
- ?? "feature_probe_failed");
135
- // Only closed facts are stored: identity, versions, the eligibility verdict.
136
- // Never command output, never a message body.
137
- await saveCache(file, {
138
- schemaVersion: BOOTSTRAP_CACHE_SCHEMA, adapterId: adapter.id, platform, identity,
139
- accVersion: runtimeVersionKnown ? runtimeVersion : null,
140
- clientVersion, supported, reasonCode: outcomeReason,
141
- protocolContract: supported ? eligibility.protocolContract : null,
142
- modes: supported ? [...eligibility.modes] : [],
143
- checkedAt: new Date(now).toISOString(),
144
- expiresAt: new Date(now + (supported ? SUPPORTED_TTL_MS : FAILED_TTL_MS)).toISOString(),
145
- }).catch(() => null);
146
- return closedOutcome(supported, outcomeReason);
147
- } catch {
148
- return closedOutcome(false, "feature_probe_failed");
149
- }
150
- }