agents-can-communicate 0.8.0 → 0.8.2
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/bin/entrypoints/acc-mcp.mjs +0 -1
- package/bin/entrypoints/acc.mjs +0 -1
- package/docs/ADAPTER_AUTHORING.md +50 -17
- package/docs/ARCHITECTURE.md +9 -6
- package/docs/CAPABILITIES.md +24 -14
- package/docs/CLI.md +22 -3
- package/docs/CONFIGURATION.md +73 -8
- package/docs/GETTING_STARTED.md +6 -3
- package/docs/HOW_IT_WORKS.md +3 -1
- package/docs/PROTOCOL.md +2 -2
- package/docs/RELEASING.md +6 -4
- package/docs/SECURITY_MODEL.md +12 -1
- package/docs/TROUBLESHOOTING.md +34 -7
- package/docs/UPGRADING.md +29 -1
- 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/plugin/skills/acc/SKILL.md +7 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +5 -4
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +204 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +45 -10
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +14 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +12 -17
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +5 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-observed-directories.mjs +62 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +41 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +6 -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/install.mjs +5 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +69 -17
- 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/capabilities.mjs +7 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +16 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +85 -77
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +4 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +9 -4
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +11 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +49 -12
- 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/native-session-diagnostics.mjs +20 -10
- 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/status.mjs +26 -2
- 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 +9 -41
- 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-binding.mjs +3 -4
- 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 +12 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -7
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/installer/src/receiver-sockets.mjs +19 -0
- 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 +30 -4
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +56 -25
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +6 -2
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { lstat, realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { socketIsReady } from "./native-endpoint.mjs";
|
|
3
|
+
import { readySocketPath, socketIsReady } from "./native-endpoint.mjs";
|
|
4
4
|
import { probeNativeDelivery } from "./native-delivery.mjs";
|
|
5
5
|
import { installCodexStandalone } from "./standalone-install.mjs";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { refusesConnections } from "./ws-json-rpc.mjs";
|
|
7
|
+
import { approvedProcessIsDead, failMaintenance, maintenanceContext, probeMaintenanceCli,
|
|
8
|
+
probeMaintenanceInstall, readMaintenancePid, runMaintenanceCommand, verifyMaintenanceProcess }
|
|
9
|
+
from "./maintenance-host.mjs";
|
|
8
10
|
|
|
9
11
|
const keys = ["home", "codexHome", "cliPath", "cliVersion", "managedPath", "managedVersion",
|
|
10
12
|
"managedRealPath", "cliIdentity", "managedIdentity", "socketPath", "pidPath", "platform"];
|
|
@@ -14,6 +16,7 @@ const own = info => typeof process.getuid !== "function" || info.uid === process
|
|
|
14
16
|
const sessionNeeded = "Codex service ready; open a new Codex session to establish its delivery binding and review client hooks and permissions";
|
|
15
17
|
const diagnostics = {
|
|
16
18
|
native_endpoint_unavailable: "Prepare the missing Codex service with codex app-server daemon start, then open a new Codex session",
|
|
19
|
+
service_stopped: "The Codex service stopped and left its files behind; acc install starts it in place with codex app-server daemon start, which replaces them, then open a new Codex session",
|
|
17
20
|
managed_install_missing: "Codex service needs its standalone package; run acc install to download the matching official Codex release and prepare the service",
|
|
18
21
|
managed_install_incomplete: "Codex standalone installation is incomplete; repair it with the official Codex installer, then retry acc install",
|
|
19
22
|
prerequisite_consent_required: "Codex standalone download was not approved. Run acc install --adapter codex --delivery actionable to allow the download and automatic peer requests",
|
|
@@ -21,7 +24,6 @@ const diagnostics = {
|
|
|
21
24
|
prerequisite_integrity_failed: "The Codex installer checksum did not match the reviewed version; no installer was run",
|
|
22
25
|
prerequisite_install_failed: "The official Codex installation did not complete; check connectivity and the selected Codex home, then retry acc install",
|
|
23
26
|
managed_binary_mismatch: "Codex CLI and managed standalone versions differ; repair the vendor installation, then retry acc install",
|
|
24
|
-
maintenance_platform_unsupported: "Codex service preparation is captured only on darwin-arm64; configure the vendor service manually",
|
|
25
27
|
maintenance_cli_unsupported: "Codex cold service preparation requires codex-cli 0.154.0 or newer with daemon commands; update the vendor installation, then retry acc install",
|
|
26
28
|
};
|
|
27
29
|
function report(state, reasonCode, facts = {}) {
|
|
@@ -38,6 +40,20 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
38
40
|
try { return await fs.lstat(file); }
|
|
39
41
|
catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
40
42
|
}
|
|
43
|
+
// A service that stopped without cleaning up: its pid file names a process
|
|
44
|
+
// that is gone, and its socket refuses every connection; either may be gone
|
|
45
|
+
// already. Prepared like an absent one: Codex's own start replaces both
|
|
46
|
+
// files (measured on 0.155.1), and every identity check follows it.
|
|
47
|
+
async function serviceStopped(paths) {
|
|
48
|
+
if (await info(paths.pidPath)) {
|
|
49
|
+
const recorded = await readMaintenancePid(paths.pidPath).catch(() => null);
|
|
50
|
+
if (recorded === null || !await approvedProcessIsDead(recorded, paths, run)) return false;
|
|
51
|
+
}
|
|
52
|
+
// Absent, or a symlink whose socket is gone (what 0.157.1 leaves after a
|
|
53
|
+
// reboot cleared /private/tmp), or a socket nothing listens on.
|
|
54
|
+
const target = await fs.realpath(paths.socketPath).catch(() => null);
|
|
55
|
+
return target === null || (await socketIsReady(target) && await refusesConnections(target));
|
|
56
|
+
}
|
|
41
57
|
async function safeDirectories(paths) {
|
|
42
58
|
for (const dir of [paths.codexHome, path.dirname(paths.socketPath), path.dirname(paths.pidPath)]) {
|
|
43
59
|
const stat = await info(dir);
|
|
@@ -79,7 +95,7 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
79
95
|
socketPath: paths.socketPath, pidPath: paths.pidPath, platform: paths.platform, ...installation });
|
|
80
96
|
}
|
|
81
97
|
async function verify(paths, facts) {
|
|
82
|
-
if (!await
|
|
98
|
+
if (!await readySocketPath(paths.socketPath)) failMaintenance("daemon_socket_unproven");
|
|
83
99
|
const version = await run(facts.cliPath, ["app-server", "daemon", "version"], paths.options);
|
|
84
100
|
if (version.status !== 0) failMaintenance("daemon_version_unavailable");
|
|
85
101
|
const observed = JSON.parse(version.stdout);
|
|
@@ -103,7 +119,6 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
103
119
|
async function inspect(context, { strict = false } = {}) {
|
|
104
120
|
try {
|
|
105
121
|
const paths = await contextPaths(context);
|
|
106
|
-
if (paths.platform !== "darwin-arm64") failMaintenance("maintenance_platform_unsupported");
|
|
107
122
|
if (!strict) {
|
|
108
123
|
const native = await probe({ env: paths.options.env, timeoutMs: 1_500 });
|
|
109
124
|
// Preserve the existing delivery support matrix: a healthy older
|
|
@@ -116,6 +131,7 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
116
131
|
if (!await info(paths.socketPath) && !await info(paths.pidPath)) {
|
|
117
132
|
return report("needed", "native_endpoint_unavailable", facts);
|
|
118
133
|
}
|
|
134
|
+
if (await serviceStopped(paths)) return report("needed", "service_stopped", facts);
|
|
119
135
|
return await verify(paths, facts);
|
|
120
136
|
} catch (error) {
|
|
121
137
|
const reason = error.reasonCode ?? "service_inspection_failed";
|
|
@@ -166,9 +182,10 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
166
182
|
if (current.state !== "needed") return { ...current, state: "blocked", started };
|
|
167
183
|
const paths = await contextPaths(context);
|
|
168
184
|
if (!same(plan, await installed(paths))) failMaintenance("service_identity_changed");
|
|
169
|
-
// Last check immediately before the only mutating command.
|
|
170
|
-
//
|
|
171
|
-
|
|
185
|
+
// Last check immediately before the only mutating command. ACC deletes no
|
|
186
|
+
// metadata and never stops or restarts a process on this path; a service
|
|
187
|
+
// whose files are all that is left of it is started over them.
|
|
188
|
+
if ((await info(paths.socketPath) || await info(paths.pidPath)) && !await serviceStopped(paths)) {
|
|
172
189
|
return { ...await verify(paths, current), started };
|
|
173
190
|
}
|
|
174
191
|
started = true;
|
|
@@ -63,6 +63,18 @@ export function acceptKey(key) {
|
|
|
63
63
|
return createHash("sha1").update(`${key}${GUID}`).digest("base64");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** True only when a connect to the socket is refused: a file with nobody
|
|
67
|
+
* listening. Accepting, timing out, or any other error is not proof of that. */
|
|
68
|
+
export function refusesConnections(socketPath, { timeoutMs = 1_000 } = {}) {
|
|
69
|
+
return new Promise(resolve => {
|
|
70
|
+
const socket = net.createConnection({ path: socketPath });
|
|
71
|
+
const settle = refused => { clearTimeout(timer); socket.destroy(); resolve(refused); };
|
|
72
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
73
|
+
socket.once("connect", () => settle(false));
|
|
74
|
+
socket.once("error", error => settle(error?.code === "ECONNREFUSED"));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
export function openWebSocketPeer({ socketPath, timeoutMs, path = "/", host = "localhost",
|
|
67
79
|
retainNotifications = true }) {
|
|
68
80
|
const notifications = [];
|
|
@@ -129,6 +129,13 @@ export function defineAdapter(manifest) {
|
|
|
129
129
|
usage(`optional native method ${method} must be a function`, { id: manifest.id, method });
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
// `inboundSockets(context)` names the Unix socket paths - directories or
|
|
133
|
+
// sockets - a sender connects to when this adapter's sessions receive live
|
|
134
|
+
// delivery. A sandboxed sender's installer allows exactly these, so a value
|
|
135
|
+
// that is not a function would silently read as "nothing to allow".
|
|
136
|
+
if (manifest.inboundSockets !== undefined && typeof manifest.inboundSockets !== "function") {
|
|
137
|
+
usage("optional inboundSockets must be a function", { id: manifest.id });
|
|
138
|
+
}
|
|
132
139
|
if (manifest.nativeDelivery !== undefined) {
|
|
133
140
|
const client = manifest.client.certificationName ?? manifest.client.command;
|
|
134
141
|
native.nativeDelivery = validateNativeDeliveryContract(manifest.nativeDelivery,
|
|
@@ -25,13 +25,13 @@ const TRIPLE = /^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
|
25
25
|
* version ACC has already observed is not an older client. Anything unreadable
|
|
26
26
|
* - a probe that failed, "unknown", a vendor string - returns null.
|
|
27
27
|
*/
|
|
28
|
-
function versionOrder(text) {
|
|
28
|
+
export function versionOrder(text) {
|
|
29
29
|
const match = typeof text === "string" && text.toLowerCase() !== "unknown"
|
|
30
30
|
? TRIPLE.exec(text) : null;
|
|
31
31
|
return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function compareVersionOrder(left, right) {
|
|
34
|
+
export function compareVersionOrder(left, right) {
|
|
35
35
|
if (left === null || right === null) return left === right ? 0 : left === null ? -1 : 1;
|
|
36
36
|
for (let index = 0; index < 3; index += 1) {
|
|
37
37
|
if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
|
|
@@ -84,6 +84,15 @@ const PLACEHOLDER = "{{ACC}}";
|
|
|
84
84
|
// shell's to interpret.
|
|
85
85
|
const quote = value => `"${String(value).replace(/(["\\$`])/g, "\\$1")}"`;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Whether a path is one shell word exactly as written: absolute, and made only
|
|
89
|
+
* of the POSIX portable filename characters and slashes. Nothing in it is
|
|
90
|
+
* expanded, split or quoted by a shell, so the unquoted text and the word a
|
|
91
|
+
* shell runs are the same string.
|
|
92
|
+
*/
|
|
93
|
+
export const isShellWord = value => typeof value === "string"
|
|
94
|
+
&& /^\/[A-Za-z0-9._/-]*$/.test(value);
|
|
95
|
+
|
|
87
96
|
/**
|
|
88
97
|
* Write the small shim a client's hook command points at.
|
|
89
98
|
*
|
|
@@ -231,13 +240,18 @@ export const defaultCli = () => ownBinary("acc.mjs");
|
|
|
231
240
|
* JSON, worked out the schema, and wrote records and events by hand - inventing
|
|
232
241
|
* an event type, a harness name, and its own generation tokens. So the command
|
|
233
242
|
* is pinned here for the same reason it is pinned in the shim.
|
|
243
|
+
*
|
|
244
|
+
* `bareWhenSafe` names the shim without quotes when it is one shell word. A
|
|
245
|
+
* client that matches allow rules against the command's first word needs it:
|
|
246
|
+
* Antigravity CLI 1.2.12 never matched a command whose program word was quoted,
|
|
247
|
+
* whatever rule was loaded (issue #214). A path that needs quotes keeps them.
|
|
234
248
|
*/
|
|
235
|
-
export async function bakeSkillCommand({ root, cliShim }) {
|
|
249
|
+
export async function bakeSkillCommand({ root, cliShim, bareWhenSafe = false }) {
|
|
236
250
|
if (typeof cliShim !== "string" || cliShim === "") {
|
|
237
251
|
throw new AccError(EXIT.DATA, "refusing to bake an empty skill command", { cliShim });
|
|
238
252
|
}
|
|
239
253
|
// A managed data home is "Application Support"; an unquoted path splits.
|
|
240
|
-
const command = quote(cliShim);
|
|
254
|
+
const command = bareWhenSafe && isShellWord(cliShim) ? cliShim : quote(cliShim);
|
|
241
255
|
const baked = [];
|
|
242
256
|
const walk = async directory => {
|
|
243
257
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// Capability contract, context projection, config ownership, and the binding
|
|
2
2
|
// that survives between two ephemeral hook processes.
|
|
3
3
|
export { CAPABILITY_SHAPE, assertCapabilities, defineAdapter } from "./capabilities.mjs";
|
|
4
|
-
export { capabilityEvidence, effectiveCapabilities, validateCertification
|
|
4
|
+
export { capabilityEvidence, compareVersionOrder, effectiveCapabilities, validateCertification,
|
|
5
|
+
versionOrder }
|
|
5
6
|
from "./certification.mjs";
|
|
6
|
-
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES,
|
|
7
|
+
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_REASON_CODES,
|
|
7
8
|
compareStableVersions, evaluateNativeEligibility, evaluateVersionContract, parseStableVersion,
|
|
8
9
|
validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
|
|
9
10
|
from "./native-delivery.mjs";
|
|
10
11
|
export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
|
|
11
|
-
export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli,
|
|
12
|
+
export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli, isShellWord,
|
|
12
13
|
defaultRunner, removeInstalledTree, runnerExists, writeCliShim, writeHookShim }
|
|
13
14
|
from "./hook-shim.mjs";
|
|
14
15
|
export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_BINDING_MODES,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
PLANNABLE_ACTIVATION_KINDS, PROBE_KEYS, TIMESTAMP, assertModes, assertReasonCode, closed,
|
|
3
|
+
compareStableVersions, deepFreeze, isPlainObject, isText, parseStableVersion, usage }
|
|
4
|
+
from "./native-vocabulary.mjs";
|
|
5
|
+
import { compareVersionOrder, versionOrder } from "./certification.mjs";
|
|
5
6
|
|
|
6
|
-
// The native-delivery compatibility contract: a
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// The native-delivery compatibility contract: a minimum that is a real passing
|
|
8
|
+
// capture, one or more anchors naming the captured protocol, an explicit
|
|
9
|
+
// denylist, and the activation kinds an adapter may ask the installer for.
|
|
10
|
+
// There is deliberately no maximum: a newer stable client is admitted only
|
|
10
11
|
// when a current read-only probe and a per-session handshake confirm the same
|
|
11
|
-
// protocol contract.
|
|
12
|
-
//
|
|
12
|
+
// protocol contract. There is no platform either: a capture records where a
|
|
13
|
+
// behaviour was observed, the minimum it establishes applies everywhere, and
|
|
14
|
+
// the probe and handshake decide on the machine in hand (see
|
|
15
|
+
// docs/design/2026-09-26-native-delivery-across-platforms.md). Certification
|
|
16
|
+
// evidence still governs every other capability; this rule is used for native
|
|
17
|
+
// live delivery alone.
|
|
13
18
|
|
|
14
|
-
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES,
|
|
19
|
+
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_REASON_CODES,
|
|
15
20
|
PLANNABLE_ACTIVATION_KINDS, compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
|
|
16
21
|
export { validateNativeActivationPlan } from "./native-activation.mjs";
|
|
17
22
|
|
|
@@ -29,8 +34,16 @@ export function validateNativeDeliveryContract(value, { certification, client })
|
|
|
29
34
|
// the next-turn hook, which records the offer itself once its stdout carried
|
|
30
35
|
// the body. Recording a wake as offered would hide the body from that hook.
|
|
31
36
|
const offerKind = Object.hasOwn(value ?? {}, "offerKind") ? value.offerKind : "message";
|
|
37
|
+
// The per-platform shape 0.8.0 and earlier declared. Refused by name: an
|
|
38
|
+
// adapter built against that SDK should learn what replaced it, not fail on
|
|
39
|
+
// a generic unknown field.
|
|
40
|
+
if (Object.hasOwn(value ?? {}, "minimumByPlatform")) {
|
|
41
|
+
usage("nativeDelivery.minimumByPlatform is no longer declared; a capture's minimum applies on "
|
|
42
|
+
+ "every platform, so declare nativeDelivery.minimum instead "
|
|
43
|
+
+ "(docs/design/2026-09-26-native-delivery-across-platforms.md)");
|
|
44
|
+
}
|
|
32
45
|
closed({ ...value, policySource, offerKind },
|
|
33
|
-
["
|
|
46
|
+
["minimum", "anchors", "knownBad", "activationKinds", "policySource", "offerKind"],
|
|
34
47
|
"nativeDelivery");
|
|
35
48
|
if (policySource !== "installation-record") {
|
|
36
49
|
usage("nativeDelivery.policySource must be installation-record");
|
|
@@ -38,48 +51,43 @@ export function validateNativeDeliveryContract(value, { certification, client })
|
|
|
38
51
|
if (!["message", "wake"].includes(offerKind)) {
|
|
39
52
|
usage("nativeDelivery.offerKind must be message or wake");
|
|
40
53
|
}
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
usage("nativeDelivery.
|
|
44
|
-
}
|
|
45
|
-
for (const [platform, version] of Object.entries(minimums)) {
|
|
46
|
-
if (!NATIVE_PLATFORMS.includes(platform)) {
|
|
47
|
-
usage(`nativeDelivery.minimumByPlatform names an unknown platform ${platform}`);
|
|
48
|
-
}
|
|
49
|
-
if (parseStableVersion(version) === null) {
|
|
50
|
-
usage(`nativeDelivery.minimumByPlatform ${platform} must be a stable version`);
|
|
51
|
-
}
|
|
54
|
+
const minimum = value.minimum;
|
|
55
|
+
if (parseStableVersion(minimum) === null) {
|
|
56
|
+
usage("nativeDelivery.minimum must be a stable version");
|
|
52
57
|
}
|
|
53
58
|
if (!Array.isArray(value.anchors) || value.anchors.length === 0) {
|
|
54
59
|
usage("nativeDelivery.anchors must name at least one passing capture");
|
|
55
60
|
}
|
|
56
61
|
const anchors = value.anchors.map((anchor, index) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
if (Object.hasOwn(anchor ?? {}, "platform")) {
|
|
63
|
+
usage(`nativeDelivery anchor ${index} names a platform; the platform a capture was taken on `
|
|
64
|
+
+ "is recorded by its evidence, and the anchor names the version alone "
|
|
65
|
+
+ "(docs/design/2026-09-26-native-delivery-across-platforms.md)");
|
|
66
|
+
}
|
|
67
|
+
closed(anchor, ["version", "protocolContract"], "nativeDelivery anchor");
|
|
68
|
+
if (parseStableVersion(anchor.version) === null) {
|
|
69
|
+
usage(`nativeDelivery anchor ${index} version must be a stable version`);
|
|
61
70
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
if (compareStableVersions(anchor.version, minimum) < 0) {
|
|
72
|
+
usage(`nativeDelivery anchor ${index} version ${anchor.version} is below the minimum ${minimum}: `
|
|
73
|
+
+ "the minimum must be the first passing capture");
|
|
65
74
|
}
|
|
66
75
|
if (!isText(anchor.protocolContract) || !CONTRACT_ID.test(anchor.protocolContract)) {
|
|
67
76
|
usage(`nativeDelivery anchor ${index} protocolContract must be a closed identifier`);
|
|
68
77
|
}
|
|
78
|
+
// Where the capture was taken is provenance the evidence row keeps; the
|
|
79
|
+
// anchor is proven by a passing capture of this version on any platform.
|
|
69
80
|
const proven = (certification?.evidence ?? []).some(item => item.result === "pass"
|
|
70
81
|
&& item.capability === "delivery.livePush" && item.client === client
|
|
71
|
-
&& item.version === anchor.version
|
|
82
|
+
&& item.version === anchor.version);
|
|
72
83
|
if (!proven) {
|
|
73
|
-
usage(`nativeDelivery anchor ${anchor.version}
|
|
74
|
-
|
|
84
|
+
usage(`nativeDelivery anchor ${anchor.version} has no passing delivery.livePush certification`,
|
|
85
|
+
{ anchor });
|
|
75
86
|
}
|
|
76
87
|
return { ...anchor };
|
|
77
88
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
usage(`nativeDelivery minimum ${minimum} on ${platform} must be the first passing capture: `
|
|
81
|
-
+ "no anchor matches it");
|
|
82
|
-
}
|
|
89
|
+
if (!anchors.some(anchor => compareStableVersions(anchor.version, minimum) === 0)) {
|
|
90
|
+
usage(`nativeDelivery minimum ${minimum} must be the first passing capture: no anchor matches it`);
|
|
83
91
|
}
|
|
84
92
|
if (!Array.isArray(value.knownBad)) usage("nativeDelivery.knownBad must be an array");
|
|
85
93
|
const knownBad = value.knownBad.map(entry => {
|
|
@@ -104,24 +112,29 @@ export function validateNativeDeliveryContract(value, { certification, client })
|
|
|
104
112
|
|| kinds.some(kind => !PLANNABLE_ACTIVATION_KINDS.includes(kind))) {
|
|
105
113
|
usage(`nativeDelivery.activationKinds must be unique entries of ${PLANNABLE_ACTIVATION_KINDS.join(", ")}`);
|
|
106
114
|
}
|
|
107
|
-
return deepFreeze({
|
|
108
|
-
|
|
115
|
+
return deepFreeze({ minimum, anchors, knownBad, activationKinds: [...kinds], policySource,
|
|
116
|
+
offerKind });
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
|
|
119
|
+
// `order` is the client's release triple; the denylist names stable versions,
|
|
120
|
+
// so a prerelease of a denylisted release is on it too.
|
|
121
|
+
function knownBadHit(contract, order) {
|
|
122
|
+
const hit = (left, right) => compareVersionOrder(left, versionOrder(right));
|
|
112
123
|
return contract.knownBad.some(entry => (Object.hasOwn(entry, "version")
|
|
113
|
-
?
|
|
114
|
-
:
|
|
124
|
+
? hit(order, entry.version) === 0
|
|
125
|
+
: hit(order, entry.from) >= 0 && hit(order, entry.to) <= 0));
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
// Judges one reported client version against an adapter's captured
|
|
118
|
-
// native-delivery contract: is the
|
|
119
|
-
//
|
|
129
|
+
// native-delivery contract: is the version at or above the captured minimum,
|
|
130
|
+
// and is it not on the denylist. Returns
|
|
120
131
|
// { reasonCode, minimumVersion, protocolContract }; reasonCode is null when
|
|
121
132
|
// the version satisfies the contract, otherwise one of
|
|
122
|
-
// "native_delivery_unsupported", "
|
|
123
|
-
//
|
|
124
|
-
//
|
|
133
|
+
// "native_delivery_unsupported", "version_unavailable", "below_minimum_version",
|
|
134
|
+
// or "known_bad_version". The platform the caller runs on plays no part: the
|
|
135
|
+
// minimum was captured somewhere and applies everywhere. A prerelease is
|
|
136
|
+
// judged by its release triple, as hook capabilities are: what admits it is
|
|
137
|
+
// the protocol contract the probe or handshake reports, not the suffix.
|
|
125
138
|
//
|
|
126
139
|
// This is the static half of the rule only: it never contacts the client, so
|
|
127
140
|
// passing here proves nothing about whether a live probe or handshake
|
|
@@ -130,38 +143,33 @@ function knownBadHit(contract, version) {
|
|
|
130
143
|
// validateNativeHandshake below, and the delivery router's own offer check,
|
|
131
144
|
// which uses this function alone because a response already carries no probe
|
|
132
145
|
// or handshake shape to check further).
|
|
133
|
-
export function evaluateVersionContract(adapter, { clientVersion
|
|
146
|
+
export function evaluateVersionContract(adapter, { clientVersion } = {}) {
|
|
134
147
|
const contract = adapter?.nativeDelivery;
|
|
135
|
-
|
|
136
|
-
// contract" as a missing one, and reading `contract.minimumByPlatform` off it
|
|
137
|
-
// threw the very TypeError the guard below exists to prevent. defineAdapter
|
|
138
|
-
// cannot produce that shape; a hand-built registry entry can.
|
|
139
|
-
if (contract == null) {
|
|
140
|
-
return { reasonCode: "native_delivery_unsupported", minimumVersion: null, protocolContract: null };
|
|
141
|
-
}
|
|
142
|
-
const uncaptured = { reasonCode: "platform_not_captured", minimumVersion: null,
|
|
148
|
+
const unsupported = { reasonCode: "native_delivery_unsupported", minimumVersion: null,
|
|
143
149
|
protocolContract: null };
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
// `== null` rather than `=== undefined`: a null declaration is as much "no
|
|
151
|
+
// contract" as a missing one, and reading `contract.minimum` off it threw
|
|
152
|
+
// the very TypeError the guard below exists to prevent. defineAdapter cannot
|
|
153
|
+
// produce that shape; a hand-built registry entry can.
|
|
154
|
+
if (contract == null) return unsupported;
|
|
155
|
+
// validateNativeDeliveryContract guarantees a minimum and a matching anchor,
|
|
156
|
+
// but this function is also handed adapter objects that never went through
|
|
157
|
+
// it. A declaration missing either half has captured nothing, which is a
|
|
158
|
+
// closed answer - not a TypeError raised deep inside a delivery offer, far
|
|
159
|
+
// from the declaration that caused it. Every other entry point below
|
|
160
|
+
// already answers malformed input this way.
|
|
161
|
+
const minimumVersion = isText(contract.minimum) ? contract.minimum : null;
|
|
162
|
+
if (minimumVersion === null) return unsupported;
|
|
153
163
|
const anchor = (Array.isArray(contract.anchors) ? contract.anchors : [])
|
|
154
|
-
.find(item => item
|
|
155
|
-
if (anchor === undefined) return
|
|
164
|
+
.find(item => item?.version === minimumVersion);
|
|
165
|
+
if (anchor === undefined) return unsupported;
|
|
156
166
|
const facts = { minimumVersion, protocolContract: anchor.protocolContract };
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
if (compareStableVersions(clientVersion, minimumVersion) < 0) {
|
|
167
|
+
const order = versionOrder(clientVersion);
|
|
168
|
+
if (order === null) return { ...facts, reasonCode: "version_unavailable" };
|
|
169
|
+
if (compareVersionOrder(order, versionOrder(minimumVersion)) < 0) {
|
|
162
170
|
return { ...facts, reasonCode: "below_minimum_version" };
|
|
163
171
|
}
|
|
164
|
-
if (knownBadHit(contract,
|
|
172
|
+
if (knownBadHit(contract, order)) return { ...facts, reasonCode: "known_bad_version" };
|
|
165
173
|
return { ...facts, reasonCode: null };
|
|
166
174
|
}
|
|
167
175
|
|
|
@@ -184,13 +192,13 @@ function validateNativeProbe(probe) {
|
|
|
184
192
|
return probe;
|
|
185
193
|
}
|
|
186
194
|
|
|
187
|
-
export function evaluateNativeEligibility(adapter, { clientVersion,
|
|
195
|
+
export function evaluateNativeEligibility(adapter, { clientVersion, probe }) {
|
|
188
196
|
// The probe names the process that will serve the delivery. Judging the
|
|
189
197
|
// detected binary instead refuses a service that satisfies the contract.
|
|
190
198
|
// Only the version is read here; the shape is validated where it always was,
|
|
191
199
|
// so a malformed probe still returns a closed result rather than throwing.
|
|
192
200
|
const serving = isText(probe?.clientVersion) ? probe.clientVersion : clientVersion;
|
|
193
|
-
const rule = evaluateVersionContract(adapter, { clientVersion: serving
|
|
201
|
+
const rule = evaluateVersionContract(adapter, { clientVersion: serving });
|
|
194
202
|
const base = { eligible: false, reasonCode: null, minimumVersion: rule.minimumVersion,
|
|
195
203
|
protocolContract: rule.protocolContract, modes: [] };
|
|
196
204
|
const closedResult = reasonCode => deepFreeze({ ...base, reasonCode });
|
|
@@ -242,13 +250,13 @@ function validateNativeHandshakeShape(handshake) {
|
|
|
242
250
|
// the two records disagreed on every machine where a daemon had updated under
|
|
243
251
|
// its CLI, which is exactly the case the release exists to support. A refused
|
|
244
252
|
// verdict admits nothing and carries null.
|
|
245
|
-
export function validateNativeHandshake(adapter, { clientVersion,
|
|
253
|
+
export function validateNativeHandshake(adapter, { clientVersion, handshake }) {
|
|
246
254
|
// Same rationale as the probe: the handshake names the session that will
|
|
247
255
|
// actually serve, so the static rule is judged against that version. Only
|
|
248
256
|
// the version is read here; the shape is validated where it always was, so
|
|
249
257
|
// a malformed handshake still returns a closed result rather than throwing.
|
|
250
258
|
const serving = isText(handshake?.clientVersion) ? handshake.clientVersion : clientVersion;
|
|
251
|
-
const rule = evaluateVersionContract(adapter, { clientVersion: serving
|
|
259
|
+
const rule = evaluateVersionContract(adapter, { clientVersion: serving });
|
|
252
260
|
const base = { ok: false, reasonCode: null, clientVersion: null,
|
|
253
261
|
protocolContract: rule.protocolContract, modes: [],
|
|
254
262
|
opaqueEndpointRef: null, leaseUntil: null };
|
|
@@ -18,12 +18,12 @@ export const NATIVE_ACTIVATION_KINDS = Object.freeze([
|
|
|
18
18
|
// The kinds an adapter may still ask for.
|
|
19
19
|
export const PLANNABLE_ACTIVATION_KINDS = Object.freeze(["native-config", "native-service"]);
|
|
20
20
|
|
|
21
|
-
export const NATIVE_PLATFORMS = Object.freeze([
|
|
22
|
-
"darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-x64",
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
21
|
// Every reason a client is not live, closed so doctor, hooks, and docs share
|
|
26
22
|
// one vocabulary and vendor strings never leak through it.
|
|
23
|
+
// "platform_not_captured" and "prerelease_not_captured" are historical: 0.8.0
|
|
24
|
+
// and earlier refused a platform no capture named and any prerelease suffix,
|
|
25
|
+
// and their native-attempt records carry both. Nothing produces either any
|
|
26
|
+
// more (docs/design/2026-09-26-native-delivery-across-platforms.md).
|
|
27
27
|
export const NATIVE_REASON_CODES = Object.freeze([
|
|
28
28
|
"native_delivery_unsupported", "platform_not_captured", "version_unavailable",
|
|
29
29
|
"prerelease_not_captured", "below_minimum_version", "known_bad_version",
|
|
@@ -12,7 +12,7 @@ export { describeNative } from "./native-delivery-status.mjs";
|
|
|
12
12
|
import { nativeSessionLines, updateNativeSessions } from "./native-session-diagnostics.mjs";
|
|
13
13
|
import { ALL_ADAPTERS, clientContext, probeTimeout } from "./install-command.mjs";
|
|
14
14
|
import { decisionOf } from "./install-delivery-consent.mjs";
|
|
15
|
-
import { describePresence } from "./main.mjs";
|
|
15
|
+
import { describePresence, presenceBreakdown } from "./main.mjs";
|
|
16
16
|
import { platformPaths } from "./platform-paths.mjs";
|
|
17
17
|
import { noticeUpdate } from "./update-check.mjs";
|
|
18
18
|
import { managedUpdateDiagnostic } from "./managed-runtime/diagnostics.mjs";
|
|
@@ -116,7 +116,7 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
|
|
|
116
116
|
env: runtime?.env ?? {} });
|
|
117
117
|
// The same environment install reads, so detection probes the same client.
|
|
118
118
|
const clients = clientContext(home, path.join(dataHome, "acc"),
|
|
119
|
-
{ env: runtime?.env ?? {} });
|
|
119
|
+
{ env: runtime?.env ?? {}, platform: runtime?.platform });
|
|
120
120
|
const adapters = ALL_ADAPTERS();
|
|
121
121
|
const detected = await detect({ adapters, context: clients,
|
|
122
122
|
probeTimeoutMs: probeTimeout(runtime?.env) });
|
|
@@ -192,8 +192,12 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
192
192
|
// "unreadable protocol.json" and a person's first `acc doctor` in a new
|
|
193
193
|
// project would answer that their store is broken.
|
|
194
194
|
const started = await stat(path.join(root, "protocol.json")).then(() => true, () => false);
|
|
195
|
+
// Shaped like the storage report, so a reader of one finds every field of
|
|
196
|
+
// the other: without `trimmedThrough` the summary said "history from
|
|
197
|
+
// undefined" on every first doctor in a new project.
|
|
195
198
|
const report = !started
|
|
196
|
-
? { healthy: true, blocked: [], corrupt: [], repaired: []
|
|
199
|
+
? { healthy: true, blocked: [], corrupt: [], repaired: [], swept: 0, staged: 0, partials: 0,
|
|
200
|
+
retired: 0, trimmedThrough: null }
|
|
197
201
|
: options.repair === true
|
|
198
202
|
? await repairFilesystemStore({ root, clock })
|
|
199
203
|
: await diagnoseFilesystemStore({ root });
|
|
@@ -276,7 +280,8 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
276
280
|
report.trimmedThrough === null ? null : `history from ${report.trimmedThrough}`,
|
|
277
281
|
].filter(Boolean);
|
|
278
282
|
const staging = held.length === 0 ? "" : ` (${held.join(", ")})`;
|
|
279
|
-
const text = [`store healthy${staging};
|
|
283
|
+
const text = [`store healthy${staging}; `
|
|
284
|
+
+ `${describePresence(status.counts, presenceBreakdown(status.participants, ALL_ADAPTERS()))}; `
|
|
280
285
|
+ `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
|
|
281
286
|
...adapters.filter(adapter => (adapter.present || adapter.installed)
|
|
282
287
|
&& adapter.nativeDelivery.reasonCode === "native_delivery_unsupported"
|
|
@@ -7,7 +7,7 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
|
7
7
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
8
8
|
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
9
9
|
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
10
|
-
import { applyPlan, detectInstallation, loadOwnership, planInstallation }
|
|
10
|
+
import { applyPlan, detectInstallation, loadOwnership, planInstallation, receiverSockets }
|
|
11
11
|
from "@agents-can-communicate/installer";
|
|
12
12
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
13
13
|
|
|
@@ -28,7 +28,7 @@ export { decideDelivery } from "./install-delivery-consent.mjs";
|
|
|
28
28
|
// pointed at the home itself writes beside them rather than inside them. That
|
|
29
29
|
// install reports success and the client never reads a byte of it.
|
|
30
30
|
export const clientContext = (home, stateRoot,
|
|
31
|
-
{ env = {}, dataHome, cwd = process.cwd() } = {}) => ({
|
|
31
|
+
{ env = {}, dataHome, cwd = process.cwd(), platform } = {}) => withReceivers({
|
|
32
32
|
home,
|
|
33
33
|
...(dataHome === undefined ? {} : { dataHome }),
|
|
34
34
|
configDir: path.join(home, ".claude"),
|
|
@@ -59,7 +59,14 @@ export const clientContext = (home, stateRoot,
|
|
|
59
59
|
// roster and record nothing, every write failing with EPERM on the writer
|
|
60
60
|
// lock. Measured with `codex exec`, which is how an agent actually runs.
|
|
61
61
|
stateRoot,
|
|
62
|
-
});
|
|
62
|
+
}, platform);
|
|
63
|
+
|
|
64
|
+
// Every socket ACC can deliver to, as each receiving adapter declares it, for
|
|
65
|
+
// the one sender that sandboxes its shell: the Codex permission profile is
|
|
66
|
+
// written and diagnosed from this list, so it covers Claude Code's inbox even
|
|
67
|
+
// when an install names Codex alone (issue #213).
|
|
68
|
+
const withReceivers = (context, platform) => ({ ...context,
|
|
69
|
+
receiverSockets: receiverSockets(ALL_ADAPTERS(), { ...context, platform }) });
|
|
63
70
|
|
|
64
71
|
export const ALL_ADAPTERS = () => [createAntigravityAdapter(), createClaudeCodeAdapter(),
|
|
65
72
|
createCodexAdapter(), createGeminiCliAdapter(), createGrokAdapter(), createKimiAdapter()];
|
|
@@ -222,7 +229,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
222
229
|
const { data: dataHome } = platformPaths({ platform: runtime.platform,
|
|
223
230
|
env: runtime.env ?? {} });
|
|
224
231
|
const context = clientContext(home, path.join(dataHome, "acc"),
|
|
225
|
-
{ env: runtime.env ?? {}, dataHome });
|
|
232
|
+
{ env: runtime.env ?? {}, dataHome, platform: runtime.platform });
|
|
226
233
|
|
|
227
234
|
const detected = await detectInstallation({ adapters, context,
|
|
228
235
|
probeTimeoutMs: probeTimeout(runtime.env) });
|