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
package/docs/UPGRADING.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Upgrading to 0.5.0
2
2
 
3
+ ## From 0.8.x
4
+
5
+ Update as usual:
6
+
7
+ ```bash
8
+ acc update
9
+ acc version
10
+ acc doctor
11
+ ```
12
+
13
+ The update refreshes every installed integration with its recorded policy. Two of them change
14
+ a client's own settings:
15
+
16
+ - **Codex.** ACC's permission profile in `~/.codex/config.toml` also allows the Claude Code
17
+ inbox socket directories, so a Codex session can wake a Claude Code session live. Grants are
18
+ written in their resolved form, such as `/private/tmp/cc-socks` on macOS. A profile you
19
+ edited stays yours: doctor then names the missing grants and `acc install --adapter codex`.
20
+ Start a new Codex session after the update; a session already open keeps its old policy.
21
+ - **Antigravity CLI.** The install adds `command(<wrapper>)` to `permissions.allow` in
22
+ `~/.gemini/antigravity-cli/settings.json`, and the skill calls the wrapper without quotes, so
23
+ a woken session runs its ACC commands without an approval prompt. Restart `agy`: it reads
24
+ the file only at startup. `acc uninstall` removes every copy of that rule.
25
+
26
+ `acc doctor` and `acc status` describe an idle session by what a message sent to it meets
27
+ (`idle, wake on send`, `idle until next turn`, `idle, inbox only`), and each status row carries
28
+ `processTracked`, `liveDelivery` and `liveLeaseCurrent`. This release adds no store event type
29
+ or field, so a store it writes stays readable by 0.8.x.
30
+
3
31
  ## From 0.7.x
4
32
 
5
33
  Claude Code live delivery wakes each session through the inbox socket that Claude Code opens
@@ -165,7 +193,7 @@ keeps holding activation across an uninstall until it exits.
165
193
 
166
194
  ## Confirmed client service maintenance
167
195
 
168
- On macOS arm64, `acc update` can ask once to restart a verified Codex service that holds a
196
+ `acc update` can ask once to restart a verified Codex service that holds a
169
197
  native binding blocking activation, or that is serving a version outside the captured
170
198
  native-delivery contract. A serving version that satisfies that contract is left alone,
171
199
  even when the installed CLI has moved to a newer build. The captured maintenance contract
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-antigravity",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "acc",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Coordinate this Antigravity CLI session with other AI agent sessions working in the same workspace."
5
5
  }
@@ -41,6 +41,13 @@ work. An MCP connection can have a different participant from the hook session;
41
41
  use inbox/reply only for the participant the message addresses. Do not borrow a
42
42
  peer's ID, read runtime bindings, or improvise credentials.
43
43
 
44
+ ### Run each command as written
45
+
46
+ Start every ACC command with `{{ACC}}` exactly as this skill writes it: add no
47
+ quotes around it and no `sh -c` wrapper. Antigravity CLI decides from a command's
48
+ first word whether it needs approval, and the rule that lets ACC commands run
49
+ without a prompt matches only that form.
50
+
44
51
  ## Start shared work once
45
52
 
46
53
  After understanding the request, publish one concise intent:
@@ -1,4 +1,4 @@
1
- import { defineAdapter, projectContext, projectContextResult }
1
+ import { channelSocketDirectory, defineAdapter, projectContext, projectContextResult }
2
2
  from "@agents-can-communicate/adapter-sdk";
3
3
  import certification from "../certification.json" with { type: "json" };
4
4
 
@@ -76,13 +76,14 @@ export function createAntigravityAdapter() {
76
76
  // no retireNativeSession here: the hook runner retires and re-publishes the
77
77
  // binding on every turn, and must not take the relay with it.
78
78
  nativeDelivery: {
79
- minimumByPlatform: { "darwin-arm64": ANTIGRAVITY_CLI_VERSION },
80
- anchors: [{ platform: "darwin-arm64", version: ANTIGRAVITY_CLI_VERSION,
81
- protocolContract: PROTOCOL_CONTRACT }],
79
+ minimum: ANTIGRAVITY_CLI_VERSION,
80
+ anchors: [{ version: ANTIGRAVITY_CLI_VERSION, protocolContract: PROTOCOL_CONTRACT }],
82
81
  knownBad: [], activationKinds: ["native-config"], policySource: "installation-record",
83
82
  },
84
83
  probeNativeDelivery, planNativeActivation, bindNativeSession, refreshNativeSession,
85
84
  offerMessage,
85
+ // The relay binds in ACC's own channel directory; a sandboxed sender is allowed it.
86
+ inboundSockets: () => [channelSocketDirectory()],
86
87
  deliveryFallback: { diagnostic:
87
88
  `Antigravity CLI next-turn and live delivery are certified from ${ANTIGRAVITY_CLI_VERSION} `
88
89
  + "onward; live delivery also needs the agent to start ACC's relay once per conversation, "
@@ -0,0 +1,204 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { blankJson, isShellWord, removeIfEmpty, writeForeignJson }
5
+ from "@agents-can-communicate/adapter-sdk";
6
+
7
+ /**
8
+ * The one allow rule ACC may add to Antigravity CLI's settings, with consent.
9
+ *
10
+ * Antigravity CLI asks before every shell command. On 1.2.12 a live push woke an
11
+ * idle session and its first ACC command waited at an approval prompt with
12
+ * nobody there, so the peer never got an answer (issue #214). A rule under
13
+ * `permissions.allow` removes the prompt. What 1.2.12 was captured doing:
14
+ *
15
+ * - The file is `~/.gemini/antigravity-cli/settings.json`, read once at startup.
16
+ * `~/.gemini/settings.json` belongs to Gemini CLI and is never touched here.
17
+ * - A one-word rule is a prefix: `command("<wrapper>")` let `<wrapper> help`
18
+ * run unasked.
19
+ * - A command whose first word is quoted matched no rule at all, quoted or not;
20
+ * quotes around an argument are fine. So the skill names the wrapper bare,
21
+ * and a wrapper path that only stays one word inside quotes cannot be allowed.
22
+ *
23
+ * ACC writes the bare form, `command(<wrapper>)`, on every install of this
24
+ * adapter: the user decided on 2026-09-27 that it is always on, with no
25
+ * question and no dependence on the delivery policy. The file is still the
26
+ * operator's: one that does not parse, or whose `permissions.allow` is not a
27
+ * list, is never rewritten. What ACC added is recorded in its own data home -
28
+ * the rule and the containers it had to create - and uninstall takes back
29
+ * exactly that, and always ends the claim, so the same words added later by
30
+ * the operator stay theirs.
31
+ */
32
+ export const agySettingsPath = home => path.join(home, ".gemini", "antigravity-cli",
33
+ "settings.json");
34
+ export const cliWrapperPath = home => path.join(home, ".gemini", "config", "acc", "acc-cli.sh");
35
+ export const accAllowRule = wrapper => `command(${wrapper})`;
36
+
37
+ // Forms that already allow every command starting with the bare wrapper: the
38
+ // one ACC writes, and the quoted one-word form 1.2.12 was captured matching.
39
+ const allowingForms = wrapper => [accAllowRule(wrapper), `command("${wrapper}")`];
40
+
41
+ // Beside the `created-*` markers, for the same reason: the installer removes the
42
+ // shim directory before uninstall runs. `acc doctor` passes the state root.
43
+ const claimDir = ({ dataHome, stateRoot, home }) => typeof dataHome === "string" && dataHome !== ""
44
+ ? path.join(dataHome, "acc", "adapter-antigravity")
45
+ : typeof stateRoot === "string" && stateRoot !== ""
46
+ ? path.join(stateRoot, "adapter-antigravity")
47
+ : path.join(home, ".gemini", "config", "acc");
48
+ const claimPath = (context, file) => path.join(claimDir(context),
49
+ `allow-rule-${Buffer.from(file).toString("base64url")}`);
50
+
51
+ const isObject = value => value !== null && typeof value === "object" && !Array.isArray(value);
52
+
53
+ async function readClaim(file) {
54
+ try {
55
+ const claim = JSON.parse(await readFile(file, "utf8"));
56
+ return typeof claim?.rule === "string" ? claim : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /** The settings as the client would read them, or why ACC must not edit them. */
63
+ async function readSettings(file) {
64
+ let text;
65
+ try {
66
+ text = await readFile(file, "utf8");
67
+ } catch (error) {
68
+ if (error.code === "ENOENT") return { state: "missing", value: {}, allow: [] };
69
+ return { state: "unreadable", reason: `could not be read (${error.code ?? error.message})` };
70
+ }
71
+ let value;
72
+ try {
73
+ value = JSON.parse(text);
74
+ } catch {
75
+ return { state: "unreadable", reason: "is not valid JSON" };
76
+ }
77
+ if (!isObject(value)) return { state: "unreadable", reason: "does not hold a JSON object" };
78
+ if (value.permissions !== undefined && !isObject(value.permissions)) {
79
+ return { state: "unreadable", reason: "has a permissions value that is not an object" };
80
+ }
81
+ const allow = value.permissions?.allow;
82
+ if (allow !== undefined && !Array.isArray(allow)) {
83
+ return { state: "unreadable", reason: "has a permissions.allow value that is not a list" };
84
+ }
85
+ return { state: "read", value, allow: allow ?? [] };
86
+ }
87
+
88
+ const RESTART = "Antigravity CLI reads it at startup, so a session started before it was "
89
+ + "added still asks until agy restarts";
90
+
91
+ /** What the rule's state is on this machine, in words doctor can print. */
92
+ export async function inspectAllowRule(context) {
93
+ const file = agySettingsPath(context.home);
94
+ const wrapper = cliWrapperPath(context.home);
95
+ const rule = accAllowRule(wrapper);
96
+ const base = { file, wrapper, rule, owned: false };
97
+ if (!isShellWord(wrapper)) {
98
+ return { ...base, state: "unmatchable", diagnostic: "each ACC command waits for approval, "
99
+ + "so a live wake stops at the agent's first ACC command until someone answers the "
100
+ + `approval prompt. No allow rule can remove it here: the wrapper ${wrapper} stays one `
101
+ + "shell word only inside quotes, and Antigravity CLI matches no rule against a command "
102
+ + "whose first word is quoted. ACC adds the prefix rule to permissions.allow in "
103
+ + `${file} when the wrapper path holds only letters, digits and . _ - /` };
104
+ }
105
+ const settings = await readSettings(file);
106
+ if (settings.state === "unreadable") {
107
+ return { ...base, state: "unreadable", diagnostic: `${file} ${settings.reason}, so ACC `
108
+ + "leaves it as it is and cannot tell whether ACC commands wait for approval; a live "
109
+ + "wake stops at the agent's first ACC command while they do. Repair the file, then run "
110
+ + `acc install --adapter antigravity to add ${rule} to permissions.allow` };
111
+ }
112
+ const matched = settings.allow.find(entry => allowingForms(wrapper).includes(entry));
113
+ if (matched !== undefined) {
114
+ const claim = await readClaim(claimPath(context, file));
115
+ const owned = claim?.rule === matched;
116
+ return { ...base, state: "allowed", owned, diagnostic: "ACC commands run without an "
117
+ + `approval prompt: permissions.allow in ${file} holds ${matched}`
118
+ + `${owned ? ", added by ACC" : ""}. ${RESTART}` };
119
+ }
120
+ // Every install writes the rule, so its absence means one did not run here, or
121
+ // the rule was taken out since.
122
+ return { ...base, state: "prompts",
123
+ diagnostic: "each ACC command waits for approval, so a live wake stops at the agent's "
124
+ + "first ACC command until someone answers the approval prompt. ACC's install adds "
125
+ + `${rule} to permissions.allow in ${file}, and it is not there: run acc install `
126
+ + "--adapter antigravity, then restart agy" };
127
+ }
128
+
129
+ async function addRule(context, found) {
130
+ const settings = await readSettings(found.file);
131
+ const claimFile = claimPath(context, found.file);
132
+ // Only what this write creates. A file or container that is already there is
133
+ // the operator's as far as ACC can tell: one ACC made earlier cannot be told
134
+ // from one they put in its place, and carrying the old flags over deleted
135
+ // their file at uninstall (review of #218). The cost is an empty container
136
+ // ACC once made staying after uninstall, never the operator's file going.
137
+ const created = {
138
+ file: settings.state === "missing",
139
+ permissions: settings.value.permissions === undefined,
140
+ allow: settings.value.permissions?.allow === undefined,
141
+ };
142
+ // The claim first: a crash between the two leaves a claim with no rule, which
143
+ // uninstall drops, rather than a rule nobody can tell is ACC's.
144
+ await mkdir(path.dirname(claimFile), { recursive: true });
145
+ await writeFile(claimFile, `${JSON.stringify({ file: found.file, rule: found.rule,
146
+ created })}\n`);
147
+ await writeForeignJson(found.file, { ...settings.value, permissions: {
148
+ ...settings.value.permissions, allow: [...settings.allow, found.rule] } },
149
+ { readFile, writeFile, mkdir });
150
+ }
151
+
152
+ /**
153
+ * Take back the rule ACC recorded adding, and the containers it created if they
154
+ * are empty. Every copy of it goes: one in the same words cannot be told from
155
+ * ACC's, and the client itself has duplicated grants across reloads, so taking
156
+ * one back left ACC commands running unasked after uninstall (review of #218).
157
+ * Anything else in the file stays, and a rule the operator had before ACC came
158
+ * was never claimed.
159
+ */
160
+ export async function withdrawAllowRule(context) {
161
+ const file = agySettingsPath(context.home);
162
+ const claimFile = claimPath(context, file);
163
+ const claim = await readClaim(claimFile);
164
+ if (claim === null) return { changes: [], diagnostics: [], needsAction: [] };
165
+ const settings = await readSettings(file);
166
+ if (settings.state === "unreadable") {
167
+ // Kept, so the next install or uninstall can finish once the file reads.
168
+ return { changes: [], diagnostics: [], needsAction: [`${file} ${settings.reason}; ACC `
169
+ + `left it as it is and still has to remove its ${claim.rule} from permissions.allow`] };
170
+ }
171
+ if (!settings.allow.includes(claim.rule)) {
172
+ await rm(claimFile, { force: true });
173
+ return { changes: [], diagnostics: [], needsAction: [] };
174
+ }
175
+ const permissions = { ...settings.value.permissions,
176
+ allow: settings.allow.filter(rule => rule !== claim.rule) };
177
+ if (claim.created?.allow === true && permissions.allow.length === 0) delete permissions.allow;
178
+ const next = { ...settings.value, permissions };
179
+ if (claim.created?.permissions === true && Object.keys(permissions).length === 0) {
180
+ delete next.permissions;
181
+ }
182
+ await writeForeignJson(file, next, { readFile, writeFile, mkdir });
183
+ if (claim.created?.file === true) {
184
+ await removeIfEmpty(file, { readFile, rm, isEmpty: blankJson() });
185
+ }
186
+ await rm(claimFile, { force: true });
187
+ return { changes: [file], diagnostics: [`removed ACC's ${claim.rule} from ${file}`],
188
+ needsAction: [] };
189
+ }
190
+
191
+ /** Put ACC's rule in place, at every install. */
192
+ export async function ensureAllowRule(context) {
193
+ const found = await inspectAllowRule(context);
194
+ if (found.state === "unmatchable" || found.state === "unreadable") {
195
+ return { changes: [], diagnostics: [found.diagnostic], needsAction: [found.diagnostic] };
196
+ }
197
+ if (found.state === "allowed") {
198
+ return { changes: [], diagnostics: [found.diagnostic], needsAction: [] };
199
+ }
200
+ await addRule(context, found);
201
+ return { changes: [found.file], needsAction: [], diagnostics: [`added ${found.rule} to `
202
+ + `permissions.allow in ${found.file}: ACC commands run without an approval prompt in `
203
+ + "Antigravity sessions started from now on; restart agy for a session already open"] };
204
+ }
@@ -5,11 +5,13 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { promisify } from "node:util";
7
7
 
8
- import { bakeSkillCommand, blankJson, defaultAntigravityRelay, ownVersion, removeIfEmpty,
9
- stampPluginVersion, writeCliShim, writeForeignJson, writeHookShim }
8
+ import { bakeSkillCommand, blankJson, defaultAntigravityRelay, isShellWord, ownVersion,
9
+ removeIfEmpty, stampPluginVersion, writeCliShim, writeForeignJson, writeHookShim }
10
10
  from "@agents-can-communicate/adapter-sdk";
11
11
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
12
12
 
13
+ import { agySettingsPath, cliWrapperPath, ensureAllowRule, inspectAllowRule,
14
+ withdrawAllowRule } from "./allow-rule.mjs";
13
15
  import { relayShimPath, runningRelays, stopRelays } from "./relays.mjs";
14
16
 
15
17
  const run = promisify(execFile);
@@ -349,7 +351,9 @@ async function installSkillPlugin(context) {
349
351
  await cp(bundle, plugin, { recursive: true });
350
352
  await stampPluginVersion({ file: path.join(plugin, "plugin.json"),
351
353
  version: await ownVersion(import.meta.url), io: { readFile, writeFile } });
352
- await bakeSkillCommand({ root: plugin, cliShim });
354
+ // Bare when the path allows it: 1.2.12 matched no allow rule against a
355
+ // command whose first word was quoted (issue #214).
356
+ await bakeSkillCommand({ root: plugin, cliShim, bareWhenSafe: true });
353
357
  await agyFor(context)(["plugin", "install", plugin], { home, env: context.env });
354
358
  } finally {
355
359
  await rm(stage, { recursive: true, force: true });
@@ -399,10 +403,13 @@ export async function installAntigravity(context) {
399
403
  await writeFile(createdMarker(context, file), `${file}\n`);
400
404
  }
401
405
  await installSkillPlugin(context);
402
- const changes = [shimDir(home), file, pluginInstallPath(home), vendorManifestPath(home)];
406
+ // After the skill, whose wrapper the rule names. Always on (#214, 2026-09-27).
407
+ const allow = await ensureAllowRule(context);
408
+ const changes = [shimDir(home), file, pluginInstallPath(home), vendorManifestPath(home),
409
+ ...allow.changes];
403
410
 
404
- const diagnostics = [];
405
- const needsAction = [];
411
+ const diagnostics = [...allow.diagnostics];
412
+ const needsAction = [...allow.needsAction];
406
413
  if (locationOf(context) === "workspace") {
407
414
  diagnostics.push("a workspace registration loads only while this project is an open "
408
415
  + `Antigravity workspace; in print mode pass --add-dir ${context.antigravityWorkspace}`);
@@ -524,8 +531,14 @@ export async function uninstallAntigravity(context) {
524
531
  await rm(manifestMarker, { force: true });
525
532
  }
526
533
 
534
+ // The allow rule ACC recorded adding, and nothing else in the operator's file.
535
+ const allow = await withdrawAllowRule(context);
536
+ changes.push(...allow.changes);
537
+ diagnostics.push(...allow.diagnostics);
538
+
527
539
  if (!keep.includes(shimDir(home))) await rm(shimDir(home), { recursive: true, force: true });
528
- return { ok: true, changes, diagnostics };
540
+ return { ok: true, changes, diagnostics,
541
+ ...(allow.needsAction.length > 0 ? { needsAction: allow.needsAction } : {}) };
529
542
  }
530
543
 
531
544
  /**
@@ -560,6 +573,12 @@ export async function detectAntigravity(context) {
560
573
  const needsAction = [];
561
574
  const blocked = locationChoice(context);
562
575
  const imported = await exists(importedPluginPath(context.home));
576
+ // Read from files only, so it is known for a client ACC did not run. It is
577
+ // the client's own approval control over what a woken session can answer.
578
+ const commandApproval = await inspectAllowRule(context);
579
+ const approval = { commandApproval,
580
+ inboundDelivery: { state: commandApproval.state, diagnostic: commandApproval.diagnostic } };
581
+ diagnostics.push(commandApproval.diagnostic);
563
582
  // The installer runs every adapter's detect and passes a null version for a
564
583
  // client it did not find. Asking agy then would spawn a binary the installer
565
584
  // could not run a moment ago, or - where it does exist but did not answer -
@@ -568,7 +587,8 @@ export async function detectAntigravity(context) {
568
587
  diagnostics.push("acc hooks not registered: Antigravity CLI was not found, so it was "
569
588
  + "not asked what it has loaded");
570
589
  if (blocked !== null) diagnostics.push(blocked.reason);
571
- return { ok: true, changes: [], diagnostics, ...(blocked === null ? {} : { blocked }) };
590
+ return { ok: true, changes: [], diagnostics, ...approval,
591
+ ...(blocked === null ? {} : { blocked }) };
572
592
  }
573
593
  let readback = null;
574
594
  try {
@@ -622,11 +642,22 @@ export async function detectAntigravity(context) {
622
642
  + "first run and loads its skill, but none of its hook names, so no ACC hook runs here "
623
643
  + "(issue #176)");
624
644
  }
645
+ // Every install writes the rule, so where ACC's wrapper is on disk a missing
646
+ // or unreadable rule is an install to finish, whatever the delivery policy.
647
+ if (await exists(cliWrapperPath(context.home))) {
648
+ if (commandApproval.state === "prompts") {
649
+ needsAction.push(`acc install --adapter antigravity # ${commandApproval.rule} is missing `
650
+ + `from permissions.allow in ${commandApproval.file}, so each ACC command waits for `
651
+ + "approval");
652
+ } else if (commandApproval.state === "unreadable") {
653
+ needsAction.push(commandApproval.diagnostic);
654
+ }
655
+ }
625
656
  if (blocked !== null) {
626
657
  diagnostics.push(blocked.reason);
627
658
  needsAction.push(blocked.reason);
628
659
  }
629
- return { ok: true, changes: [], diagnostics,
660
+ return { ok: true, changes: [], diagnostics, ...approval,
630
661
  ...(blocked === null ? {} : { blocked }),
631
662
  ...(needsAction.length > 0 ? { needsAction } : {}) };
632
663
  }
@@ -678,8 +709,12 @@ export function planAntigravityInstall(context) {
678
709
  // installer leaves to the adapter - so the installer never deletes the copy
679
710
  // ahead of `agy plugin uninstall` and strands its manifest entry. That exact
680
711
  // ordering, with the shim directory, once left `{}` behind in the user's home.
712
+ // The operator's own settings, edited for ACC's allow rule wherever one can
713
+ // match: a wrapper path that needs quotes gets none.
681
714
  const plugin = [{ path: pluginInstallPath(context.home), kind: "merge" },
682
- { path: vendorManifestPath(context.home), kind: "merge" }];
715
+ { path: vendorManifestPath(context.home), kind: "merge" },
716
+ ...(isShellWord(cliWrapperPath(context.home))
717
+ ? [{ path: agySettingsPath(context.home), kind: "merge" }] : [])];
683
718
  if (locationChoice(context) !== null) {
684
719
  return [shim, ...plugin,
685
720
  { path: globalHooksPath(context.home), kind: "merge" },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -5,6 +5,8 @@ import certification from "../certification.json" with { type: "json" };
5
5
  import { denyOutcome, injectOutcome, injectStartOwnerOutcome, normalizeClaudeHook } from "./hooks.mjs";
6
6
  import { MIN_VERSION, PROTOCOL_CONTRACT, bindNativeSession, offerMessage, planNativeActivation,
7
7
  probeNativeDelivery, refreshNativeSession, retireNativeSession } from "./inbox-delivery.mjs";
8
+ import { observedInboxDirectories } from "./inbox-observed-directories.mjs";
9
+ import { inboxSocketDirectories } from "./inbox-socket-directories.mjs";
8
10
  import { planClaudeInstall, detectClaude, installClaudePlugin, uninstallClaudePlugin } from "./install.mjs";
9
11
 
10
12
  export const CLAUDE_CODE_VERSION = "2.1.233";
@@ -22,8 +24,9 @@ export const CLAUDE_DELIVERY_FALLBACK = Object.freeze({
22
24
  * and a parent/child mapping claimed without observation is the kind of thing
23
25
  * that quietly maps every child onto its parent. `startupInjection` and
24
26
  * `safePointInjection` were not exercised - only the before-turn path was.
25
- * Native live delivery is the inbox wake, certified from 2.1.282 on
26
- * darwin-arm64; reply routing through the transport is not claimed.
27
+ * Native live delivery is the inbox wake, certified from 2.1.282 (captured on
28
+ * darwin-arm64, applied on every platform the probe admits); reply routing
29
+ * through the transport is not claimed.
27
30
  */
28
31
  export function createClaudeCodeAdapter() {
29
32
  return defineAdapter({
@@ -52,8 +55,8 @@ export function createClaudeCodeAdapter() {
52
55
  // newer stable client is admitted by the probe and the per-session
53
56
  // handshake. The offer is a wake; see src/inbox-delivery.mjs.
54
57
  nativeDelivery: {
55
- minimumByPlatform: { "darwin-arm64": MIN_VERSION },
56
- anchors: [{ platform: "darwin-arm64", version: MIN_VERSION, protocolContract: PROTOCOL_CONTRACT }],
58
+ minimum: MIN_VERSION,
59
+ anchors: [{ version: MIN_VERSION, protocolContract: PROTOCOL_CONTRACT }],
57
60
  knownBad: [],
58
61
  activationKinds: ["native-service"],
59
62
  policySource: "installation-record",
@@ -91,6 +94,13 @@ export function createClaudeCodeAdapter() {
91
94
  refreshNativeSession,
92
95
  retireNativeSession,
93
96
  offerMessage,
97
+ // Where a sender connects to wake a session; a sandboxed sender is allowed these:
98
+ // the directories this environment implies, and those of every inbox bound here.
99
+ inboundSockets: context => [
100
+ ...inboxSocketDirectories({ env: context?.env, platform: context?.platform, uid: context?.uid }),
101
+ // Ownership is this process's own uid, never the context's naming override.
102
+ ...observedInboxDirectories({ stateRoot: context?.stateRoot }),
103
+ ],
94
104
 
95
105
  denyOutcome,
96
106
  injectOutcome,
@@ -1,4 +1,5 @@
1
1
  import net from "node:net";
2
+ import { compareVersionOrder, versionOrder } from "@agents-can-communicate/adapter-sdk";
2
3
 
3
4
  import { INBOX_MODES, MIN_VERSION, PROTOCOL_CONTRACT, TRANSPORT } from "./inbox-contract.mjs";
4
5
  import { claimWake, newEndpointId, readInboxEndpoint, releaseWake, removeInboxEndpoint,
@@ -26,7 +27,6 @@ const LEASE_MS = 120_000;
26
27
  const MESSAGE_ID = /^[A-Za-z0-9_-]{1,200}$/;
27
28
  const PROBE_NEEDLE = Buffer.from("messagingSocketPath");
28
29
  const PROBE_MAX_BYTES = 256 * 1024 * 1024;
29
- const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
30
30
 
31
31
  export { INBOX_MODES, MIN_VERSION, PROTOCOL_CONTRACT, TRANSPORT };
32
32
 
@@ -37,12 +37,11 @@ export const wakeText = messageId => `ACC: new peer message ${messageId} for thi
37
37
  + `acc inbox --message ${messageId}. Answer it through ACC with acc reply --message ${messageId}; `
38
38
  + "SendMessage cannot deliver to an ACC participant.";
39
39
 
40
- function compare(left, right) {
41
- const a = left.split("+")[0].split(".").map(Number);
42
- const b = right.split("+")[0].split(".").map(Number);
43
- for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
44
- return 0;
45
- }
40
+ // A version is judged by its release triple: a prerelease of a version above
41
+ // the minimum is not an older client, and the inbox in the executable is what
42
+ // admits it. An unreadable version orders as null.
43
+ const belowMinimum = clientVersion => compareVersionOrder(versionOrder(clientVersion),
44
+ versionOrder(MIN_VERSION)) < 0;
46
45
 
47
46
  // The registry field only a build with the inbox writes. Read-only, bounded.
48
47
  async function executableHasInbox(realExecutable) {
@@ -92,8 +91,8 @@ function withTimeout(work, ms) {
92
91
  }
93
92
 
94
93
  /**
95
- * Read-only: a stable version at or above the capture, a platform whose inbox
96
- * is a Unix socket, and an executable that writes the inbox into its session
94
+ * Read-only: a version at or above the capture, a platform whose inbox is a
95
+ * Unix socket, and an executable that writes the inbox into its session
97
96
  * registry. Never launches a session. Native Windows serves a named pipe that
98
97
  * demands an auth line; nothing there is captured, so it keeps hook delivery.
99
98
  */
@@ -105,10 +104,8 @@ export async function probeNativeDelivery({ realExecutable, timeoutMs = 750, pla
105
104
  if (typeof realExecutable !== "string" || realExecutable === "") return unsupported("feature_probe_failed");
106
105
  const clientVersion = await withTimeout(Promise.resolve(readVersion(realExecutable, timeoutMs)), timeoutMs)
107
106
  .catch(() => null);
108
- if (clientVersion === null || !STABLE_VERSION.test(clientVersion)) {
109
- return unsupported("feature_probe_failed", clientVersion);
110
- }
111
- if (compare(clientVersion, MIN_VERSION) < 0) return unsupported("below_minimum_version", clientVersion);
107
+ if (versionOrder(clientVersion) === null) return unsupported("feature_probe_failed", clientVersion);
108
+ if (belowMinimum(clientVersion)) return unsupported("below_minimum_version", clientVersion);
112
109
  const present = await withTimeout(Promise.resolve(hasInbox(realExecutable)), timeoutMs).catch(() => false);
113
110
  if (!present) return unsupported("protocol_mismatch", clientVersion);
114
111
  return { supported: true, clientVersion, protocolContract: PROTOCOL_CONTRACT,
@@ -145,13 +142,11 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
145
142
  if (typeof event?.sessionId !== "string" || event.sessionId === "") {
146
143
  return closed(clientVersion, "handshake_failed");
147
144
  }
148
- if (typeof clientVersion !== "string" || !STABLE_VERSION.test(clientVersion)) {
149
- return closed(clientVersion, "version_unavailable");
150
- }
145
+ if (versionOrder(clientVersion) === null) return closed(clientVersion, "version_unavailable");
151
146
  // The contract refuses it anyway, after this returns. Refusing here keeps an
152
147
  // older client - which binds again on every turn - from writing an endpoint
153
148
  // record each time.
154
- if (compare(clientVersion, MIN_VERSION) < 0) return closed(clientVersion, "below_minimum_version");
149
+ if (belowMinimum(clientVersion)) return closed(clientVersion, "below_minimum_version");
155
150
  const socketPath = env?.CLAUDE_CODE_MESSAGING_SOCKET;
156
151
  if (typeof socketPath !== "string" || socketPath === "") {
157
152
  return closed(clientVersion, "native_endpoint_unavailable");
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, readdir, realpath, rename, rm } from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { versionOrder } from "@agents-can-communicate/adapter-sdk";
5
6
 
6
7
  import { PROTOCOL_CONTRACT } from "./inbox-contract.mjs";
7
8
  import { inboxSocketIsSafe } from "./inbox-registry.mjs";
@@ -14,13 +15,14 @@ import { isReception } from "./inbox-settings.mjs";
14
15
  const KEYS = ["schemaVersion", "endpointId", "socketPath", "configDir", "clientPid", "sessionId",
15
16
  "clientVersion", "protocolContract", "leaseUntil", "reception"];
16
17
  const ENDPOINT = /^claude_inbox_[a-f0-9]{32}$/;
17
- const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
18
18
  const MAX_BYTES = 8_192;
19
19
  const absolute = value => typeof value === "string" && path.isAbsolute(value) && !value.includes("\0");
20
20
  const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
21
21
  const invalid = () => new Error("invalid Claude inbox endpoint registration");
22
22
 
23
23
  export const newEndpointId = () => `claude_inbox_${randomBytes(16).toString("hex")}`;
24
+ // Where, under a workspace runtime directory, the records live.
25
+ export const ENDPOINTS_DIRECTORY = "claude-inbox-endpoints";
24
26
 
25
27
  function valid(record) {
26
28
  return record !== null && typeof record === "object" && !Array.isArray(record)
@@ -29,7 +31,7 @@ function valid(record) {
29
31
  && absolute(record.socketPath) && absolute(record.configDir)
30
32
  && Number.isInteger(record.clientPid) && record.clientPid > 0
31
33
  && typeof record.sessionId === "string" && record.sessionId !== "" && record.sessionId.length <= 200
32
- && typeof record.clientVersion === "string" && STABLE.test(record.clientVersion)
34
+ && typeof record.clientVersion === "string" && versionOrder(record.clientVersion) !== null
33
35
  && record.protocolContract === PROTOCOL_CONTRACT
34
36
  && typeof record.leaseUntil === "string"
35
37
  && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(record.leaseUntil)
@@ -41,7 +43,7 @@ async function directory(runtimeDir, create = false) {
41
43
  if (!absolute(runtimeDir)) throw invalid();
42
44
  if (create) await mkdir(runtimeDir, { recursive: true, mode: 0o700 });
43
45
  const root = await realpath(runtimeDir);
44
- const dir = path.join(root, "claude-inbox-endpoints");
46
+ const dir = path.join(root, ENDPOINTS_DIRECTORY);
45
47
  if (create) await mkdir(dir, { mode: 0o700 }).catch(error => {
46
48
  if (error.code !== "EEXIST") throw error;
47
49
  });
@@ -0,0 +1,62 @@
1
+ import { closeSync, constants, fstatSync, openSync, readdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { ENDPOINTS_DIRECTORY } from "./inbox-endpoint.mjs";
5
+
6
+ // The inbox directories of Claude Code sessions ACC has bound on this machine,
7
+ // read from the adapter's own endpoint records in every workspace under ACC's
8
+ // state root (review of #217). The environment-derived directories cover
9
+ // sessions started from the installer's environment; a session started with
10
+ // its own absolute CLAUDE_CODE_TMPDIR or XDG_RUNTIME_DIR binds elsewhere, and
11
+ // the hook that bound it wrote down where.
12
+ //
13
+ // Synchronous: install, doctor and the managed refresh compose the grant list
14
+ // while building one context, once per command. Every workspace and every
15
+ // matching record is read - a count cap dropped whatever sorted past it, and
16
+ // doctor then called the profile configured (review of #217) - and each read
17
+ // is bounded to one small record. Nothing here throws; a record that cannot be
18
+ // read or cannot be a Claude Code inbox adds nothing. A grant widens the Codex sandbox, so a directory is taken only when
19
+ // it is named as Claude Code names its inbox directories (2.1.283:
20
+ // `cc-socks` or `cc-socks-<uid>`), whatever a file on disk says.
21
+
22
+ const RECORD = /^claude_inbox_[a-f0-9]{32}\.json$/;
23
+ const INBOX_DIRECTORY = /^cc-socks(?:-\d+)?$/;
24
+ const MAX_BYTES = 8_192;
25
+
26
+ const list = directory => {
27
+ try {
28
+ return readdirSync(directory).sort();
29
+ } catch {
30
+ return [];
31
+ }
32
+ };
33
+
34
+ function socketPathIn(file, uid) {
35
+ let fd;
36
+ try {
37
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
38
+ const info = fstatSync(fd);
39
+ if (!info.isFile() || (uid !== null && info.uid !== uid) || info.size > MAX_BYTES) return null;
40
+ return JSON.parse(readFileSync(fd, "utf8"))?.socketPath ?? null;
41
+ } catch {
42
+ return null;
43
+ } finally {
44
+ if (fd !== undefined) try { closeSync(fd); } catch { /* already closed */ }
45
+ }
46
+ }
47
+
48
+ export function observedInboxDirectories({ stateRoot, uid = process.getuid?.() ?? null } = {}) {
49
+ if (typeof stateRoot !== "string" || !path.isAbsolute(stateRoot)) return [];
50
+ const workspaces = path.join(stateRoot, "workspaces");
51
+ const found = new Set();
52
+ for (const workspace of list(workspaces)) {
53
+ const records = path.join(workspaces, workspace, ENDPOINTS_DIRECTORY);
54
+ for (const name of list(records).filter(item => RECORD.test(item))) {
55
+ const socketPath = socketPathIn(path.join(records, name), uid);
56
+ if (typeof socketPath !== "string" || !path.isAbsolute(socketPath) || socketPath.includes("\0")) continue;
57
+ const directory = path.dirname(path.normalize(socketPath));
58
+ if (INBOX_DIRECTORY.test(path.basename(directory))) found.add(directory);
59
+ }
60
+ }
61
+ return [...found].sort();
62
+ }