agents-can-communicate 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +3 -1
  2. package/bin/entrypoints/acc-bootstrap.mjs +6 -48
  3. package/bin/entrypoints/acc-claude-channel.mjs +47 -157
  4. package/bin/entrypoints/acc-mcp.mjs +0 -1
  5. package/bin/entrypoints/acc.mjs +0 -1
  6. package/docs/ADAPTER_AUTHORING.md +74 -24
  7. package/docs/ARCHITECTURE.md +31 -18
  8. package/docs/CAPABILITIES.md +38 -25
  9. package/docs/CLI.md +51 -13
  10. package/docs/CONCEPTS.md +5 -4
  11. package/docs/CONFIGURATION.md +18 -11
  12. package/docs/GETTING_STARTED.md +15 -12
  13. package/docs/GLOSSARY.md +2 -1
  14. package/docs/HOW_IT_WORKS.md +8 -4
  15. package/docs/PROTOCOL.md +25 -6
  16. package/docs/RELEASING.md +8 -5
  17. package/docs/SECURITY_MODEL.md +14 -1
  18. package/docs/TROUBLESHOOTING.md +66 -29
  19. package/docs/UPGRADING.md +49 -8
  20. package/docs/WHY_ACC.md +5 -3
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +2 -3
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +26 -30
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +249 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
  38. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -4
  40. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
  41. package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +5 -5
  42. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
  44. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
  45. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
  46. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -0
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  50. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  51. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +2 -2
  54. package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
  55. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
  56. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
  57. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
  58. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +104 -84
  59. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +8 -4
  60. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  61. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
  62. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
  63. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
  64. package/node_modules/@agents-can-communicate/cli/src/main.mjs +24 -3
  65. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
  66. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
  67. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
  68. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
  69. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
  70. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
  71. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  72. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +17 -2
  73. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
  74. package/node_modules/@agents-can-communicate/core/src/status.mjs +31 -1
  75. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  76. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
  77. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +33 -48
  78. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  79. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
  80. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
  81. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +27 -15
  82. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
  83. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +4 -2
  84. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  85. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
  86. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
  87. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +6 -11
  88. package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
  89. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
  90. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
  91. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
  92. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
  93. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  95. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  96. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
  97. package/package.json +1 -1
  98. package/bin/acc-bootstrap.mjs +0 -7
  99. package/bin/acc-claude-channel.mjs +0 -10
  100. package/bin/entrypoints/claude-channel-binding.mjs +0 -46
  101. package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
  102. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
  103. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
  104. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
  105. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
@@ -3,7 +3,7 @@ import { constants } from "node:fs";
3
3
  import { access, lstat, open, realpath } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { compareStableVersions, parseStableVersion } from "./app-server-client.mjs";
6
+ import { compareVersions, versionOrder } from "./app-server-client.mjs";
7
7
 
8
8
  export const MINIMUM_MAINTENANCE_CLI = "0.154.0";
9
9
  export const absolute = value => typeof value === "string" && path.isAbsolute(value)
@@ -15,6 +15,20 @@ export const startTimeValid = value => typeof value === "string"
15
15
  export const managedExecutablePaths = codexHome => ["bin/codex", "codex"].map(name =>
16
16
  path.join(codexHome, "packages/standalone/current", name));
17
17
 
18
+ // Where lsof lives: /usr/sbin on macOS, /usr/bin on Linux. A fixed list of
19
+ // absolute paths, never PATH, so a directory an operator can write to cannot
20
+ // supply the tool that proves socket ownership. The first executable wins;
21
+ // none means the socket cannot be proven and verification fails closed.
22
+ export const LSOF_CANDIDATES = Object.freeze(["/usr/sbin/lsof", "/usr/bin/lsof"]);
23
+
24
+ export async function resolveLsof({ access: check = access } = {}) {
25
+ for (const candidate of LSOF_CANDIDATES) {
26
+ try { await check(candidate, constants.X_OK); return candidate; }
27
+ catch { /* the next candidate may be the one this host has */ }
28
+ }
29
+ return null;
30
+ }
31
+
18
32
  export function runMaintenanceCommand(command, args, options) {
19
33
  return new Promise(resolve => execFile(command, args,
20
34
  { timeout: 20_000, ...options, maxBuffer: 131_072, windowsHide: true },
@@ -71,12 +85,12 @@ async function resolveCli(pathEnv) {
71
85
  }
72
86
 
73
87
  const cliVersion = response => response.status === 0
74
- ? /^codex-cli (\d+\.\d+\.\d+)\s*$/.exec(response.stdout)?.[1] ?? null : null;
88
+ ? /^codex-cli (\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)\s*$/.exec(response.stdout)?.[1] ?? null : null;
75
89
 
76
90
  export async function probeMaintenanceCli(paths, run) {
77
91
  const cliPath = await resolveCli(paths.options.env.PATH);
78
92
  const version = cliVersion(await run(cliPath, ["--version"], paths.options));
79
- if (!parseStableVersion(version) || compareStableVersions(version, MINIMUM_MAINTENANCE_CLI) < 0) {
93
+ if (!versionOrder(version) || compareVersions(version, MINIMUM_MAINTENANCE_CLI) < 0) {
80
94
  failMaintenance("maintenance_cli_unsupported");
81
95
  }
82
96
  const help = await run(cliPath, ["app-server", "daemon", "--help"], paths.options);
@@ -117,17 +131,39 @@ export async function observeMaintenanceProcess(pid, paths, run) {
117
131
  return { state: "alive", processStartTime: match[1], command: match[2] };
118
132
  }
119
133
 
120
- export async function verifyMaintenanceProcess(snapshot, paths, run) {
134
+ /** Whether `lsof -Fn` output lists a socket at one of these paths. macOS prints
135
+ * the path alone; Linux appends the socket type (`/path type=STREAM`, measured
136
+ * on the ubuntu CI job), and a connected peer may carry more after it. The name
137
+ * is the text before the first ` type=`, compared whole. */
138
+ export function socketListedIn(stdout, socketPaths) {
139
+ const names = new Set(typeof socketPaths === "string" ? [socketPaths] : socketPaths);
140
+ return stdout.split("\n").some(line => line.startsWith("n")
141
+ && names.has(line.slice(1).split(" type=")[0]));
142
+ }
143
+
144
+ // The daemon's command line as measured: 0.154.0 ran `<current path> app-server
145
+ // --listen unix://`; 0.157.1 runs the resolved releases path with
146
+ // `--managed-daemon` appended. The executable must live in the standalone tree
147
+ // and be the managed binary itself, however the tree names it.
148
+ const DAEMON_COMMAND = /^(\/.+?) app-server --listen unix:\/\/(?: --managed-daemon)?$/;
149
+
150
+ export async function verifyMaintenanceProcess(snapshot, paths, run, { lsof = resolveLsof } = {}) {
121
151
  const process = await observeMaintenanceProcess(snapshot.pid, paths, run);
122
- const suffix = " app-server --listen unix://";
123
- const executable = process.command?.endsWith(suffix) ? process.command.slice(0, -suffix.length) : null;
152
+ const executable = DAEMON_COMMAND.exec(process.command ?? "")?.[1] ?? null;
153
+ const standalone = path.join(paths.codexHome, "packages", "standalone") + path.sep;
124
154
  if (process.state !== "alive" || process.processStartTime !== snapshot.processStartTime
125
- || !managedExecutablePaths(paths.codexHome).includes(executable)
126
- || await realpath(executable) !== await realpath(paths.managedPath)) {
155
+ || executable === null || !executable.startsWith(standalone)
156
+ || await realpath(executable).catch(() => null) !== await realpath(paths.managedPath)) {
127
157
  failMaintenance("daemon_identity_unavailable");
128
158
  }
129
- const sockets = await run("/usr/sbin/lsof", ["-n", "-a", "-p", String(snapshot.pid), "-U", "-Fn"], paths.options);
130
- if (sockets.status !== 0 || !sockets.stdout.split("\n").includes(`n${paths.socketPath}`)) {
159
+ // 0.157.1 lists the socket by the path it listens on, not by the symlink it
160
+ // reports; either names the same socket.
161
+ const listening = [paths.socketPath, await realpath(paths.socketPath).catch(() => null)]
162
+ .filter(value => value !== null);
163
+ const lsofPath = await lsof();
164
+ if (lsofPath === null) failMaintenance("daemon_socket_unproven");
165
+ const sockets = await run(lsofPath, ["-n", "-a", "-p", String(snapshot.pid), "-U", "-Fn"], paths.options);
166
+ if (sockets.status !== 0 || !socketListedIn(sockets.stdout, listening)) {
131
167
  failMaintenance("daemon_socket_unproven");
132
168
  }
133
169
  }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
- import { openCodexAppServer, parseStableVersion } from "./app-server-client.mjs";
3
- import { socketIsReady } from "./native-endpoint.mjs";
2
+ import { openCodexAppServer, versionOrder } from "./app-server-client.mjs";
3
+ import { readySocketPath } from "./native-endpoint.mjs";
4
4
  import { absolute, approvedProcessIsDead, failMaintenance, maintenanceContext,
5
5
  managedExecutablePaths, metadataExists, probeMaintenanceInstall, readMaintenancePid, runMaintenanceCommand,
6
6
  startTimeValid, verifyMaintenanceProcess } from "./maintenance-host.mjs";
@@ -16,7 +16,7 @@ function validApproval(value) {
16
16
  return value?.serviceId === "codex-app-server" && ["ready", "busy"].includes(value.state)
17
17
  && Number.isSafeInteger(value.pid) && value.pid > 1 && startTimeValid(value.processStartTime)
18
18
  && ["codexHome", "cliPath", "managedPath", "socketPath"].every(key => absolute(value[key]))
19
- && ["cliVersion", "serverVersion", "managedVersion"].every(key => parseStableVersion(value[key]))
19
+ && ["cliVersion", "serverVersion", "managedVersion"].every(key => versionOrder(value[key]))
20
20
  && value.managedVersion === value.cliVersion
21
21
  && managedExecutablePaths(value.codexHome).includes(value.managedPath)
22
22
  && value.socketPath === path.join(value.codexHome, "app-server-control/app-server-control.sock");
@@ -36,8 +36,7 @@ export function createCodexMaintenance({ run = runMaintenanceCommand, open = ope
36
36
  const paths = await maintenanceContext(context);
37
37
  Object.assign(snapshot, { codexHome: paths.codexHome, socketPath: paths.socketPath, managedPath: paths.managedPath });
38
38
  if (!await metadataExists(paths.socketPath) && !await metadataExists(paths.pidPath)) return null;
39
- if (paths.platform !== "darwin-arm64") failMaintenance("maintenance_platform_unsupported");
40
- if (!await socketIsReady(paths.socketPath)) failMaintenance("daemon_socket_unproven");
39
+ if (!await readySocketPath(paths.socketPath)) failMaintenance("daemon_socket_unproven");
41
40
  Object.assign(snapshot, await probeMaintenanceInstall(paths, run));
42
41
  const version = await run(snapshot.cliPath, ["app-server", "daemon", "version"], paths.options);
43
42
  if (version.status !== 0) failMaintenance("daemon_version_unavailable");
@@ -45,7 +44,7 @@ export function createCodexMaintenance({ run = runMaintenanceCommand, open = ope
45
44
  if (observed.status !== "running" || observed.backend !== "pid") failMaintenance("maintenance_backend_unsupported");
46
45
  if (observed.managedCodexPath !== snapshot.managedPath || observed.socketPath !== snapshot.socketPath
47
46
  || observed.cliVersion !== snapshot.cliVersion || observed.managedCodexVersion !== snapshot.managedVersion
48
- || !parseStableVersion(observed.appServerVersion)) failMaintenance("daemon_identity_unavailable");
47
+ || !versionOrder(observed.appServerVersion)) failMaintenance("daemon_identity_unavailable");
49
48
  snapshot.serverVersion = observed.appServerVersion;
50
49
  Object.assign(snapshot, await readMaintenancePid(paths.pidPath));
51
50
  await verifyMaintenanceProcess(snapshot, paths, run);
@@ -83,7 +82,6 @@ export function createCodexMaintenance({ run = runMaintenanceCommand, open = ope
83
82
  try {
84
83
  const pinned = { ...context, env: { ...(context?.env ?? process.env), CODEX_HOME: expected.codexHome } };
85
84
  const paths = await maintenanceContext(pinned);
86
- if (paths.platform !== "darwin-arm64") return refused("maintenance_platform_unsupported");
87
85
  const install = { ...paths, ...await probeMaintenanceInstall(paths, run) };
88
86
  if (!sameFields(expected, install, installationKeys)) return refused("service_identity_changed");
89
87
  const existing = await inspectMaintenance(pinned);
@@ -1,11 +1,10 @@
1
- import { realpath } from "node:fs/promises";
2
1
  import { decisionBody } from "@agents-can-communicate/adapter-sdk";
3
2
 
4
3
  import { CODEX_QUEUE_MINIMUM, MINIMUM_VERSION, PROTOCOL_CONTRACT, QUEUE_MODES,
5
- addCodexQueueMessage, canonicalCwd, compareStableVersions, controlSocketPath,
6
- locateCodexThread, openCodexAppServer, parseStableVersion, probeCodexQueue,
7
- safeReason, serverVersionOf } from "./app-server-client.mjs";
8
- import { newEndpointId, readNativeEndpoint, removeNativeEndpoint, socketIsReady,
4
+ addCodexQueueMessage, canonicalCwd, compareVersions, controlSocketPath,
5
+ locateCodexThread, openCodexAppServer, probeCodexQueue, safeReason, serverVersionOf,
6
+ versionOrder } from "./app-server-client.mjs";
7
+ import { newEndpointId, readNativeEndpoint, readySocketPath, removeNativeEndpoint, socketIsReady,
9
8
  writeNativeEndpoint } from "./native-endpoint.mjs";
10
9
 
11
10
  // The receiver's hook supplies thread and cwd. Core holds only a random endpoint
@@ -35,8 +34,8 @@ export async function probeNativeDelivery({ timeoutMs = 750, env = process.env,
35
34
  open = openCodexAppServer } = {}) {
36
35
  const unsupported = (reasonCode, clientVersion = null) => ({ supported: false, clientVersion,
37
36
  protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
38
- const socketPath = controlSocketPath(env);
39
- if (!await socketIsReady(socketPath)) return unsupported("native_endpoint_unavailable");
37
+ const socketPath = await readySocketPath(controlSocketPath(env));
38
+ if (socketPath === null) return unsupported("native_endpoint_unavailable");
40
39
  try {
41
40
  return await usingPeer(socketPath, timeoutMs, open, async peer => {
42
41
  const probe = await probeCodexQueue(peer);
@@ -46,7 +45,12 @@ export async function probeNativeDelivery({ timeoutMs = 750, env = process.env,
46
45
  modes: [...QUEUE_MODES], reasonCode: null };
47
46
  });
48
47
  } catch (error) {
49
- return unsupported(error?.code === "ETIMEDOUT" ? "probe_timeout" : "feature_probe_failed");
48
+ if (error?.code === "ETIMEDOUT") return unsupported("probe_timeout");
49
+ // A service that died without cleaning up leaves its socket file behind,
50
+ // and a connect to it is refused. That is a missing service, the one fact
51
+ // whose advice (start the daemon) fixes it, not a failed feature probe.
52
+ return unsupported(safeReason(error) === "transport_unavailable"
53
+ ? "native_endpoint_unavailable" : "feature_probe_failed");
50
54
  }
51
55
  }
52
56
 
@@ -85,7 +89,7 @@ export async function verifyReceiver(peer, endpoint, { probe = probeCodexQueue,
85
89
  : result.reasonCode === "below_minimum_version" ? "handshake_version_mismatch"
86
90
  : "protocol_mismatch", servingVersion: null };
87
91
  }
88
- if (compareStableVersions(result.serverVersion, CODEX_QUEUE_MINIMUM) < 0) {
92
+ if (compareVersions(result.serverVersion, CODEX_QUEUE_MINIMUM) < 0) {
89
93
  return { reasonCode: "handshake_version_mismatch", servingVersion: null };
90
94
  }
91
95
  const located = await locate(peer, { threadId: endpoint.threadId, cwd: endpoint.cwd });
@@ -99,12 +103,12 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
99
103
  env = process.env, timeoutMs = 750, now = Date.now, open = openCodexAppServer } = {}) {
100
104
  const rejected = reason => closed(clientVersion, reason);
101
105
  if (!Number.isInteger(clientPid) || clientPid <= 0) return rejected("client_process_unknown");
102
- if (parseStableVersion(clientVersion) === null) return rejected("version_unavailable");
106
+ if (versionOrder(clientVersion) === null) return rejected("version_unavailable");
103
107
  if (typeof event?.sessionId !== "string" || event.sessionId === "") return rejected("handshake_failed");
104
108
  const cwd = await canonicalCwd(event.cwd);
105
109
  if (cwd === null) return rejected("workspace_identity_unavailable");
106
- const socketPath = await realpath(controlSocketPath(env)).catch(() => null);
107
- if (!await socketIsReady(socketPath)) return rejected("handshake_failed");
110
+ const socketPath = await readySocketPath(controlSocketPath(env));
111
+ if (socketPath === null) return rejected("handshake_failed");
108
112
  try {
109
113
  return await usingPeer(socketPath, timeoutMs, open, async peer => {
110
114
  const endpoint = { schemaVersion: 1, endpointId: newEndpointId(), socketPath,
@@ -3,7 +3,7 @@ import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
- import { PROTOCOL_CONTRACT, parseStableVersion } from "./app-server-client.mjs";
6
+ import { PROTOCOL_CONTRACT, versionOrder } from "./app-server-client.mjs";
7
7
 
8
8
  const KEYS = ["schemaVersion", "endpointId", "socketPath", "threadId", "cwd",
9
9
  "clientVersion", "protocolContract", "leaseUntil"];
@@ -21,7 +21,7 @@ function valid(record) {
21
21
  && record.schemaVersion === 1 && ENDPOINT.test(record.endpointId)
22
22
  && absolute(record.socketPath) && absolute(record.cwd)
23
23
  && typeof record.threadId === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(record.threadId)
24
- && parseStableVersion(record.clientVersion) !== null
24
+ && versionOrder(record.clientVersion) !== null
25
25
  && record.protocolContract === PROTOCOL_CONTRACT
26
26
  && typeof record.leaseUntil === "string"
27
27
  && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(record.leaseUntil)
@@ -49,6 +49,16 @@ export async function socketIsReady(socketPath) {
49
49
  } catch { return false; }
50
50
  }
51
51
 
52
+ /** The socket a control socket path leads to, or null. Codex 0.157.1 keeps a
53
+ * symlink at the path it reports and the socket itself under /private/tmp
54
+ * (measured 2026-09-26), so the path is followed once and the socket at its
55
+ * end is held to socketIsReady: an owned socket, never itself a link. */
56
+ export async function readySocketPath(socketPath) {
57
+ if (!absolute(socketPath)) return null;
58
+ const resolved = await realpath(socketPath).catch(() => null);
59
+ return resolved !== null && await socketIsReady(resolved) ? resolved : null;
60
+ }
61
+
52
62
  // A fresh, immutable registration for one hook binding. Refresh observes the
53
63
  // same registration again; it cannot overwrite a successor's random endpoint.
54
64
  export async function writeNativeEndpoint({ runtimeDir, record }) {
@@ -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.7.1",
3
+ "version": "0.8.1",
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.7.1",
3
+ "version": "0.8.1",
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.7.1",
3
+ "version": "0.8.1",
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.7.1",
3
+ "version": "0.8.1",
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.7.1",
3
+ "version": "0.8.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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;
@@ -4,7 +4,7 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
4
4
  // so the four cannot drift: a consumer that has to ask which adapter produced an
5
5
  // event is a consumer that will eventually get it wrong.
6
6
  export const NORMALIZED_EVENT_KEYS = Object.freeze(["kind", "sessionId", "cwd", "model",
7
- "parentSessionId", "tool", "targets"]);
7
+ "parentSessionId", "tool", "targets", "permissionMode", "endReason"]);
8
8
 
9
9
  export const EVENT_KINDS = Object.freeze(["sessionStart", "sessionEnd", "heartbeat",
10
10
  "beforeTurn", "beforeTool", "afterTool", "turnEnd", "childStart", "childEnd", "other"]);
@@ -34,7 +34,7 @@ export function normalizedEvent(fields) {
34
34
  }
35
35
  }
36
36
  const { kind, sessionId, cwd, model = null, parentSessionId = null, tool = null,
37
- targets = [] } = fields;
37
+ targets = [], permissionMode = null, endReason = null } = fields;
38
38
 
39
39
  if (!EVENT_KINDS.includes(kind)) data("unknown normalised event kind", { kind });
40
40
  if (typeof sessionId !== "string" || sessionId === "") {
@@ -50,6 +50,19 @@ export function normalizedEvent(fields) {
50
50
  }
51
51
  }
52
52
 
53
+ // The client's own name for the session's permission mode, when its hook
54
+ // reports one. Only a native binding reads it, to tell a sender whether the
55
+ // client will hold a wake for approval.
56
+ if (permissionMode !== null && (typeof permissionMode !== "string" || permissionMode === "")) {
57
+ data("normalised event permission mode must be a non-empty string", { kind });
58
+ }
59
+
60
+ // Why the client ended the session, in its own words, when it says. A sender
61
+ // is told a conversation was cleared rather than that its peer vanished.
62
+ if (endReason !== null && (typeof endReason !== "string" || endReason === "")) {
63
+ data("normalised event end reason must be a non-empty string", { kind });
64
+ }
65
+
53
66
  return Object.freeze({ kind, sessionId, cwd, model, parentSessionId, tool,
54
- targets: Object.freeze([...targets]) });
67
+ targets: Object.freeze([...targets]), permissionMode, endReason });
55
68
  }
@@ -58,8 +58,6 @@ const ownBinary = name => {
58
58
  };
59
59
 
60
60
  export const defaultRunner = () => ownBinary("acc-hook.mjs");
61
- export const defaultBootstrap = () => ownBinary("acc-bootstrap.mjs");
62
- export const defaultChannel = () => ownBinary("acc-claude-channel.mjs");
63
61
  export const defaultAntigravityRelay = () => ownBinary("acc-antigravity-relay.mjs");
64
62
 
65
63
  export const runnerExists = async runner => {
@@ -199,7 +197,10 @@ export async function writeCliShim({ dir, cli = defaultCli(), node = process.exe
199
197
  * created, and a recognised path is not a reason to delete it.
200
198
  */
201
199
  export async function removeInstalledTree(target, keep = []) {
202
- if (keep.includes(target)) return false;
200
+ // A kept tree holds someone's own work. Removing a directory inside it, or one
201
+ // that contains it, would delete that work as surely as removing it.
202
+ const within = (inner, outer) => inner === outer || inner.startsWith(`${outer}${path.sep}`);
203
+ if (keep.some(kept => within(target, kept) || within(kept, target))) return false;
203
204
  try {
204
205
  await lstat(target);
205
206
  } catch (error) {
@@ -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, defaultBootstrap, defaultChannel, defaultCli,
12
+ export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli,
12
13
  defaultRunner, removeInstalledTree, runnerExists, writeCliShim, writeHookShim }
13
14
  from "./hook-shim.mjs";
14
15
  export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { COMMAND_NAME, IDENTIFIER, NATIVE_ACTIVATION_KINDS, SHELL_SOURCE, assertReasonCode,
3
+ import { IDENTIFIER, NATIVE_ACTIVATION_KINDS, SHELL_SOURCE, assertReasonCode,
4
4
  closed, deepFreeze, isPlainObject, isText, usage } from "./native-vocabulary.mjs";
5
5
 
6
6
  // A native activation plan is what an adapter hands the installer: closed
@@ -27,19 +27,9 @@ function validateMechanism(mechanism, index) {
27
27
  usage(`${label} kind must be one of ${NATIVE_ACTIVATION_KINDS.join(", ")}`);
28
28
  }
29
29
  if (mechanism.kind === "shell-bootstrap") {
30
- closed(mechanism, ["kind", "command", "realExecutable", "prefixArgs"], label);
31
- if (!isText(mechanism.command) || !COMMAND_NAME.test(mechanism.command)) {
32
- usage(`${label} command must be a bare command name`);
33
- }
34
- if (!isText(mechanism.realExecutable) || !path.isAbsolute(mechanism.realExecutable)) {
35
- usage(`${label} realExecutable must be an absolute path`);
36
- }
37
- if (!Array.isArray(mechanism.prefixArgs) || mechanism.prefixArgs.some(arg => !isText(arg)
38
- || SHELL_SOURCE.test(arg))) {
39
- usage(`${label} prefixArgs must be plain argument strings, never shell source`);
40
- }
41
- return { kind: mechanism.kind, command: mechanism.command,
42
- realExecutable: mechanism.realExecutable, prefixArgs: [...mechanism.prefixArgs] };
30
+ // ACC 0.7.x put a `claude` shim on PATH this way. Install records of it are
31
+ // still read so it can be retired; a plan can no longer create one.
32
+ usage(`${label}: shell-bootstrap activation was removed; no adapter may plan one`);
43
33
  }
44
34
  if (mechanism.kind === "native-config") {
45
35
  closed(mechanism, ["kind", "artifactIds"], label);