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.
- package/README.md +3 -1
- package/bin/entrypoints/acc-bootstrap.mjs +6 -48
- package/bin/entrypoints/acc-claude-channel.mjs +47 -157
- package/bin/entrypoints/acc-mcp.mjs +0 -1
- package/bin/entrypoints/acc.mjs +0 -1
- package/docs/ADAPTER_AUTHORING.md +74 -24
- package/docs/ARCHITECTURE.md +31 -18
- package/docs/CAPABILITIES.md +38 -25
- package/docs/CLI.md +51 -13
- package/docs/CONCEPTS.md +5 -4
- package/docs/CONFIGURATION.md +18 -11
- package/docs/GETTING_STARTED.md +15 -12
- package/docs/GLOSSARY.md +2 -1
- package/docs/HOW_IT_WORKS.md +8 -4
- package/docs/PROTOCOL.md +25 -6
- package/docs/RELEASING.md +8 -5
- package/docs/SECURITY_MODEL.md +14 -1
- package/docs/TROUBLESHOOTING.md +66 -29
- package/docs/UPGRADING.md +49 -8
- package/docs/WHY_ACC.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +2 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +26 -30
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +249 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +5 -5
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +104 -84
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +8 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +24 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +17 -2
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +31 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +33 -48
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +27 -15
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +4 -2
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +6 -11
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
- package/package.json +1 -1
- package/bin/acc-bootstrap.mjs +0 -7
- package/bin/acc-claude-channel.mjs +0 -10
- package/bin/entrypoints/claude-channel-binding.mjs +0 -46
- package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
- 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
|
-
}
|