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.
Files changed (78) hide show
  1. package/bin/entrypoints/acc-mcp.mjs +0 -1
  2. package/bin/entrypoints/acc.mjs +0 -1
  3. package/docs/ADAPTER_AUTHORING.md +50 -17
  4. package/docs/ARCHITECTURE.md +9 -6
  5. package/docs/CAPABILITIES.md +24 -14
  6. package/docs/CLI.md +22 -3
  7. package/docs/CONFIGURATION.md +73 -8
  8. package/docs/GETTING_STARTED.md +6 -3
  9. package/docs/HOW_IT_WORKS.md +3 -1
  10. package/docs/PROTOCOL.md +2 -2
  11. package/docs/RELEASING.md +6 -4
  12. package/docs/SECURITY_MODEL.md +12 -1
  13. package/docs/TROUBLESHOOTING.md +34 -7
  14. package/docs/UPGRADING.md +29 -1
  15. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +7 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +5 -4
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +204 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +45 -10
  21. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +14 -4
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +12 -17
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +5 -3
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-observed-directories.mjs +62 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +41 -0
  27. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  28. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +6 -4
  29. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
  30. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +5 -4
  31. package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +69 -17
  32. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
  33. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
  34. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
  35. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
  36. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
  37. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -0
  38. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  40. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  41. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  42. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  43. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +7 -0
  44. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +2 -2
  45. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +16 -2
  46. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
  47. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +85 -77
  48. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +4 -4
  49. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  50. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +9 -4
  51. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +11 -4
  52. package/node_modules/@agents-can-communicate/cli/src/main.mjs +49 -12
  53. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
  54. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
  55. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +20 -10
  56. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  57. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +17 -2
  58. package/node_modules/@agents-can-communicate/core/src/status.mjs +26 -2
  59. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  60. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
  61. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +9 -41
  62. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  63. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
  64. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +3 -4
  65. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
  66. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +12 -1
  67. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  68. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
  69. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -7
  70. package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
  71. package/node_modules/@agents-can-communicate/installer/src/receiver-sockets.mjs +19 -0
  72. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  73. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  75. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +30 -4
  76. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +56 -25
  77. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +6 -2
  78. 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 { failMaintenance, maintenanceContext, probeMaintenanceCli, probeMaintenanceInstall,
7
- readMaintenancePid, runMaintenanceCommand, verifyMaintenanceProcess } from "./maintenance-host.mjs";
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 socketIsReady(paths.socketPath)) failMaintenance("daemon_socket_unproven");
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. Never delete
170
- // stale metadata or stop/restart a process on this path.
171
- if (await info(paths.socketPath) || await info(paths.pidPath)) {
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 = [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Coordinate this Gemini CLI session with other AI agent sessions working in the same workspace.",
5
5
  "contextFileName": "skills/acc/SKILL.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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, NATIVE_PLATFORMS, NATIVE_REASON_CODES,
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
- NATIVE_PLATFORMS, PLANNABLE_ACTIVATION_KINDS, PROBE_KEYS, TIMESTAMP, assertModes,
3
- assertReasonCode, closed, compareStableVersions, deepFreeze, isPlainObject, isText,
4
- parseStableVersion, usage } from "./native-vocabulary.mjs";
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 per-platform minimum that is a
7
- // real passing capture, one or more anchors naming the captured protocol, an
8
- // explicit denylist, and the activation kinds an adapter may ask the installer
9
- // for. There is deliberately no maximum: a newer stable client is admitted only
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. Exact-version certification still governs every other
12
- // capability; this rule is used for native live delivery alone.
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, NATIVE_PLATFORMS, NATIVE_REASON_CODES,
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
- ["minimumByPlatform", "anchors", "knownBad", "activationKinds", "policySource", "offerKind"],
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 minimums = value.minimumByPlatform;
42
- if (!isPlainObject(minimums) || Object.keys(minimums).length === 0) {
43
- usage("nativeDelivery.minimumByPlatform must map at least one captured platform to a version");
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
- closed(anchor, ["platform", "version", "protocolContract"], "nativeDelivery anchor");
58
- const minimum = minimums[anchor.platform];
59
- if (minimum === undefined) {
60
- usage(`nativeDelivery anchor ${index} platform ${anchor.platform} has no minimum`);
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 (parseStableVersion(anchor.version) === null
63
- || compareStableVersions(anchor.version, minimum) < 0) {
64
- usage(`nativeDelivery anchor ${index} version must be a stable version at or above the minimum`);
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 && item.platform === anchor.platform);
82
+ && item.version === anchor.version);
72
83
  if (!proven) {
73
- usage(`nativeDelivery anchor ${anchor.version} on ${anchor.platform} has no passing `
74
- + "delivery.livePush certification", { anchor });
84
+ usage(`nativeDelivery anchor ${anchor.version} has no passing delivery.livePush certification`,
85
+ { anchor });
75
86
  }
76
87
  return { ...anchor };
77
88
  });
78
- for (const [platform, minimum] of Object.entries(minimums)) {
79
- if (!anchors.some(anchor => anchor.platform === platform && anchor.version === minimum)) {
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({ minimumByPlatform: { ...minimums }, anchors, knownBad,
108
- activationKinds: [...kinds], policySource, offerKind });
115
+ return deepFreeze({ minimum, anchors, knownBad, activationKinds: [...kinds], policySource,
116
+ offerKind });
109
117
  }
110
118
 
111
- function knownBadHit(contract, version) {
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
- ? compareStableVersions(version, entry.version) === 0
114
- : compareStableVersions(version, entry.from) >= 0 && compareStableVersions(version, entry.to) <= 0));
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 platform captured, is the version at or
119
- // above the captured minimum, and is it not on the denylist. Returns
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", "platform_not_captured",
123
- // "version_unavailable", "prerelease_not_captured", "below_minimum_version",
124
- // or "known_bad_version".
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, platform }) {
146
+ export function evaluateVersionContract(adapter, { clientVersion } = {}) {
134
147
  const contract = adapter?.nativeDelivery;
135
- // `== null` rather than `=== undefined`: a null declaration is as much "no
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
- // validateNativeDeliveryContract guarantees a minimum map and a matching
145
- // anchor, but this function is also handed adapter objects that never went
146
- // through it. A declaration missing either half has captured nothing for
147
- // this platform, which is a closed answer - not a TypeError raised deep
148
- // inside a delivery offer, far from the declaration that caused it. Every
149
- // other entry point below already answers malformed input this way.
150
- if (typeof platform !== "string" || !isPlainObject(contract.minimumByPlatform)) return uncaptured;
151
- const minimumVersion = contract.minimumByPlatform[platform] ?? null;
152
- if (minimumVersion === null) return uncaptured;
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.platform === platform && item.version === minimumVersion);
155
- if (anchor === undefined) return uncaptured;
164
+ .find(item => item?.version === minimumVersion);
165
+ if (anchor === undefined) return unsupported;
156
166
  const facts = { minimumVersion, protocolContract: anchor.protocolContract };
157
- if (!isText(clientVersion)) return { ...facts, reasonCode: "version_unavailable" };
158
- if (parseStableVersion(clientVersion) === null) {
159
- return { ...facts, reasonCode: "prerelease_not_captured" };
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, clientVersion)) return { ...facts, reasonCode: "known_bad_version" };
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, platform, probe }) {
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, platform });
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, platform, handshake }) {
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, platform });
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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}; ${describePresence(status.counts)}; `
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) });