agents-can-communicate 0.8.5 → 0.9.0

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 (133) hide show
  1. package/README.md +11 -7
  2. package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
  3. package/bin/entrypoints/acc-hook.mjs +33 -19
  4. package/bin/entrypoints/hook-output.mjs +14 -5
  5. package/docs/ADAPTER_AUTHORING.md +29 -0
  6. package/docs/CAPABILITIES.md +7 -6
  7. package/docs/CLI.md +5 -0
  8. package/docs/CONFIGURATION.md +15 -10
  9. package/docs/DESIGN_DECISIONS.md +0 -2
  10. package/docs/GETTING_STARTED.md +39 -6
  11. package/docs/HOW_IT_WORKS.md +13 -8
  12. package/docs/SECURITY_MODEL.md +26 -5
  13. package/docs/TROUBLESHOOTING.md +63 -8
  14. package/docs/UPGRADING.md +33 -5
  15. package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -1
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +13 -2
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
  42. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
  43. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  44. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
  45. package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +18 -9
  48. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
  49. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
  50. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
  51. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
  56. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  57. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
  58. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
  60. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
  61. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  62. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
  63. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
  64. package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
  65. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
  66. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
  67. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
  68. package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
  69. package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
  70. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
  73. package/node_modules/@agents-can-communicate/cli/package.json +2 -1
  74. package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
  75. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
  76. package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
  77. package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
  78. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
  79. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
  80. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
  81. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
  82. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
  83. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
  84. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
  85. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
  86. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
  87. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
  88. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
  89. package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
  90. package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
  91. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
  92. package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
  93. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
  95. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
  96. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
  97. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
  98. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  99. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  100. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
  101. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
  102. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
  103. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
  104. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
  105. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
  106. package/node_modules/@agents-can-communicate/installer/package.json +3 -2
  107. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
  108. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
  109. package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
  110. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
  111. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
  112. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  113. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  114. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +6 -2
  115. package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
  116. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  117. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
  118. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
  119. package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
  120. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
  121. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
  122. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
  123. package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
  124. package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
  129. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
  130. package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
  131. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
  132. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
  133. package/package.json +1 -5
@@ -1,13 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
- import { execFile } from "node:child_process";
3
2
  import { constants } from "node:fs";
4
- import { lstat, mkdir, open, rm } from "node:fs/promises";
3
+ import { lstat, mkdir, open, readdir, rm } from "node:fs/promises";
5
4
  import net from "node:net";
6
5
  import path from "node:path";
7
6
 
8
- import { decisionBody } from "@agents-can-communicate/adapter-sdk";
7
+ import { decisionBody, isWindowsPlatform, readProcessArgs, resolveExecutable, runExecutable }
8
+ from "@agents-can-communicate/adapter-sdk";
9
9
 
10
- import { PROTOCOL_CONTRACT, RELAY_MODES, listRegistrations, readRegistration, relayDir }
10
+ import { isAgy } from "./client-process.mjs";
11
+ import { bindDesktop, offerDesktop, refreshDesktop } from "./desktop-delivery.mjs";
12
+ import { isDesktopEndpointId } from "./desktop-endpoint.mjs";
13
+ import { relayStartCommand } from "./relays.mjs";
14
+ import { PROTOCOL_CONTRACT, RELAY_MODES, listRegistrations, readRegistration, relayDir, relayPipeName }
11
15
  from "./relay-endpoint.mjs";
12
16
 
13
17
  /**
@@ -18,19 +22,31 @@ import { PROTOCOL_CONTRACT, RELAY_MODES, listRegistrations, readRegistration, re
18
22
  */
19
23
  const TRANSPORT = "antigravity-relay";
20
24
  const defaultAlive = pid => { try { process.kill(pid, 0); return true; } catch { return false; } };
21
- const runDefault = (command, args, { timeout }) => new Promise(resolve => {
22
- execFile(command, args, { timeout, windowsHide: true },
23
- (_error, stdout, stderr) => resolve({ stdout: `${stdout ?? ""}${stderr ?? ""}` }));
24
- });
25
- const argvDefault = async pid => (await runDefault("ps", ["-o", "args=", "-p", String(pid)],
26
- { timeout: 1_000 })).stdout.trim().split(/\s+/).filter(Boolean);
25
+ // agy is found the way the platform finds it, and a .cmd starts through cmd.exe.
26
+ const runDefault = async (command, args, { timeout }) => {
27
+ const file = await resolveExecutable(command) ?? command;
28
+ return runExecutable(file, args, { timeout }).then(
29
+ ({ stdout, stderr }) => ({ stdout: `${stdout ?? ""}${stderr ?? ""}` }),
30
+ error => ({ stdout: `${error?.stdout ?? ""}${error?.stderr ?? ""}` }));
31
+ };
32
+ const argvDefault = async pid => await readProcessArgs(pid, { timeoutMs: 1_000 }) ?? [];
27
33
 
28
34
  export function isPrintMode(argv) {
29
35
  return argv.some(arg => arg === "-p" || arg === "--print" || arg.startsWith("--print=")
30
36
  || arg.startsWith("-p="));
31
37
  }
32
38
 
33
- export async function isSocketSafe(socketPath) {
39
+ const listPipesOfMachine = () => readdir("\\\\.\\pipe\\");
40
+
41
+ /** A relay socket only this user can open; on Windows a relay pipe the machine lists. */
42
+ export async function isSocketSafe(socketPath, { platform = process.platform,
43
+ listPipes = listPipesOfMachine } = {}) {
44
+ if (isWindowsPlatform(platform)) {
45
+ const name = relayPipeName(socketPath);
46
+ if (name === null) return false;
47
+ const pipes = await Promise.resolve().then(listPipes).catch(() => []);
48
+ return pipes.some(pipe => String(pipe).toLowerCase() === name);
49
+ }
34
50
  try {
35
51
  const info = await lstat(socketPath);
36
52
  return info.isSocket() && (typeof process.getuid !== "function" || info.uid === process.getuid())
@@ -63,12 +79,12 @@ const closedHandshake = (clientVersion, reasonCode) => ({ supported: false,
63
79
  clientVersion: clientVersion ?? null, protocolContract: PROTOCOL_CONTRACT, modes: [],
64
80
  opaqueEndpointRef: null, leaseUntil: null, reasonCode });
65
81
 
66
- async function serving(record, { timeoutMs, isAlive, clientPid }) {
82
+ async function serving(record, { timeoutMs, isAlive, clientPid, platform = process.platform }) {
67
83
  if (record === null || record.protocolContract !== PROTOCOL_CONTRACT) return "native_session_unavailable";
68
84
  if (clientPid !== undefined && record.agyPid !== clientPid) return "native_session_unavailable";
69
85
  if (!isAlive(record.agyPid) || !isAlive(record.relayPid)) return "native_session_unavailable";
70
86
  if (Date.parse(record.leaseUntil) <= Date.now()) return "native_session_unavailable";
71
- if (!await isSocketSafe(record.socketPath)) return "native_session_unavailable";
87
+ if (!await isSocketSafe(record.socketPath, { platform })) return "native_session_unavailable";
72
88
  try {
73
89
  const pong = await askRelay(record.socketPath, { nonce: record.nonce, ping: true }, timeoutMs);
74
90
  return pong?.accepted === true && pong.ping === true ? null : "handshake_failed";
@@ -81,7 +97,8 @@ const handshake = record => ({ supported: true, clientVersion: record.clientVers
81
97
  protocolContract: PROTOCOL_CONTRACT, modes: [...RELAY_MODES],
82
98
  opaqueEndpointRef: record.endpointId, leaseUntil: record.leaseUntil, reasonCode: null });
83
99
 
84
- export async function probeNativeDelivery({ timeoutMs = 750, run = runDefault } = {}) {
100
+ export async function probeNativeDelivery({ timeoutMs = 750, run = runDefault,
101
+ platform = process.platform } = {}) {
85
102
  const unsupported = (reasonCode, clientVersion = null) => ({ supported: false, clientVersion,
86
103
  protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
87
104
  try {
@@ -103,18 +120,22 @@ export function planNativeActivation() {
103
120
  }
104
121
 
105
122
  export async function bindNativeSession({ event, clientPid, clientVersion, runtimeDir,
106
- timeoutMs = 750, isAlive = defaultAlive } = {}) {
123
+ timeoutMs = 750, isAlive = defaultAlive, platform = process.platform, desktop = bindDesktop } = {}) {
107
124
  if (!Number.isInteger(clientPid) || clientPid <= 0) return closedHandshake(clientVersion, "client_process_unknown");
108
- const candidates = (await listRegistrations({ runtimeDir }))
125
+ // The desktop app's own language server: no relay, the server is the endpoint.
126
+ const served = await desktop({ event, clientPid, clientVersion, runtimeDir, timeoutMs });
127
+ if (served !== null) return served;
128
+ const candidates = (await listRegistrations({ runtimeDir, platform }))
109
129
  .filter(record => record.conversationId === event?.sessionId);
110
130
  for (const record of candidates) {
111
- if (await serving(record, { timeoutMs, isAlive, clientPid }) === null) return handshake(record);
131
+ if (await serving(record, { timeoutMs, isAlive, clientPid, platform }) === null) return handshake(record);
112
132
  }
113
133
  return closedHandshake(clientVersion, "native_session_unavailable");
114
134
  }
115
135
 
116
136
  export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 750,
117
137
  isAlive = defaultAlive } = {}) {
138
+ if (isDesktopEndpointId(binding?.opaqueEndpointRef)) return refreshDesktop({ binding, runtimeDir, timeoutMs });
118
139
  const record = await readRegistration({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
119
140
  const reasonCode = await serving(record, { timeoutMs, isAlive });
120
141
  return reasonCode === null ? handshake(record) : closedHandshake(binding?.clientVersion, reasonCode);
@@ -124,6 +145,9 @@ const SAFE_CODES = new Map([["recipient_unavailable", "recipient_unavailable"],
124
145
  ["transport_rejected", "transport_rejected"], ["bad_nonce", "recipient_unavailable"]]);
125
146
 
126
147
  export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 8_000 } = {}) {
148
+ if (isDesktopEndpointId(binding?.opaqueEndpointRef)) {
149
+ return offerDesktop({ binding, message, runtimeDir, timeoutMs });
150
+ }
127
151
  const rejected = safeErrorCode => ({ accepted: false, transport: TRANSPORT,
128
152
  clientVersion: binding?.clientVersion ?? null, safeErrorCode });
129
153
  const record = await readRegistration({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
@@ -146,9 +170,8 @@ export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 8
146
170
  }
147
171
 
148
172
  const ASK_LIMIT = 3;
149
- const HINT = home => "ACC: live delivery is on but not running in this conversation. To let "
150
- + `peers reach you while idle, run once: sh "${path.join(home, ".gemini", "config", "acc",
151
- "acc-relay.sh")}" start`;
173
+ const HINT = (home, platform) => "ACC: live delivery is on but not running in this conversation. "
174
+ + `To let peers reach you while idle, run once: ${relayStartCommand(home, platform)}`;
152
175
 
153
176
  // Each ask is its own file, created exclusively. A counter read and then
154
177
  // rewritten lets overlapping calls all observe the same count and each return
@@ -191,15 +214,20 @@ async function relayAlreadyServing({ runtimeDir, conversationId, clientPid, isAl
191
214
  }
192
215
 
193
216
  export async function nativeActivationHint({ event, nativeBinding, runtimeDir, clientPid, env,
194
- argvOf = argvDefault, isAlive = defaultAlive }) {
217
+ argvOf = argvDefault, isAlive = defaultAlive, platform = process.platform }) {
195
218
  // Only a degraded binding can be helped by starting a relay: "off" means the
196
219
  // live policy is off, "unsupported" that the contract does not admit this
197
220
  // client, and "active" that a relay already serves it. Those calls spend
198
221
  // none of the asks recorded below.
199
222
  if (nativeBinding?.state !== "degraded") return null;
200
- const home = env?.HOME;
223
+ // Windows keeps the profile in USERPROFILE; HOME is set only by Git Bash.
224
+ const home = isWindowsPlatform(platform) ? env?.USERPROFILE ?? env?.HOME : env?.HOME;
201
225
  if (typeof event?.sessionId !== "string" || typeof home !== "string" || home === "") return null;
202
- if (!Number.isInteger(clientPid) || isPrintMode(await argvOf(clientPid).catch(() => []))) return null;
226
+ if (!Number.isInteger(clientPid)) return null;
227
+ // The relay is Antigravity CLI's: the desktop app runs no agy, and its
228
+ // sandbox lets no relay start. A print-mode agy ends with its turn.
229
+ const argv = await argvOf(clientPid).catch(() => []);
230
+ if (!isAgy(argv) || isPrintMode(argv)) return null;
203
231
  // The runner asks only when its own handshake failed. A relay can still be
204
232
  // serving under a stale degraded binding; telling the agent to start it again
205
233
  // would be wrong, and the check does not spend an ask.
@@ -212,5 +240,14 @@ export async function nativeActivationHint({ event, nativeBinding, runtimeDir, c
212
240
  // the number now; release gives it back when the runner does not deliver.
213
241
  const slot = await claimAsk(asked, marker);
214
242
  if (slot === null) return null;
215
- return { line: HINT(home), release: () => releaseAsk(slot) };
243
+ let line;
244
+ try {
245
+ line = HINT(home, platform);
246
+ } catch {
247
+ // A profile path no shell reads alike gets no command to run; the message
248
+ // still arrives on the next turn.
249
+ await releaseAsk(slot);
250
+ return null;
251
+ }
252
+ return { line, release: () => releaseAsk(slot) };
216
253
  }
@@ -1,8 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { constants } from "node:fs";
3
2
  import { lstat, mkdir, open, readdir, realpath, rename, rm } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
 
5
+ import { closedTo, isWindowsPlatform, openRegularNoFollow } from "@agents-can-communicate/adapter-sdk";
6
+
6
7
  /**
7
8
  * Where a running relay is found, and what it may be trusted with.
8
9
  *
@@ -11,6 +12,10 @@ import path from "node:path";
11
12
  * session endpoint - the language-server address and the CSRF token - is never
12
13
  * part of it. A key outside the closed list below is refused, so a mistake that
13
14
  * tried to persist one fails loudly instead of quietly writing a credential.
15
+ *
16
+ * On Windows the relay listens on a named pipe, `\\.\pipe\acc-relay-<32 hex>`,
17
+ * and the record and its directory carry no mode bits: the profile's ACL keeps
18
+ * them to this user.
14
19
  */
15
20
  export const PROTOCOL_CONTRACT = "antigravity-agentapi-relay-v1";
16
21
  export const RELAY_MODES = Object.freeze(["livePush", "idleWake", "busyQueue"]);
@@ -23,42 +28,46 @@ const NONCE = /^[a-f0-9]{64}$/;
23
28
  const VERSION = /^\d+\.\d+\.\d+$/;
24
29
  const LEASE = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/;
25
30
  const MAX_BYTES = 4_096;
31
+ const RELAY_PIPE = /^\\\\\.\\pipe\\(acc-relay-[0-9a-f]{32})$/;
26
32
  const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
27
33
  const pid = value => Number.isInteger(value) && value > 0;
28
34
  const invalid = () => new Error("invalid Antigravity relay registration");
29
35
 
30
36
  export const relayDir = runtimeDir => path.join(runtimeDir, "native", "antigravity");
31
37
  export const newRelayId = () => `antigravity_relay_${randomBytes(16).toString("hex")}`;
38
+ /** The pipe name a relay pipe path carries, or null. */
39
+ export const relayPipeName = value => (typeof value === "string" ? RELAY_PIPE.exec(value)?.[1] ?? null : null);
40
+ const socketPathValid = (value, platform) => (isWindowsPlatform(platform) ? relayPipeName(value) !== null
41
+ : typeof value === "string" && path.isAbsolute(value) && Buffer.byteLength(value) < 104);
32
42
 
33
- export function validRegistration(record) {
43
+ export function validRegistration(record, { platform = process.platform } = {}) {
34
44
  return record !== null && typeof record === "object" && !Array.isArray(record)
35
45
  && Object.keys(record).length === KEYS.length && KEYS.every(key => Object.hasOwn(record, key))
36
46
  && record.schemaVersion === 1 && RELAY_ID.test(record.endpointId)
37
47
  && CONVERSATION.test(record.conversationId) && pid(record.agyPid) && pid(record.relayPid)
38
- && typeof record.socketPath === "string" && path.isAbsolute(record.socketPath)
39
- && Buffer.byteLength(record.socketPath) < 104 && NONCE.test(record.nonce)
48
+ && socketPathValid(record.socketPath, platform) && NONCE.test(record.nonce)
40
49
  && VERSION.test(record.clientVersion) && record.protocolContract === PROTOCOL_CONTRACT
41
50
  && Array.isArray(record.modes) && record.modes.every(mode => RELAY_MODES.includes(mode))
42
51
  && typeof record.leaseUntil === "string" && LEASE.test(record.leaseUntil)
43
52
  && Number.isFinite(Date.parse(record.leaseUntil));
44
53
  }
45
54
 
46
- async function directory(runtimeDir, create = false) {
55
+ async function directory(runtimeDir, create = false, platform = process.platform) {
47
56
  if (typeof runtimeDir !== "string" || !path.isAbsolute(runtimeDir)) throw invalid();
48
57
  const dir = relayDir(await realpath(runtimeDir));
49
58
  if (create) await mkdir(dir, { recursive: true, mode: 0o700 });
50
59
  const info = await lstat(dir);
51
- if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || (info.mode & 0o077) !== 0) {
60
+ if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || !closedTo(info, 0o077, { platform })) {
52
61
  throw invalid();
53
62
  }
54
63
  return dir;
55
64
  }
56
65
 
57
- export async function writeRegistration({ runtimeDir, record }) {
58
- if (!validRegistration(record)) throw invalid();
66
+ export async function writeRegistration({ runtimeDir, record, platform = process.platform }) {
67
+ if (!validRegistration(record, { platform })) throw invalid();
59
68
  const text = `${JSON.stringify(record)}\n`;
60
69
  if (Buffer.byteLength(text) > MAX_BYTES) throw invalid();
61
- const dir = await directory(runtimeDir, true);
70
+ const dir = await directory(runtimeDir, true, platform);
62
71
  const file = path.join(dir, `${record.endpointId}.json`);
63
72
  const temporary = path.join(dir, `.${record.endpointId}.${randomBytes(8).toString("hex")}.tmp`);
64
73
  let handle;
@@ -74,17 +83,18 @@ export async function writeRegistration({ runtimeDir, record }) {
74
83
  return file;
75
84
  }
76
85
 
77
- export async function readRegistration({ runtimeDir, endpointId }) {
86
+ export async function readRegistration({ runtimeDir, endpointId, platform = process.platform }) {
78
87
  if (typeof endpointId !== "string" || !RELAY_ID.test(endpointId)) return null;
79
88
  let handle;
80
89
  try {
81
- const dir = await directory(runtimeDir);
82
- handle = await open(path.join(dir, `${endpointId}.json`),
83
- constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
90
+ const dir = await directory(runtimeDir, false, platform);
91
+ handle = await openRegularNoFollow(path.join(dir, `${endpointId}.json`), undefined, { platform });
84
92
  const info = await handle.stat();
85
- if (!info.isFile() || !own(info) || info.size > MAX_BYTES || (info.mode & 0o077) !== 0) return null;
93
+ if (!info.isFile() || !own(info) || info.size > MAX_BYTES || !closedTo(info, 0o077, { platform })) {
94
+ return null;
95
+ }
86
96
  const record = JSON.parse(await handle.readFile("utf8"));
87
- return validRegistration(record) && record.endpointId === endpointId ? record : null;
97
+ return validRegistration(record, { platform }) && record.endpointId === endpointId ? record : null;
88
98
  } catch {
89
99
  return null;
90
100
  } finally {
@@ -92,25 +102,25 @@ export async function readRegistration({ runtimeDir, endpointId }) {
92
102
  }
93
103
  }
94
104
 
95
- export async function listRegistrations({ runtimeDir }) {
105
+ export async function listRegistrations({ runtimeDir, platform = process.platform }) {
96
106
  let names;
97
107
  try {
98
- names = await readdir(await directory(runtimeDir));
108
+ names = await readdir(await directory(runtimeDir, false, platform));
99
109
  } catch {
100
110
  return [];
101
111
  }
102
112
  const found = [];
103
113
  for (const name of names) {
104
114
  if (!name.endsWith(".json")) continue;
105
- const record = await readRegistration({ runtimeDir, endpointId: name.slice(0, -5) });
115
+ const record = await readRegistration({ runtimeDir, endpointId: name.slice(0, -5), platform });
106
116
  if (record !== null) found.push(record);
107
117
  }
108
118
  return found;
109
119
  }
110
120
 
111
- export async function removeRegistration({ runtimeDir, endpointId }) {
121
+ export async function removeRegistration({ runtimeDir, endpointId, platform = process.platform }) {
112
122
  if (typeof endpointId !== "string" || !RELAY_ID.test(endpointId)) return;
113
123
  try {
114
- await rm(path.join(await directory(runtimeDir), `${endpointId}.json`), { force: true });
124
+ await rm(path.join(await directory(runtimeDir, false, platform), `${endpointId}.json`), { force: true });
115
125
  } catch { /* retirement is already decided; cleanup is best effort */ }
116
126
  }
@@ -86,7 +86,8 @@ export function spawnRelay({ command, args, env, payload, readyMs = 10_000, spaw
86
86
  return new Promise(resolve => {
87
87
  let child;
88
88
  try {
89
- child = spawn(command, args, { env, detached: true, stdio: ["pipe", "pipe", "ignore"] });
89
+ child = spawn(command, args, { env, detached: true, windowsHide: true,
90
+ stdio: ["pipe", "pipe", "ignore"] });
90
91
  } catch {
91
92
  resolve({ ok: false, reason: "could not start" });
92
93
  return;
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
3
3
  import net from "node:net";
4
4
  import path from "node:path";
5
5
 
6
- import { channelSocketDirectory } from "@agents-can-communicate/adapter-sdk";
6
+ import { channelSocketDirectory, isWindowsPlatform } from "@agents-can-communicate/adapter-sdk";
7
7
 
8
8
  import { PROTOCOL_CONTRACT, RELAY_MODES, newRelayId, removeRegistration, writeRegistration }
9
9
  from "./relay-endpoint.mjs";
@@ -52,13 +52,17 @@ export function createRelay({ runtimeDir, socketDir = channelSocketDirectory(),
52
52
  leaseMs = 120_000,
53
53
  renewMs = 40_000, pidCheckMs = 5_000, probeMs = 60_000, lifetimeMs = 86_400_000,
54
54
  budgetBytes = 6_000, refreshBinding = null, onExit = () => {}, observe = () => {},
55
- now = Date.now }) {
55
+ now = Date.now, platform = process.platform }) {
56
56
  // The binary picks the id first, so its log file can be named before the
57
57
  // relay exists; tests let the relay choose.
58
58
  const endpointId = requestedId ?? newRelayId();
59
59
  const nonce = randomBytes(32).toString("hex");
60
- const socketPath = path.join(socketDir, `r${randomBytes(6).toString("hex")}.sock`);
61
- if (Buffer.byteLength(socketPath) >= 104) throw new Error("relay socket path is too long");
60
+ // Windows: a named pipe whose 128 random bits only the private registration
61
+ // records; libuv creates the first instance, so no one can take the name first.
62
+ const windows = isWindowsPlatform(platform);
63
+ const socketPath = windows ? `\\\\.\\pipe\\acc-relay-${randomBytes(16).toString("hex")}`
64
+ : path.join(socketDir, `r${randomBytes(6).toString("hex")}.sock`);
65
+ if (!windows && Buffer.byteLength(socketPath) >= 104) throw new Error("relay socket path is too long");
62
66
  const seen = new Set();
63
67
  const connections = new Set();
64
68
  const timers = [];
@@ -140,7 +144,7 @@ export function createRelay({ runtimeDir, socketDir = channelSocketDirectory(),
140
144
  async function renew() {
141
145
  if (closed) return;
142
146
  const next = record();
143
- await writeRegistration({ runtimeDir, record: next });
147
+ await writeRegistration({ runtimeDir, record: next, platform });
144
148
  if (refreshBinding !== null) await Promise.resolve(refreshBinding(next.leaseUntil)).catch(() => {});
145
149
  }
146
150
 
@@ -150,8 +154,9 @@ export function createRelay({ runtimeDir, socketDir = channelSocketDirectory(),
150
154
  for (const timer of timers) clearInterval(timer);
151
155
  for (const socket of connections) socket.destroy();
152
156
  if (server !== null) await new Promise(resolve => server.close(() => resolve()));
153
- try { if (existsSync(socketPath)) unlinkSync(socketPath); } catch { /* already gone */ }
154
- await removeRegistration({ runtimeDir, endpointId });
157
+ // A pipe goes with its server; a socket file stays behind until removed.
158
+ try { if (!windows && existsSync(socketPath)) unlinkSync(socketPath); } catch { /* already gone */ }
159
+ await removeRegistration({ runtimeDir, endpointId, platform });
155
160
  observe({ event: "relay_closed", reasonCode: reason });
156
161
  onExit(reason);
157
162
  }
@@ -163,18 +168,21 @@ export function createRelay({ runtimeDir, socketDir = channelSocketDirectory(),
163
168
  }
164
169
 
165
170
  async function listen() {
166
- mkdirSync(socketDir, { recursive: true, mode: 0o700 });
167
- chmodSync(socketDir, 0o700);
168
- if (existsSync(socketPath)) throw new Error("relay socket already exists");
171
+ if (!windows) {
172
+ mkdirSync(socketDir, { recursive: true, mode: 0o700 });
173
+ chmodSync(socketDir, 0o700);
174
+ if (existsSync(socketPath)) throw new Error("relay socket already exists");
175
+ }
169
176
  server = net.createServer(handleConnection);
170
177
  await new Promise((resolve, reject) => {
171
178
  server.once("error", reject);
179
+ if (windows) { server.listen(socketPath, resolve); return; }
172
180
  const previous = process.umask(0o177);
173
181
  server.listen(socketPath, () => { process.umask(previous); resolve(); });
174
182
  });
175
- chmodSync(socketPath, 0o600);
183
+ if (!windows) chmodSync(socketPath, 0o600);
176
184
  const first = record();
177
- await writeRegistration({ runtimeDir, record: first });
185
+ await writeRegistration({ runtimeDir, record: first, platform });
178
186
  const started = now();
179
187
  every(renewMs, renew);
180
188
  every(pidCheckMs, () => { if (!isAlive(agyPid)) return close("client_exited"); });
@@ -1,7 +1,9 @@
1
- import { execFile } from "node:child_process";
2
1
  import { readdir, rm } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
 
4
+ import { isWindowsPlatform, readProcessArgs, shellQuote, windowsHookCommand }
5
+ from "@agents-can-communicate/adapter-sdk";
6
+
5
7
  import { listRegistrations, relayDir, removeRegistration } from "./relay-endpoint.mjs";
6
8
 
7
9
  /**
@@ -16,10 +18,22 @@ import { listRegistrations, relayDir, removeRegistration } from "./relay-endpoin
16
18
  const RELAY_MARK = "acc-antigravity-relay";
17
19
  const RELAY_LOG = /^antigravity_relay_[a-f0-9]{32}\.log$/;
18
20
  const defaultAlive = pid => { try { process.kill(pid, 0); return true; } catch { return false; } };
19
- const psArgv = pid => new Promise(resolve => execFile("ps", ["-o", "args=", "-p", String(pid)],
20
- { timeout: 1_000 }, (_error, out) => resolve(String(out ?? "").trim().split(/\s+/).filter(Boolean))));
21
+ const psArgv = async pid => await readProcessArgs(pid, { timeoutMs: 1_000 }) ?? [];
22
+
23
+ // Windows runs no `sh`: there the shim is a Node script, started with node on a
24
+ // forward-slash path that PowerShell, cmd and Git Bash read alike.
25
+ export const relayShimName = platform => (isWindowsPlatform(platform) ? "acc-relay.mjs" : "acc-relay.sh");
26
+
27
+ // The path in the platform's own spelling, for the command the agent is told to
28
+ // run. The file itself is written where this host spells the same path.
29
+ export const relayShimPath = (home, platform = process.platform) => (isWindowsPlatform(platform)
30
+ ? path.win32.join(home, ".gemini", "config", "acc", relayShimName(platform))
31
+ : path.posix.join(home, ".gemini", "config", "acc", relayShimName(platform)));
21
32
 
22
- export const relayShimPath = home => path.join(home, ".gemini", "config", "acc", "acc-relay.sh");
33
+ /** The command the agent runs once per conversation to start live delivery. */
34
+ export const relayStartCommand = (home, platform = process.platform) => (isWindowsPlatform(platform)
35
+ ? windowsHookCommand("portable", { node: "node", shim: relayShimPath(home, platform), args: ["start"] })
36
+ : `sh ${shellQuote(relayShimPath(home, platform))} start`);
23
37
 
24
38
  async function workspaceDirs(dataHome, list = readdir) {
25
39
  if (typeof dataHome !== "string" || dataHome === "") return [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -6,6 +6,7 @@ import { denyOutcome, injectOutcome, injectStartOwnerOutcome, normalizeClaudeHoo
6
6
  import { MIN_VERSION, PROTOCOL_CONTRACT, bindNativeSession, offerMessage, planNativeActivation,
7
7
  probeNativeDelivery, refreshNativeSession, retireNativeSession } from "./inbox-delivery.mjs";
8
8
  import { observedInboxDirectories } from "./inbox-observed-directories.mjs";
9
+ import { claudeConfigDir, readSessionRecord } from "./inbox-registry.mjs";
9
10
  import { inboxSocketDirectories } from "./inbox-socket-directories.mjs";
10
11
  import { planClaudeInstall, detectClaude, installClaudePlugin, uninstallClaudePlugin } from "./install.mjs";
11
12
 
@@ -36,6 +37,11 @@ export function createClaudeCodeAdapter() {
36
37
  // decide whether the client is on this machine, so it has to be the
37
38
  // real command rather than the adapter id: `2.1.233 (Claude Code)`.
38
39
  client: { command: "claude", certificationName: "claude-code", versionArgs: ["--version"] },
40
+ // The build the session runs. Claude Code updates itself under running
41
+ // sessions and Claude.app runs a build of its own, so the `claude` on PATH
42
+ // can name another version; each session's registry record names its own.
43
+ clientVersionOf: async ({ pid, env }) => (await readSessionRecord({
44
+ configDir: claudeConfigDir(env), clientPid: pid }))?.version ?? null,
39
45
  certification,
40
46
  deliveryFallback: CLAUDE_DELIVERY_FALLBACK,
41
47
  capabilities: {