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,10 +1,12 @@
1
1
  import net from "node:net";
2
- import { compareVersionOrder, versionOrder } from "@agents-can-communicate/adapter-sdk";
2
+ import { compareVersionOrder, isWindowsPlatform, npmShimTarget, runExecutable, versionOrder }
3
+ from "@agents-can-communicate/adapter-sdk";
3
4
 
4
5
  import { INBOX_MODES, MIN_VERSION, PROTOCOL_CONTRACT, TRANSPORT } from "./inbox-contract.mjs";
5
6
  import { claimWake, newEndpointId, readInboxEndpoint, releaseWake, removeInboxEndpoint,
6
7
  sweepDeadEndpoints, writeInboxEndpoint } from "./inbox-endpoint.mjs";
7
- import { claudeConfigDir, readSessionRecord, verifyInbox } from "./inbox-registry.mjs";
8
+ import { claudeConfigDir, listPipesOfMachine, readPeerKey, readSessionRecord, verifyInbox }
9
+ from "./inbox-registry.mjs";
8
10
  import { MANAGED_SETTINGS, permissionModeFromArgs, readInboundSettings, readProcessArgs, receptionOf }
9
11
  from "./inbox-settings.mjs";
10
12
 
@@ -19,9 +21,12 @@ import { MANAGED_SETTINGS, permissionModeFromArgs, readInboundSettings, readProc
19
21
  // Claude Code frames every inbox message as a teammate's request to act on,
20
22
  // and the only thing a wake gives it to act on is ACC's own notice.
21
23
  //
22
- // The connection sends no auth line, and this module never reads the
23
- // session's messaging token or key file. A frame carrying that token would
24
- // pass as the session's own child and skip the inbound controls its user set.
24
+ // On POSIX the connection sends no auth line: the socket is this user's alone.
25
+ // Windows refuses a frame without one (measured on 2.1.286), so there the
26
+ // offer authenticates with the peer key Claude publishes for other sessions of
27
+ // the same user, and the wake is classified a peer's. This module never reads
28
+ // the session's own messaging token: a frame carrying it would pass as the
29
+ // session's child and skip the inbound controls its user set.
25
30
 
26
31
  const LEASE_MS = 120_000;
27
32
  const MESSAGE_ID = /^[A-Za-z0-9_-]{1,200}$/;
@@ -69,16 +74,11 @@ async function executableHasInbox(realExecutable) {
69
74
  }
70
75
  }
71
76
 
77
+ // runExecutable starts an npm .cmd through cmd.exe, which execFile refuses.
72
78
  function defaultReadVersion(realExecutable, timeoutMs) {
73
- return new Promise(resolve => {
74
- import("node:child_process").then(({ execFile }) => {
75
- execFile(realExecutable, ["--version"], { timeout: timeoutMs, windowsHide: true },
76
- (error, stdout, stderr) => {
77
- if (error !== null) return resolve(null);
78
- resolve(/(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/.exec(`${stdout}${stderr}`)?.[1] ?? null);
79
- });
80
- });
81
- });
79
+ return runExecutable(realExecutable, ["--version"], { timeout: timeoutMs })
80
+ .then(({ stdout, stderr }) => /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/.exec(`${stdout}${stderr}`)?.[1] ?? null,
81
+ () => null);
82
82
  }
83
83
 
84
84
  function withTimeout(work, ms) {
@@ -91,22 +91,23 @@ function withTimeout(work, ms) {
91
91
  }
92
92
 
93
93
  /**
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
96
- * registry. Never launches a session. Native Windows serves a named pipe that
97
- * demands an auth line; nothing there is captured, so it keeps hook delivery.
94
+ * Read-only: a version at or above the capture and an executable that writes
95
+ * the inbox into its session registry. Never launches a session. On Windows an
96
+ * npm install puts a .cmd on PATH; the inbox is looked for in the file it runs.
98
97
  */
99
98
  export async function probeNativeDelivery({ realExecutable, timeoutMs = 750, platform = process.platform,
100
- hasInbox = executableHasInbox, readVersion = defaultReadVersion } = {}) {
99
+ hasInbox = executableHasInbox, readVersion = defaultReadVersion, shimTarget = npmShimTarget } = {}) {
101
100
  const unsupported = (reasonCode, clientVersion = null) => ({ supported: false, clientVersion,
102
101
  protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
103
- if (platform === "win32") return unsupported("native_delivery_unsupported");
104
102
  if (typeof realExecutable !== "string" || realExecutable === "") return unsupported("feature_probe_failed");
103
+ const scanned = isWindowsPlatform(platform) && /\.(cmd|bat)$/i.test(realExecutable)
104
+ ? await shimTarget(realExecutable) : realExecutable;
105
+ if (scanned === null) return unsupported("feature_probe_failed");
105
106
  const clientVersion = await withTimeout(Promise.resolve(readVersion(realExecutable, timeoutMs)), timeoutMs)
106
107
  .catch(() => null);
107
108
  if (versionOrder(clientVersion) === null) return unsupported("feature_probe_failed", clientVersion);
108
109
  if (belowMinimum(clientVersion)) return unsupported("below_minimum_version", clientVersion);
109
- const present = await withTimeout(Promise.resolve(hasInbox(realExecutable)), timeoutMs).catch(() => false);
110
+ const present = await withTimeout(Promise.resolve(hasInbox(scanned)), timeoutMs).catch(() => false);
110
111
  if (!present) return unsupported("protocol_mismatch", clientVersion);
111
112
  return { supported: true, clientVersion, protocolContract: PROTOCOL_CONTRACT,
112
113
  executableFingerprint: null, modes: [...INBOX_MODES], reasonCode: null };
@@ -137,7 +138,8 @@ const handshake = (endpoint, now) => ({ supported: true, clientVersion: endpoint
137
138
  */
138
139
  export async function bindNativeSession({ event, clientPid, clientVersion, runtimeDir,
139
140
  env = process.env, now = Date.now, managedSettingsPath = MANAGED_SETTINGS[process.platform],
140
- readClientArgs = readProcessArgs } = {}) {
141
+ readClientArgs = readProcessArgs, platform = process.platform, listPipes = listPipesOfMachine,
142
+ profileDir } = {}) {
141
143
  if (!Number.isInteger(clientPid) || clientPid <= 0) return closed(clientVersion, "client_process_unknown");
142
144
  if (typeof event?.sessionId !== "string" || event.sessionId === "") {
143
145
  return closed(clientVersion, "handshake_failed");
@@ -151,14 +153,14 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
151
153
  if (typeof socketPath !== "string" || socketPath === "") {
152
154
  return closed(clientVersion, "native_endpoint_unavailable");
153
155
  }
154
- await sweepDeadEndpoints({ runtimeDir });
156
+ await sweepDeadEndpoints({ runtimeDir, platform });
155
157
  const configDir = claudeConfigDir(env);
156
158
  const refused = await verifyInbox({ configDir, clientPid, sessionId: event.sessionId, socketPath,
157
- anyConversation: true });
159
+ anyConversation: true, platform, listPipes, ...(profileDir === undefined ? {} : { profileDir }) });
158
160
  if (refused !== null) return closed(clientVersion, refused);
159
161
  // Project settings sit where the session was started, which the registry
160
162
  // records; the hook's cwd follows the shell.
161
- const record = await readSessionRecord({ configDir, clientPid });
163
+ const record = await readSessionRecord({ configDir, clientPid, platform });
162
164
  const settings = await readInboundSettings({ configDir, projectDir: record?.cwd ?? event.cwd,
163
165
  managedSettingsPath });
164
166
  // The hook's mode is current. SessionStart carries none, so until the first
@@ -172,30 +174,33 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
172
174
  sessionId: event.sessionId, clientVersion, protocolContract: PROTOCOL_CONTRACT,
173
175
  leaseUntil: new Date(now() + LEASE_MS).toISOString(), reception };
174
176
  try {
175
- await writeInboxEndpoint({ runtimeDir, record: endpoint });
177
+ await writeInboxEndpoint({ runtimeDir, record: endpoint, platform, listPipes });
176
178
  } catch {
177
179
  return closed(clientVersion, "handshake_failed");
178
180
  }
179
181
  return handshake(endpoint, now);
180
182
  }
181
183
 
182
- async function verifiedEndpoint(binding, runtimeDir) {
183
- const endpoint = await readInboxEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
184
+ async function verifiedEndpoint(binding, runtimeDir, system) {
185
+ const endpoint = await readInboxEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef,
186
+ platform: system.platform });
184
187
  if (endpoint === null) return null;
185
188
  const refused = await verifyInbox({ configDir: endpoint.configDir, clientPid: endpoint.clientPid,
186
- sessionId: endpoint.sessionId, socketPath: endpoint.socketPath });
189
+ sessionId: endpoint.sessionId, socketPath: endpoint.socketPath, ...system });
187
190
  return refused === null ? endpoint : null;
188
191
  }
189
192
 
190
193
  // The router calls this when a lease ran out, so a session that sits idle
191
194
  // between turns stays reachable. The id never changes on a refresh.
192
- export async function refreshNativeSession({ binding, runtimeDir, now = Date.now } = {}) {
193
- const endpoint = await verifiedEndpoint(binding, runtimeDir);
195
+ export async function refreshNativeSession({ binding, runtimeDir, now = Date.now,
196
+ platform = process.platform, listPipes = listPipesOfMachine, profileDir } = {}) {
197
+ const endpoint = await verifiedEndpoint(binding, runtimeDir, { platform, listPipes,
198
+ ...(profileDir === undefined ? {} : { profileDir }) });
194
199
  return endpoint === null ? closed(binding?.clientVersion, "handshake_failed") : handshake(endpoint, now);
195
200
  }
196
201
 
197
- export const retireNativeSession = ({ binding, runtimeDir }) =>
198
- removeInboxEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
202
+ export const retireNativeSession = ({ binding, runtimeDir, platform = process.platform }) =>
203
+ removeInboxEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef, platform });
199
204
 
200
205
  /**
201
206
  * Sender side: re-verify the receiver, then write one wake line. The socket
@@ -204,20 +209,27 @@ export const retireNativeSession = ({ binding, runtimeDir }) =>
204
209
  * receiver's own hook.
205
210
  */
206
211
  export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 2_000,
207
- connect = net.createConnection } = {}) {
212
+ connect = net.createConnection, platform = process.platform, listPipes = listPipesOfMachine,
213
+ profileDir } = {}) {
208
214
  const rejected = safeErrorCode => ({ accepted: false, transport: TRANSPORT,
209
215
  clientVersion: binding?.clientVersion ?? null, safeErrorCode });
210
216
  if (typeof message?.messageId !== "string" || !MESSAGE_ID.test(message.messageId)) {
211
217
  return rejected("transport_rejected");
212
218
  }
213
- const endpoint = await verifiedEndpoint(binding, runtimeDir);
219
+ const endpoint = await verifiedEndpoint(binding, runtimeDir, { platform, listPipes,
220
+ ...(profileDir === undefined ? {} : { profileDir }) });
214
221
  if (endpoint === null) return rejected("recipient_unavailable");
222
+ // Read per offer and kept nowhere: the key of the process the record names now.
223
+ const peerToken = isWindowsPlatform(platform) ? await readPeerKey({ configDir: endpoint.configDir,
224
+ record: await readSessionRecord({ configDir: endpoint.configDir, clientPid: endpoint.clientPid, platform }) })
225
+ : null;
226
+ if (isWindowsPlatform(platform) && peerToken === null) return rejected("recipient_unavailable");
215
227
  // The receiver's own crossSessionInbound refuses unattested wakes; one would
216
228
  // be dropped, so none is sent and the message waits for its next turn.
217
229
  if (endpoint.reception === "refused") return rejected("delivery_disabled");
218
230
  const accepted = { accepted: true, transport: TRANSPORT, clientVersion: endpoint.clientVersion,
219
231
  ...(endpoint.reception === "held" ? { pendingApproval: true } : {}) };
220
- const wake = { runtimeDir, endpointId: endpoint.endpointId, messageId: message.messageId };
232
+ const wake = { runtimeDir, endpointId: endpoint.endpointId, messageId: message.messageId, platform };
221
233
  // Claude Code delivers a repeated msg_id again, so the dedupe is ours.
222
234
  let first;
223
235
  try {
@@ -226,7 +238,8 @@ export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 2
226
238
  return rejected("transport_error");
227
239
  }
228
240
  if (!first) return accepted;
229
- const frame = `${JSON.stringify({ type: "user",
241
+ const auth = peerToken === null ? "" : `${JSON.stringify({ type: "auth", token: peerToken })}\n`;
242
+ const frame = `${auth}${JSON.stringify({ type: "user",
230
243
  message: { role: "user", content: wakeText(message.messageId) },
231
244
  msg_id: `acc-wake-${message.messageId}` })}\n`;
232
245
  const result = await new Promise(resolve => {
@@ -1,16 +1,17 @@
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
- import { versionOrder } from "@agents-can-communicate/adapter-sdk";
4
+ import { closedTo, isWindowsPlatform, openRegularNoFollow, versionOrder } from "@agents-can-communicate/adapter-sdk";
6
5
 
7
6
  import { PROTOCOL_CONTRACT } from "./inbox-contract.mjs";
8
- import { inboxSocketIsSafe } from "./inbox-registry.mjs";
7
+ import { inboxSocketIsSafe, isInboxPipe, listPipesOfMachine } from "./inbox-registry.mjs";
9
8
  import { isReception } from "./inbox-settings.mjs";
10
9
 
11
10
  // One private record per hook binding, the way the Codex adapter keeps its
12
11
  // daemon endpoints: core holds only the random id, and the socket path, pid and
13
- // session id stay in a 0600 file under the workspace runtime directory.
12
+ // session id stay in a 0600 file under the workspace runtime directory. On
13
+ // Windows the file and its directory carry no mode bits; the profile's ACL keeps
14
+ // them to this user, and the inbox is a named pipe rather than a socket path.
14
15
 
15
16
  const KEYS = ["schemaVersion", "endpointId", "socketPath", "configDir", "clientPid", "sessionId",
16
17
  "clientVersion", "protocolContract", "leaseUntil", "reception"];
@@ -24,11 +25,12 @@ export const newEndpointId = () => `claude_inbox_${randomBytes(16).toString("hex
24
25
  // Where, under a workspace runtime directory, the records live.
25
26
  export const ENDPOINTS_DIRECTORY = "claude-inbox-endpoints";
26
27
 
27
- function valid(record) {
28
+ function valid(record, platform) {
28
29
  return record !== null && typeof record === "object" && !Array.isArray(record)
29
30
  && Object.keys(record).length === KEYS.length && KEYS.every(key => Object.hasOwn(record, key))
30
31
  && record.schemaVersion === 1 && ENDPOINT.test(record.endpointId)
31
- && absolute(record.socketPath) && absolute(record.configDir)
32
+ && (isWindowsPlatform(platform) ? isInboxPipe(record.socketPath) : absolute(record.socketPath))
33
+ && absolute(record.configDir)
32
34
  && Number.isInteger(record.clientPid) && record.clientPid > 0
33
35
  && typeof record.sessionId === "string" && record.sessionId !== "" && record.sessionId.length <= 200
34
36
  && typeof record.clientVersion === "string" && versionOrder(record.clientVersion) !== null
@@ -39,7 +41,7 @@ function valid(record) {
39
41
  && isReception(record.reception);
40
42
  }
41
43
 
42
- async function directory(runtimeDir, create = false) {
44
+ async function directory(runtimeDir, create = false, platform = process.platform) {
43
45
  if (!absolute(runtimeDir)) throw invalid();
44
46
  if (create) await mkdir(runtimeDir, { recursive: true, mode: 0o700 });
45
47
  const root = await realpath(runtimeDir);
@@ -48,15 +50,20 @@ async function directory(runtimeDir, create = false) {
48
50
  if (error.code !== "EEXIST") throw error;
49
51
  });
50
52
  const info = await lstat(dir);
51
- if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || (info.mode & 0o022) !== 0) throw invalid();
53
+ if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || !closedTo(info, 0o022, { platform })) {
54
+ throw invalid();
55
+ }
52
56
  return dir;
53
57
  }
54
58
 
55
- export async function writeInboxEndpoint({ runtimeDir, record }) {
56
- if (!valid(record) || !await inboxSocketIsSafe(record.socketPath)) throw invalid();
59
+ export async function writeInboxEndpoint({ runtimeDir, record, platform = process.platform,
60
+ listPipes = listPipesOfMachine }) {
61
+ if (!valid(record, platform) || !await inboxSocketIsSafe(record.socketPath, { platform, listPipes })) {
62
+ throw invalid();
63
+ }
57
64
  const text = `${JSON.stringify(record)}\n`;
58
65
  if (Buffer.byteLength(text) > MAX_BYTES) throw invalid();
59
- const dir = await directory(runtimeDir, true);
66
+ const dir = await directory(runtimeDir, true, platform);
60
67
  const file = path.join(dir, `${record.endpointId}.json`);
61
68
  const temporary = path.join(dir, `.${record.endpointId}.${randomBytes(8).toString("hex")}.tmp`);
62
69
  let handle;
@@ -72,17 +79,18 @@ export async function writeInboxEndpoint({ runtimeDir, record }) {
72
79
  }
73
80
 
74
81
  // An expired record is metadata for re-verification, never proof of reach.
75
- export async function readInboxEndpoint({ runtimeDir, endpointId }) {
82
+ export async function readInboxEndpoint({ runtimeDir, endpointId, platform = process.platform }) {
76
83
  if (typeof endpointId !== "string" || !ENDPOINT.test(endpointId)) return null;
77
84
  let handle;
78
85
  try {
79
- const dir = await directory(runtimeDir);
80
- handle = await open(path.join(dir, `${endpointId}.json`),
81
- constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
86
+ const dir = await directory(runtimeDir, false, platform);
87
+ handle = await openRegularNoFollow(path.join(dir, `${endpointId}.json`), undefined, { platform });
82
88
  const info = await handle.stat();
83
- if (!info.isFile() || !own(info) || info.size > MAX_BYTES || (info.mode & 0o077) !== 0) return null;
89
+ if (!info.isFile() || !own(info) || info.size > MAX_BYTES || !closedTo(info, 0o077, { platform })) {
90
+ return null;
91
+ }
84
92
  const record = JSON.parse(await handle.readFile("utf8"));
85
- return valid(record) && record.endpointId === endpointId ? record : null;
93
+ return valid(record, platform) && record.endpointId === endpointId ? record : null;
86
94
  } catch {
87
95
  return null;
88
96
  } finally {
@@ -90,14 +98,15 @@ export async function readInboxEndpoint({ runtimeDir, endpointId }) {
90
98
  }
91
99
  }
92
100
 
93
- export async function removeInboxEndpoint({ runtimeDir, endpointId }) {
101
+ export async function removeInboxEndpoint({ runtimeDir, endpointId, platform = process.platform }) {
94
102
  if (typeof endpointId !== "string" || !ENDPOINT.test(endpointId)) return;
95
103
  try {
96
- const dir = await directory(runtimeDir);
104
+ const dir = await directory(runtimeDir, false, platform);
97
105
  await rm(path.join(dir, `${endpointId}.json`), { force: true });
98
106
  } catch { /* retirement is already recorded; cleanup is best effort */ }
99
107
  try {
100
- await rm(path.join(await wakesDirectory(runtimeDir), endpointId), { recursive: true, force: true });
108
+ await rm(path.join(await wakesDirectory(runtimeDir, false, platform), endpointId),
109
+ { recursive: true, force: true });
101
110
  } catch { /* the same */ }
102
111
  }
103
112
 
@@ -115,17 +124,18 @@ const processExists = pid => {
115
124
  * retired. Only a record whose process is gone is removed: a live session whose
116
125
  * registry briefly names another conversation (right after /resume) keeps its own.
117
126
  */
118
- export async function sweepDeadEndpoints({ runtimeDir, exists = processExists, limit = 64 }) {
127
+ export async function sweepDeadEndpoints({ runtimeDir, exists = processExists, limit = 64,
128
+ platform = process.platform }) {
119
129
  let names;
120
130
  try {
121
- names = await readdir(await directory(runtimeDir));
131
+ names = await readdir(await directory(runtimeDir, false, platform));
122
132
  } catch {
123
133
  return;
124
134
  }
125
135
  for (const name of names.filter(item => /^claude_inbox_[a-f0-9]{32}\.json$/.test(item)).slice(0, limit)) {
126
136
  const endpointId = name.slice(0, -".json".length);
127
- const record = await readInboxEndpoint({ runtimeDir, endpointId });
128
- if (record !== null && !exists(record.clientPid)) await removeInboxEndpoint({ runtimeDir, endpointId });
137
+ const record = await readInboxEndpoint({ runtimeDir, endpointId, platform });
138
+ if (record !== null && !exists(record.clientPid)) await removeInboxEndpoint({ runtimeDir, endpointId, platform });
129
139
  }
130
140
  }
131
141
 
@@ -134,20 +144,22 @@ export async function sweepDeadEndpoints({ runtimeDir, exists = processExists, l
134
144
  // transport again; the marker keeps it to one wake per message and binding.
135
145
  const WAKE_MESSAGE = /^[A-Za-z0-9_-]{1,200}$/;
136
146
 
137
- async function wakesDirectory(runtimeDir, create = false) {
138
- const dir = path.join(path.dirname(await directory(runtimeDir, create)), "claude-inbox-wakes");
147
+ async function wakesDirectory(runtimeDir, create = false, platform = process.platform) {
148
+ const dir = path.join(path.dirname(await directory(runtimeDir, create, platform)), "claude-inbox-wakes");
139
149
  if (create) await mkdir(dir, { mode: 0o700 }).catch(error => {
140
150
  if (error.code !== "EEXIST") throw error;
141
151
  });
142
152
  const info = await lstat(dir);
143
- if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || (info.mode & 0o022) !== 0) throw invalid();
153
+ if (!info.isDirectory() || info.isSymbolicLink() || !own(info) || !closedTo(info, 0o022, { platform })) {
154
+ throw invalid();
155
+ }
144
156
  return dir;
145
157
  }
146
158
 
147
159
  /** True when this call is the first to wake the binding for the message. */
148
- export async function claimWake({ runtimeDir, endpointId, messageId }) {
160
+ export async function claimWake({ runtimeDir, endpointId, messageId, platform = process.platform }) {
149
161
  if (!ENDPOINT.test(endpointId) || !WAKE_MESSAGE.test(messageId)) throw invalid();
150
- const dir = path.join(await wakesDirectory(runtimeDir, true), endpointId);
162
+ const dir = path.join(await wakesDirectory(runtimeDir, true, platform), endpointId);
151
163
  await mkdir(dir, { mode: 0o700 }).catch(error => {
152
164
  if (error.code !== "EEXIST") throw error;
153
165
  });
@@ -160,9 +172,10 @@ export async function claimWake({ runtimeDir, endpointId, messageId }) {
160
172
  }
161
173
  }
162
174
 
163
- export async function releaseWake({ runtimeDir, endpointId, messageId }) {
175
+ export async function releaseWake({ runtimeDir, endpointId, messageId, platform = process.platform }) {
164
176
  if (!ENDPOINT.test(endpointId) || !WAKE_MESSAGE.test(messageId)) return;
165
177
  try {
166
- await rm(path.join(await wakesDirectory(runtimeDir), endpointId, messageId), { force: true });
178
+ await rm(path.join(await wakesDirectory(runtimeDir, false, platform), endpointId, messageId),
179
+ { force: true });
167
180
  } catch { /* a marker left behind only suppresses a repeat wake */ }
168
181
  }
@@ -1,33 +1,65 @@
1
- import { constants } from "node:fs";
2
- import { lstat, open, realpath } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readdir, realpath } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { isWindowsPlatform as isWindows, openRegularNoFollow } from "@agents-can-communicate/adapter-sdk";
5
6
 
6
7
  // Claude Code's own record of a live session: `<config>/sessions/<pid>.json`
7
- // names the session id and the inbox socket it listens on. ACC reads that file
8
- // and nothing beside it - never the `<pid>.<hash>.key` next to it.
8
+ // names the session id and the inbox it listens on. On POSIX the inbox is a
9
+ // socket only this user can open, and ACC reads that file and nothing beside it.
10
+ // On Windows the inbox is a named pipe that refuses a frame without an auth
11
+ // line, and Claude publishes a key for other sessions of the same user beside
12
+ // the record: `<pid>.<sha256 of the lowercased pipe path>.key` (measured on
13
+ // 2.1.286). ACC reads that peer key there, so a wake is classified a peer's and
14
+ // the recipient's inbound settings apply; it never reads the session's own
15
+ // messaging token, which would pass as the session's child.
9
16
 
10
17
  const MAX_BYTES = 16_384;
18
+ const KEY_MAX_BYTES = 4_096;
19
+ const INBOX_PIPE = /^\\\\\.\\pipe\\((?:LOCAL\\)?cc-msg-[0-9a-f]{32})$/i;
20
+ const PEER_TOKEN = /^[0-9a-f]{32}$/;
21
+ const VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
11
22
  const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
23
+ const text = value => (typeof value === "string" && value !== "" ? value : null);
12
24
 
13
25
  export const claudeConfigDir = env => typeof env?.CLAUDE_CONFIG_DIR === "string"
14
26
  && path.isAbsolute(env.CLAUDE_CONFIG_DIR)
15
27
  ? env.CLAUDE_CONFIG_DIR : path.join(os.homedir(), ".claude");
16
28
 
29
+ /** Whether a directory lies inside the user's profile, compared as Windows compares paths. */
30
+ export function insideProfile(directory, profileDir) {
31
+ if (typeof directory !== "string" || typeof profileDir !== "string") return false;
32
+ const relative = path.win32.relative(profileDir.toLowerCase(), directory.toLowerCase());
33
+ return relative !== "" && !relative.startsWith("..") && !path.win32.isAbsolute(relative);
34
+ }
35
+
36
+ /** Whether a value names a Claude Code inbox pipe. */
37
+ export const isInboxPipe = value => typeof value === "string" && INBOX_PIPE.test(value);
38
+
39
+ export const listPipesOfMachine = () => readdir("\\\\.\\pipe\\");
40
+
17
41
  /** The registry entry for one pid, or null. Bounded, owner-checked, never through a link. */
18
- export async function readSessionRecord({ configDir, clientPid }) {
42
+ export async function readSessionRecord({ configDir, clientPid, platform = process.platform }) {
19
43
  if (!Number.isInteger(clientPid) || clientPid <= 0 || typeof configDir !== "string") return null;
44
+ const windows = isWindows(platform);
45
+ const paths = windows ? path.win32 : path;
20
46
  let handle;
21
47
  try {
22
- handle = await open(path.join(configDir, "sessions", `${clientPid}.json`),
23
- constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
48
+ handle = await openRegularNoFollow(path.join(configDir, "sessions", `${clientPid}.json`),
49
+ undefined, { platform });
24
50
  const info = await handle.stat();
25
51
  if (!info.isFile() || !own(info) || info.size > MAX_BYTES) return null;
26
52
  const record = JSON.parse(await handle.readFile("utf8"));
53
+ const inbox = typeof record?.messagingSocketPath === "string" && (windows
54
+ ? INBOX_PIPE.test(record.messagingSocketPath) : paths.isAbsolute(record.messagingSocketPath));
27
55
  return record?.pid === clientPid && typeof record.sessionId === "string" && record.sessionId !== ""
28
- && typeof record.messagingSocketPath === "string" && path.isAbsolute(record.messagingSocketPath)
56
+ && inbox
29
57
  ? { pid: record.pid, sessionId: record.sessionId, messagingSocketPath: record.messagingSocketPath,
30
- cwd: typeof record.cwd === "string" && path.isAbsolute(record.cwd) ? record.cwd : null }
58
+ cwd: typeof record.cwd === "string" && paths.isAbsolute(record.cwd) ? record.cwd : null,
59
+ // The build this process runs, as Claude Code itself recorded it.
60
+ version: typeof record.version === "string" && VERSION.test(record.version)
61
+ ? record.version : null,
62
+ ...(windows ? { procStart: text(record.procStart), pidDomain: text(record.pidDomain) } : {}) }
31
63
  : null;
32
64
  } catch {
33
65
  return null;
@@ -36,8 +68,19 @@ export async function readSessionRecord({ configDir, clientPid }) {
36
68
  }
37
69
  }
38
70
 
39
- /** A socket this user owns, reached without a link, closed to group and others. */
40
- export async function inboxSocketIsSafe(socketPath) {
71
+ /**
72
+ * A socket this user owns, reached without a link, closed to group and others.
73
+ * On Windows a Claude inbox pipe the machine lists: its name is 128 random bits
74
+ * that only this user's profile records.
75
+ */
76
+ export async function inboxSocketIsSafe(socketPath, { platform = process.platform,
77
+ listPipes = listPipesOfMachine } = {}) {
78
+ if (isWindows(platform)) {
79
+ const name = INBOX_PIPE.exec(typeof socketPath === "string" ? socketPath : "")?.[1]?.toLowerCase();
80
+ if (name === undefined) return false;
81
+ const pipes = await Promise.resolve().then(listPipes).catch(() => []);
82
+ return pipes.some(pipe => String(pipe).toLowerCase() === name);
83
+ }
41
84
  if (typeof socketPath !== "string" || !path.isAbsolute(socketPath)) return false;
42
85
  try {
43
86
  const info = await lstat(socketPath);
@@ -59,13 +102,55 @@ export async function inboxSocketIsSafe(socketPath) {
59
102
  * /resume before it rewrites the entry, so there the hook's own process and
60
103
  * socket are the proof; every offer and refresh still requires the session id.
61
104
  */
62
- export async function verifyInbox({ configDir, clientPid, sessionId, socketPath, anyConversation = false }) {
63
- if (!await inboxSocketIsSafe(socketPath)) return "native_endpoint_unavailable";
64
- const record = await readSessionRecord({ configDir, clientPid });
105
+ export async function verifyInbox({ configDir, clientPid, sessionId, socketPath, anyConversation = false,
106
+ platform = process.platform, listPipes = listPipesOfMachine, profileDir = os.homedir() }) {
107
+ // The profile's ACL is what keeps the session files on Windows to their user,
108
+ // so a configuration directory outside it is not trusted for a peer key. Both
109
+ // are resolved first: TEMP and a hand-set CLAUDE_CONFIG_DIR may use 8.3 names.
110
+ if (isWindows(platform)) {
111
+ const [config, profile] = await Promise.all([configDir, profileDir]
112
+ .map(directory => realpath(directory).catch(() => null)));
113
+ if (!insideProfile(config, profile)) return "native_endpoint_unavailable";
114
+ }
115
+ if (!await inboxSocketIsSafe(socketPath, { platform, listPipes })) return "native_endpoint_unavailable";
116
+ const record = await readSessionRecord({ configDir, clientPid, platform });
65
117
  if (record === null || (!anyConversation && record.sessionId !== sessionId)) {
66
118
  return "native_session_unavailable";
67
119
  }
120
+ // A pipe has no path to resolve; Windows compares its name without case.
121
+ if (isWindows(platform)) {
122
+ return record.messagingSocketPath.toLowerCase() === socketPath.toLowerCase()
123
+ ? null : "native_session_unavailable";
124
+ }
68
125
  const [registered, expected] = await Promise.all([
69
126
  realpath(record.messagingSocketPath).catch(() => null), realpath(socketPath).catch(() => null)]);
70
127
  return registered !== null && registered === expected ? null : "native_session_unavailable";
71
128
  }
129
+
130
+ /**
131
+ * Windows: the token Claude published for peers of this session, or null. The
132
+ * key's name binds it to this pid and this pipe, and its process start has to
133
+ * be the one the session record gives, so a key left by an earlier process
134
+ * that held the pid is never used. Read per offer, never stored by ACC.
135
+ */
136
+ export async function readPeerKey({ configDir, record }) {
137
+ if (typeof configDir !== "string" || typeof record?.messagingSocketPath !== "string"
138
+ || !Number.isInteger(record.pid) || text(record.procStart) === null) return null;
139
+ const digest = createHash("sha256").update(record.messagingSocketPath.toLowerCase()).digest("hex");
140
+ let handle;
141
+ try {
142
+ handle = await openRegularNoFollow(path.join(configDir, "sessions", `${record.pid}.${digest}.key`),
143
+ undefined, { platform: "win32" });
144
+ const info = await handle.stat();
145
+ if (!info.isFile() || info.size > KEY_MAX_BYTES) return null;
146
+ const key = JSON.parse(await handle.readFile("utf8"));
147
+ const started = text(key?.procStartFt) ?? text(key?.procStart);
148
+ const domain = record.pidDomain === null || key?.pidDomain === record.pidDomain;
149
+ return PEER_TOKEN.test(key?.peerToken ?? "") && started === record.procStart && domain
150
+ ? key.peerToken : null;
151
+ } catch {
152
+ return null;
153
+ } finally {
154
+ await handle?.close().catch(() => null);
155
+ }
156
+ }
@@ -1,6 +1,6 @@
1
- import { execFile } from "node:child_process";
2
1
  import { readFile } from "node:fs/promises";
3
2
  import path from "node:path";
3
+ import { readProcessArgs as readArgs } from "@agents-can-communicate/adapter-sdk";
4
4
 
5
5
  // What Claude Code does with a wake from a sender that attests no permission
6
6
  // mode - ACC never attests one. A session that bypasses permission prompts
@@ -67,13 +67,9 @@ export function permissionModeFromArgs(args) {
67
67
  return null;
68
68
  }
69
69
 
70
- /** A process's arguments as `ps` prints them, split on spaces, or null. */
70
+ /** A process's arguments, or null when they cannot be read. */
71
71
  export function readProcessArgs(pid, { timeoutMs = 500 } = {}) {
72
- if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32") return Promise.resolve(null);
73
- return new Promise(resolve => {
74
- execFile("ps", ["-o", "args=", "-p", String(pid)], { timeout: timeoutMs, maxBuffer: 64 * 1024 },
75
- (error, stdout) => resolve(error ? null : String(stdout).trim().split(/\s+/).filter(Boolean)));
76
- });
72
+ return readArgs(pid, { timeoutMs });
77
73
  }
78
74
 
79
75
  export function receptionOf({ permissionMode, crossSessionInbound }) {
@@ -21,6 +21,8 @@ import path from "node:path";
21
21
  // serves the inbox on a named pipe, which no Unix socket grant covers.
22
22
 
23
23
  const hostOf = platform => String(platform ?? process.platform).split("-")[0];
24
+ // Every branch below is a POSIX client's layout, built the same on any host.
25
+ const posix = path.posix;
24
26
  const set = value => typeof value === "string" && value !== "";
25
27
 
26
28
  export function inboxSocketDirectories({ env = {}, platform, uid } = {}) {
@@ -30,12 +32,12 @@ export function inboxSocketDirectories({ env = {}, platform, uid } = {}) {
30
32
  // Claude Code's `||`: an empty value falls through to the next one.
31
33
  const root = [env.XDG_RUNTIME_DIR, env.CLAUDE_CODE_TMPDIR].find(set) ?? "/tmp";
32
34
  const fallbackRoot = set(env.TERMUX_VERSION) && set(env.PREFIX)
33
- ? path.join(env.PREFIX, "tmp") : "/tmp";
35
+ ? posix.join(env.PREFIX, "tmp") : "/tmp";
34
36
  const directories = [
35
- path.join(root, "cc-socks"),
37
+ posix.join(root, "cc-socks"),
36
38
  "/tmp/cc-socks",
37
39
  ...(host === "linux" ? [`/run/user/${user}/cc-socks`] : []),
38
- path.join(fallbackRoot, `cc-socks-${user}`),
40
+ posix.join(fallbackRoot, `cc-socks-${user}`),
39
41
  ];
40
- return [...new Set(directories.filter(directory => path.isAbsolute(directory)))];
42
+ return [...new Set(directories.filter(directory => posix.isAbsolute(directory)))];
41
43
  }