agents-can-communicate 0.8.5 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
- package/bin/entrypoints/acc-hook.mjs +33 -19
- package/bin/entrypoints/hook-output.mjs +14 -5
- package/docs/ADAPTER_AUTHORING.md +29 -0
- package/docs/CAPABILITIES.md +7 -6
- package/docs/CLI.md +5 -0
- package/docs/CONFIGURATION.md +41 -10
- package/docs/DESIGN_DECISIONS.md +0 -2
- package/docs/GETTING_STARTED.md +39 -6
- package/docs/HOW_IT_WORKS.md +13 -8
- package/docs/SECURITY_MODEL.md +32 -5
- package/docs/TROUBLESHOOTING.md +76 -8
- package/docs/UPGRADING.md +47 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +13 -2
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/allow-rule.mjs +129 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
- package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +36 -13
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +3 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/src/own-version.mjs +27 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
- package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
- package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
- package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
- package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
- package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
- package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
- package/node_modules/@agents-can-communicate/installer/package.json +3 -2
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
- package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -2
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +6 -2
- package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
- package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
- package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
- package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
- package/package.json +1 -5
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
-
import { compareVersionOrder, versionOrder }
|
|
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 }
|
|
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
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
&&
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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),
|
|
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 {
|
|
2
|
-
import { lstat,
|
|
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
|
|
8
|
-
//
|
|
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
|
|
23
|
-
|
|
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
|
-
&&
|
|
56
|
+
&& inbox
|
|
29
57
|
? { pid: record.pid, sessionId: record.sessionId, messagingSocketPath: record.messagingSocketPath,
|
|
30
|
-
cwd: typeof record.cwd === "string" &&
|
|
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
|
-
/**
|
|
40
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
70
|
+
/** A process's arguments, or null when they cannot be read. */
|
|
71
71
|
export function readProcessArgs(pid, { timeoutMs = 500 } = {}) {
|
|
72
|
-
|
|
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 }) {
|
package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs
CHANGED
|
@@ -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
|
-
?
|
|
35
|
+
? posix.join(env.PREFIX, "tmp") : "/tmp";
|
|
34
36
|
const directories = [
|
|
35
|
-
|
|
37
|
+
posix.join(root, "cc-socks"),
|
|
36
38
|
"/tmp/cc-socks",
|
|
37
39
|
...(host === "linux" ? [`/run/user/${user}/cc-socks`] : []),
|
|
38
|
-
|
|
40
|
+
posix.join(fallbackRoot, `cc-socks-${user}`),
|
|
39
41
|
];
|
|
40
|
-
return [...new Set(directories.filter(directory =>
|
|
42
|
+
return [...new Set(directories.filter(directory => posix.isAbsolute(directory)))];
|
|
41
43
|
}
|