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,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,9 +2,7 @@
2
2
  // reasons; installation and doctor must not turn a version floor into readiness.
3
3
  const REASONS = Object.freeze({
4
4
  native_delivery_unsupported: "this adapter has no native delivery channel",
5
- platform_not_captured: "native delivery is not verified on this platform",
6
5
  version_unavailable: "the client version could not be verified",
7
- prerelease_not_captured: "this client version is not a verified stable release",
8
6
  below_minimum_version: "the client is below the native delivery minimum version",
9
7
  known_bad_version: "this client version has a known native delivery failure",
10
8
  native_endpoint_unavailable: "the client's local delivery service is unavailable",
@@ -13,9 +13,8 @@ const run = promisify(execFile);
13
13
 
14
14
  // Reasons the client itself will not change within an install: unsupported,
15
15
  // as opposed to degraded, which a repaired probe or shell may lift.
16
- const STATIC_REASONS = new Set(["native_delivery_unsupported", "platform_not_captured",
17
- "version_unavailable", "prerelease_not_captured", "below_minimum_version",
18
- "known_bad_version"]);
16
+ const STATIC_REASONS = new Set(["native_delivery_unsupported", "version_unavailable",
17
+ "below_minimum_version", "known_bad_version"]);
19
18
 
20
19
  const DEFAULT_PROBE_TIMEOUT_MS = 3_000;
21
20
 
@@ -32,8 +31,10 @@ const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b
32
31
  * has: nothing is written, and a client that is not installed simply fails to
33
32
  * start.
34
33
  */
35
- export const spawnProbe = async (command, args) => {
36
- const { stdout, stderr } = await run(command, args);
34
+ // `env` decides where the command is looked up, so detection given an
35
+ // environment finds the client on that environment's PATH.
36
+ export const spawnProbe = async (command, args, { env } = {}) => {
37
+ const { stdout, stderr } = await run(command, args, env === undefined ? {} : { env });
37
38
  return `${stdout}${stderr}`.trim();
38
39
  };
39
40
 
@@ -76,7 +77,7 @@ async function detectNative(adapter, entry, { context, platform, probeTimeoutMs,
76
77
  }
77
78
  try {
78
79
  facts.eligibility = evaluateNativeEligibility(adapter,
79
- { clientVersion: entry.version, platform, probe: facts.probe });
80
+ { clientVersion: entry.version, probe: facts.probe });
80
81
  } catch {
81
82
  facts.eligibility = { eligible: false, reasonCode: "feature_probe_failed" };
82
83
  }
@@ -123,8 +124,11 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
123
124
  const output = await withTimeout(
124
125
  // The declared binary, never the adapter id. Guessing the id made
125
126
  // every client whose command differs from it look uninstalled.
127
+ // On the PATH the native probes below use: a caller that passes its
128
+ // own environment must not find a different client, or none.
126
129
  Promise.resolve(probe(adapter.client.command,
127
- adapter.client.versionArgs ?? ["--version"])),
130
+ adapter.client.versionArgs ?? ["--version"],
131
+ { env: context?.env === undefined ? undefined : { ...process.env, ...context.env } })),
128
132
  probeTimeoutMs, `${adapter.id} version probe`);
129
133
  if (typeof output === "string" && output !== "") {
130
134
  entry.present = true;
@@ -161,6 +165,8 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
161
165
  if (detected.blocked) entry.blocked = detected.blocked;
162
166
  if (detected.outgoingDelivery) entry.outgoingDelivery = detected.outgoingDelivery;
163
167
  if (detected.inboundDelivery) entry.inboundDelivery = detected.inboundDelivery;
168
+ // Whether the client asks before each ACC command, and what would stop it.
169
+ if (detected.commandApproval) entry.commandApproval = detected.commandApproval;
164
170
  if (detected.nativeSetup) entry.nativeSetup = detected.nativeSetup;
165
171
  entry.diagnostics = [...(detected.diagnostics ?? [])];
166
172
  // What a person has to do, as opposed to what is true. Adapters that
@@ -1,6 +1,7 @@
1
1
  // Detection, planning, application, and the record of what was installed.
2
2
  export { detectInstallation, spawnProbe } from "./detect.mjs";
3
3
  export { planInstallation } from "./plan.mjs";
4
+ export { receiverSockets } from "./receiver-sockets.mjs";
4
5
  export { applyPlan } from "./apply.mjs";
5
6
  export { finalizeRemoval, fingerprint, loadOwnership, missingArtifactParents, recordInstall,
6
7
  removeEmptyOwnedDirectories, removeOwned, removeOwnedArtifacts, treeFingerprint, verifyOwned }
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+
3
+ /**
4
+ * Every Unix socket path ACC can deliver to on this machine.
5
+ *
6
+ * A sender whose client sandboxes its shell - Codex - has to be allowed to
7
+ * connect to the recipient's socket, and the recipient's adapter is the only
8
+ * one that knows where that socket lives. Each adapter declares its own through
9
+ * `inboundSockets(context)`; the sender's adapter receives the composed list as
10
+ * `context.receiverSockets` and never names another vendor's paths itself.
11
+ *
12
+ * Composed from every known adapter, not the ones an install names: installing
13
+ * Codex alone must still allow a Claude Code session installed before or after.
14
+ */
15
+ export function receiverSockets(adapters, context) {
16
+ const paths = [...adapters].flatMap(adapter =>
17
+ typeof adapter.inboundSockets === "function" ? adapter.inboundSockets(context) ?? [] : []);
18
+ return [...new Set(paths.filter(item => typeof item === "string" && path.isAbsolute(item)))].sort();
19
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
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/protocol",
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/storage-filesystem",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,7 +64,8 @@ async function replaceHandleBytes(handle, bytes) {
64
64
  * @returns {Promise<"published" | "already_published">}
65
65
  */
66
66
  export async function publishAtomic(destination, bytes,
67
- { root, tmpDir, stageDir, replace = false, deadlineAt, afterAccepted }) {
67
+ { root, tmpDir, stageDir, replace = false, deadlineAt, afterAccepted, afterStageEnsured,
68
+ afterStageRenamed }) {
68
69
  assertPublicationDeadline(deadlineAt);
69
70
  // The accepted stage lives apart from the partial a failed publication
70
71
  // leaves, so what a file is follows from the directory it was created in
@@ -145,9 +146,8 @@ export async function publishAtomic(destination, bytes,
145
146
  // The seam the race test uses, in the manner retainFile already
146
147
  // establishes: the window this closes is invisible without one.
147
148
  await afterAccepted?.();
148
- await ensureManagedDirectory(root, stageDir);
149
- await rename(temporary, stage);
150
- await syncDirectory(stageDir);
149
+ await retainAcceptedStage({ root, stageDir, temporary, stage, afterStageEnsured,
150
+ afterStageRenamed });
151
151
  } else {
152
152
  // A crash/error before immutable acceptance keeps its unique partial.
153
153
  // Retention avoids the unsafe parent-check/unlink pathname window, while
@@ -157,6 +157,32 @@ export async function publishAtomic(destination, bytes,
157
157
  }
158
158
  }
159
159
 
160
+ // The sweep can take `stage` between any two of these steps: it renames the
161
+ // directory aside, and a store still opening publishes here without the writer
162
+ // mutex the sweep holds (CI on #217). The bytes are already published, so the
163
+ // retained copy goes to whichever directory carries the name when it moves, and
164
+ // a copy the sweep took along with the directory needs no sync here - the sweep
165
+ // discards that directory. The two callbacks are seams for the race tests.
166
+ const STAGE_RETRIES = 4;
167
+
168
+ async function retainAcceptedStage({ root, stageDir, temporary, stage, afterStageEnsured,
169
+ afterStageRenamed }) {
170
+ for (let attempt = 0; ; attempt += 1) {
171
+ await ensureManagedDirectory(root, stageDir);
172
+ await afterStageEnsured?.();
173
+ try {
174
+ await rename(temporary, stage);
175
+ break;
176
+ } catch (error) {
177
+ if (error.code !== "ENOENT" || attempt >= STAGE_RETRIES) throw error;
178
+ }
179
+ }
180
+ await afterStageRenamed?.();
181
+ await syncDirectory(stageDir).catch(error => {
182
+ if (error.code !== "ENOENT") throw error;
183
+ });
184
+ }
185
+
160
186
  // The opener is the seam the race tests use, and stays last so callers that do
161
187
  // not care never see it.
162
188
  export async function readJsonIfPresent(filePath, root, openFile) {
@@ -29,32 +29,42 @@ function absolutePath(value, name, root = value) {
29
29
  return path.normalize(value);
30
30
  }
31
31
 
32
+ // How many more times a create-mode check takes a directory that left its name
33
+ // while being checked. The stage sweep renames `stage` aside and recreates it,
34
+ // without regard to a store that is still opening beside it, so one more try is
35
+ // the real case; the bound only stops a directory that never stays put from
36
+ // holding its caller (CI on #217).
37
+ const VANISHED_RETRIES = 4;
38
+
32
39
  async function inspectRealDirectory(directory, root, create) {
33
- let details;
34
- try {
35
- details = await lstat(directory);
36
- } catch (error) {
37
- if (error.code === "ENOENT" && !create) throw error;
38
- if (error.code !== "ENOENT") {
39
- throw invalidDirectory("cannot inspect managed directory", directory, root, error.message);
40
- }
40
+ for (let attempt = 0; ; attempt += 1) {
41
+ let details;
41
42
  try {
42
- await mkdir(directory);
43
- } catch (mkdirError) {
44
- if (mkdirError.code !== "EEXIST") {
45
- throw invalidDirectory("cannot create managed directory", directory, root,
46
- mkdirError.message);
43
+ details = await lstat(directory);
44
+ } catch (error) {
45
+ if (error.code !== "ENOENT") {
46
+ throw invalidDirectory("cannot inspect managed directory", directory, root, error.message);
47
47
  }
48
+ // Missing again right after it was created means a rename took it.
49
+ if (!create || attempt > VANISHED_RETRIES) throw error;
50
+ try {
51
+ await mkdir(directory);
52
+ } catch (mkdirError) {
53
+ if (mkdirError.code !== "EEXIST") {
54
+ throw invalidDirectory("cannot create managed directory", directory, root,
55
+ mkdirError.message);
56
+ }
57
+ }
58
+ continue;
48
59
  }
49
- details = await lstat(directory);
50
- }
51
- if (!details.isDirectory() || details.isSymbolicLink()) {
52
- throw invalidDirectory("managed directory is not a real directory", directory, root);
60
+ if (!details.isDirectory() || details.isSymbolicLink()) {
61
+ throw invalidDirectory("managed directory is not a real directory", directory, root);
62
+ }
63
+ return details;
53
64
  }
54
- return details;
55
65
  }
56
66
 
57
- async function inspectManagedDirectory(rootPath, directoryPath, create) {
67
+ async function inspectManagedDirectory(rootPath, directoryPath, create, { afterInspect } = {}) {
58
68
  const root = absolutePath(rootPath, "managed root");
59
69
  const directory = absolutePath(directoryPath, "managed directory", root);
60
70
  const relative = relativeWithin(root, directory);
@@ -67,7 +77,17 @@ async function inspectManagedDirectory(rootPath, directoryPath, create) {
67
77
  let canonicalParent = canonicalRoot;
68
78
  for (const segment of relative.split(path.sep).filter(Boolean)) {
69
79
  current = path.join(current, segment);
70
- details = await inspectRealDirectory(current, root, create);
80
+ let resolved;
81
+ for (let attempt = 0; ; attempt += 1) {
82
+ details = await inspectRealDirectory(current, root, create);
83
+ await afterInspect?.(current);
84
+ // A create-mode caller uses this name next. A directory renamed away
85
+ // between the check and the resolution leaves realpath nothing to resolve
86
+ // (ENOENT), so it is checked and created again, then resolved again; every
87
+ // check below applies to whichever attempt settles.
88
+ resolved = await resolveSegment(current, create && attempt < VANISHED_RETRIES);
89
+ if (resolved !== null) break;
90
+ }
71
91
  // realpath answers with the name the directory carries *now*, which is not
72
92
  // always the name that was asked for: on Darwin it resolves by opening the
73
93
  // path and asking the kernel for that vnode's current path, so a directory
@@ -86,7 +106,6 @@ async function inspectManagedDirectory(rootPath, directoryPath, create) {
86
106
  // swapped in under this name is not. ENOENT on the re-stat means it moved
87
107
  // again between the two calls, which cannot redirect anything: whatever
88
108
  // the caller opens through this path next fails the same way.
89
- const resolved = await realpath(current);
90
109
  if (path.dirname(resolved) !== canonicalParent
91
110
  || (path.basename(resolved) !== segment
92
111
  && !await stillTheSameDirectory(details, resolved, current, root))) {
@@ -97,6 +116,16 @@ async function inspectManagedDirectory(rootPath, directoryPath, create) {
97
116
  return { directory, stat: details };
98
117
  }
99
118
 
119
+ // null when the name went away and the caller may take it again.
120
+ async function resolveSegment(current, retry) {
121
+ try {
122
+ return await realpath(current);
123
+ } catch (error) {
124
+ if (error.code === "ENOENT" && retry) return null;
125
+ throw error;
126
+ }
127
+ }
128
+
100
129
  async function stillTheSameDirectory(details, resolved, current, root) {
101
130
  const served = await stat(resolved).then(found => found, error => {
102
131
  if (error.code === "ENOENT") return null;
@@ -105,10 +134,12 @@ async function stillTheSameDirectory(details, resolved, current, root) {
105
134
  return served === null || (served.dev === details.dev && served.ino === details.ino);
106
135
  }
107
136
 
108
- export async function assertManagedDirectory(rootPath, directoryPath) {
109
- return inspectManagedDirectory(rootPath, directoryPath, false);
137
+ // `afterInspect` is the seam the race tests use: it runs between a segment's
138
+ // check and its resolution, the window a concurrent rename lands in.
139
+ export async function assertManagedDirectory(rootPath, directoryPath, options) {
140
+ return inspectManagedDirectory(rootPath, directoryPath, false, options);
110
141
  }
111
142
 
112
- export async function ensureManagedDirectory(rootPath, directoryPath) {
113
- return (await inspectManagedDirectory(rootPath, directoryPath, true)).directory;
143
+ export async function ensureManagedDirectory(rootPath, directoryPath, options) {
144
+ return (await inspectManagedDirectory(rootPath, directoryPath, true, options)).directory;
114
145
  }
@@ -121,8 +121,12 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
121
121
  // must not roll the same journal forward concurrently.
122
122
  await recoverOpenJournals();
123
123
 
124
- // Sweeping is a write, so it holds the same mutex recovery does and no
125
- // publisher is in flight while the stage directory is detached. It is bounded
124
+ // Sweeping is a write, so it holds the same mutex recovery does, and no
125
+ // transaction publishes while the stage directory is detached. A store that is
126
+ // still opening beside it does: the identity record, the directory checks and
127
+ // the journal's first slot above run without the mutex, so the managed
128
+ // directory checks and publishAtomic take a `stage` that left its name once
129
+ // more rather than failing (CI on #217). It is bounded
126
130
  // per pass: a store carrying a large accumulation drains over several opens
127
131
  // rather than spending one hook's whole budget on it. The mutex is taken only
128
132
  // on the opens that actually sweep - see sweepIfDue, which decides before it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "accManagedUpdateProtocol": 2,
5
5
  "accStoreVersion": 6,
6
6
  "type": "module",