agents-can-communicate 0.10.3 → 0.10.4
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/docs/TROUBLESHOOTING.md +10 -0
- package/docs/UPGRADING.md +16 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- 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 +19 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +9 -6
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +16 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -0
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +5 -1
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +2 -0
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +9 -6
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +6 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +1 -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/storage-filesystem/package.json +1 -1
- package/package.json +1 -1
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -158,6 +158,16 @@ delivery, start Codex without it; put a setting you need in `config.toml` instea
|
|
|
158
158
|
If a reply stays queued with `transport_permission_denied`, the sender's permissions
|
|
159
159
|
blocked local transport. This is distinct from an unavailable recipient. The message was
|
|
160
160
|
recorded successfully; the error does not mean it was read or acknowledged.
|
|
161
|
+
For Codex sends, use the exact installed ACC wrapper through the client's approved
|
|
162
|
+
command path (`exec_command` with `sandbox_permissions: "require_escalated"`). The
|
|
163
|
+
installed ACC prefix rule can authorize that command. Keep it separate from other
|
|
164
|
+
shell commands. If delivery was already attempted, retry the same arguments and
|
|
165
|
+
the original `--client-message-id` printed by the CLI. This retries the recorded
|
|
166
|
+
message instead of recording another one. An initial execution refusal records
|
|
167
|
+
nothing; a refused retry does not erase an earlier recorded message.
|
|
168
|
+
On Codex 0.162.0 on macOS, even an installed socket allowlist did not permit direct
|
|
169
|
+
access to the daemon socket from the sandbox. A configured profile alone therefore
|
|
170
|
+
does not prove that a sandboxed sender can reach another Codex session.
|
|
161
171
|
On Codex 0.153.4 or newer, rerun `acc install --adapter codex` with existing
|
|
162
172
|
live consent. Add `--delivery actionable` when new consent is required, then start a new session.
|
|
163
173
|
With ACC 0.8.0 or 0.8.1, every live offer from a Codex session to Claude Code failed this way:
|
package/docs/UPGRADING.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Upgrading
|
|
2
2
|
|
|
3
|
+
## From 0.10.3
|
|
4
|
+
|
|
5
|
+
Update as usual with `acc update`. Version 0.10.4 preserves sender permission
|
|
6
|
+
failures as `transport_permission_denied`, including when access to socket
|
|
7
|
+
metadata or an expired delivery binding is denied. The CLI prints the original
|
|
8
|
+
retry key so the same recorded message can be offered again without a duplicate.
|
|
9
|
+
|
|
10
|
+
The refreshed Codex skill uses the client's approved command path for outgoing
|
|
11
|
+
ACC messages. ACC's existing exact-wrapper rule can authorize those commands.
|
|
12
|
+
An initial execution refusal records no message; a refused retry preserves an
|
|
13
|
+
earlier durable message. See [sender permissions](TROUBLESHOOTING.md#i-enabled-live-delivery-but-got-fallback)
|
|
14
|
+
for the recovery procedure.
|
|
15
|
+
|
|
16
|
+
This release keeps store contract 7 and adds no data migration. The update
|
|
17
|
+
refreshes installed integrations while preserving their recorded delivery policy.
|
|
18
|
+
|
|
3
19
|
## Store contract 6 to 7
|
|
4
20
|
|
|
5
21
|
ACC moves each workspace's store from contract 6 to 7 by itself, right after an update
|
|
@@ -16,6 +16,25 @@ Use this skill when hook context reports peers or actionable attention, or
|
|
|
16
16
|
the user asks for coordination between sessions. An `ACC CLI (append):` header
|
|
17
17
|
by itself supplies identity for later use; continue the user's ordinary work.
|
|
18
18
|
|
|
19
|
+
### Send through Codex's approved command path
|
|
20
|
+
|
|
21
|
+
Run `message`, `request`, `reply`, and `finish` with `exec_command` and
|
|
22
|
+
`sandbox_permissions: "require_escalated"`. Put the installed `{{ACC}}` wrapper
|
|
23
|
+
first in the command. ACC's installed prefix rule can then authorize that exact
|
|
24
|
+
local command. Do not combine it with other shell commands. These sends stay
|
|
25
|
+
on this machine and reach the user's own agent sessions.
|
|
26
|
+
|
|
27
|
+
Codex can deny its daemon socket inside the sandbox even when the ACC profile
|
|
28
|
+
allows the socket. The approved command path preserves that sandbox boundary.
|
|
29
|
+
An initial execution refusal records no message. If a retry is refused, the
|
|
30
|
+
earlier recorded message remains durable. Report which case occurred.
|
|
31
|
+
|
|
32
|
+
Choose a distinct `--client-message-id` for each new send. If delivery reports
|
|
33
|
+
`transport_permission_denied`, retry the same command through approved execution
|
|
34
|
+
with the same key and content. The CLI prints the original key when it generated
|
|
35
|
+
one. A retry uses the recorded message and offers it again only while queued.
|
|
36
|
+
Use normal sandboxed execution for local reads and claims.
|
|
37
|
+
|
|
19
38
|
## Use your own CLI credentials
|
|
20
39
|
|
|
21
40
|
When this turn's ACC hook supplies `ACC CLI (append):`, append those exact
|
|
@@ -168,22 +168,25 @@ async function bindAttempt({ event, clientPid, clientVersion, runtimeDir,
|
|
|
168
168
|
// what `codex --version` printed, and a daemon that had updated under its CLI
|
|
169
169
|
// was refused here on that difference alone.
|
|
170
170
|
async function receiverFor(binding, runtimeDir, platform = process.platform) {
|
|
171
|
-
const endpoint = await readNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef,
|
|
172
|
-
|
|
171
|
+
const endpoint = await readNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef,
|
|
172
|
+
platform, throwPermissionErrors: true });
|
|
173
|
+
return endpoint !== null && await socketIsReady(endpoint.socketPath,
|
|
174
|
+
{ platform, throwPermissionErrors: true }) ? endpoint : null;
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 750,
|
|
176
178
|
now = Date.now, open = openCodexAppServer, platform = process.platform } = {}) {
|
|
177
179
|
const rejected = reason => closed(binding?.clientVersion, reason);
|
|
178
|
-
const endpoint = await receiverFor(binding, runtimeDir, platform);
|
|
179
|
-
if (endpoint === null) return rejected("handshake_failed");
|
|
180
180
|
try {
|
|
181
|
+
const endpoint = await receiverFor(binding, runtimeDir, platform);
|
|
182
|
+
if (endpoint === null) return rejected("handshake_failed");
|
|
181
183
|
return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
|
|
182
184
|
const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
|
|
183
185
|
return reasonCode === null
|
|
184
186
|
? handshake({ ...endpoint, clientVersion: servingVersion }, now) : rejected(reasonCode);
|
|
185
187
|
});
|
|
186
188
|
} catch (error) {
|
|
189
|
+
if (["EPERM", "EACCES"].includes(error?.code)) return rejected("transport_permission_denied");
|
|
187
190
|
return rejected(error?.code === "ETIMEDOUT" ? "handshake_timeout" : "handshake_failed");
|
|
188
191
|
}
|
|
189
192
|
}
|
|
@@ -192,9 +195,9 @@ export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 5
|
|
|
192
195
|
open = openCodexAppServer, platform = process.platform } = {}) {
|
|
193
196
|
const rejected = safeErrorCode => ({ accepted: false, transport: "codex-app-server",
|
|
194
197
|
clientVersion: binding?.clientVersion ?? null, safeErrorCode });
|
|
195
|
-
const endpoint = await receiverFor(binding, runtimeDir, platform);
|
|
196
|
-
if (endpoint === null) return rejected("recipient_unavailable");
|
|
197
198
|
try {
|
|
199
|
+
const endpoint = await receiverFor(binding, runtimeDir, platform);
|
|
200
|
+
if (endpoint === null) return rejected("recipient_unavailable");
|
|
198
201
|
return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
|
|
199
202
|
const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
|
|
200
203
|
if (reasonCode !== null) return rejected(reasonCode === "handshake_timeout" ? "transport_error"
|
|
@@ -50,18 +50,25 @@ async function directory(runtimeDir, create = false, platform = process.platform
|
|
|
50
50
|
* 0.159.3), so there the socket counts while its directory lists it; Codex keeps
|
|
51
51
|
* that directory to its user.
|
|
52
52
|
*/
|
|
53
|
-
export async function socketIsReady(socketPath, { platform = process.platform
|
|
53
|
+
export async function socketIsReady(socketPath, { platform = process.platform,
|
|
54
|
+
throwPermissionErrors = false } = {}) {
|
|
54
55
|
if (isWindowsPlatform(platform)) {
|
|
55
56
|
if (!absolute(socketPath)) return false;
|
|
56
57
|
const name = path.basename(socketPath).toLowerCase();
|
|
57
|
-
const names = await readdir(path.dirname(socketPath)).catch(
|
|
58
|
+
const names = await readdir(path.dirname(socketPath)).catch(error => {
|
|
59
|
+
if (throwPermissionErrors && ["EPERM", "EACCES"].includes(error.code)) throw error;
|
|
60
|
+
return [];
|
|
61
|
+
});
|
|
58
62
|
return names.some(entry => entry.toLowerCase() === name);
|
|
59
63
|
}
|
|
60
64
|
if (!absolute(socketPath)) return false;
|
|
61
65
|
try {
|
|
62
66
|
const info = await lstat(socketPath);
|
|
63
67
|
return info.isSocket() && !info.isSymbolicLink() && own(info);
|
|
64
|
-
} catch {
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (throwPermissionErrors && ["EPERM", "EACCES"].includes(error.code)) throw error;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/** The socket a control socket path leads to, or null. Codex 0.157.1 keeps a
|
|
@@ -104,7 +111,8 @@ export async function writeNativeEndpoint({ runtimeDir, record, platform = proce
|
|
|
104
111
|
|
|
105
112
|
// Expired records are metadata for revalidation, never proof of reachability.
|
|
106
113
|
// Refuse links and oversized files before reading; no foreign error text escapes.
|
|
107
|
-
export async function readNativeEndpoint({ runtimeDir, endpointId, platform = process.platform
|
|
114
|
+
export async function readNativeEndpoint({ runtimeDir, endpointId, platform = process.platform,
|
|
115
|
+
throwPermissionErrors = false }) {
|
|
108
116
|
if (typeof endpointId !== "string" || !ENDPOINT.test(endpointId)) return null;
|
|
109
117
|
let handle;
|
|
110
118
|
try {
|
|
@@ -116,7 +124,10 @@ export async function readNativeEndpoint({ runtimeDir, endpointId, platform = pr
|
|
|
116
124
|
}
|
|
117
125
|
const record = JSON.parse(await handle.readFile("utf8"));
|
|
118
126
|
return valid(record) && record.endpointId === endpointId ? record : null;
|
|
119
|
-
} catch {
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (throwPermissionErrors && ["EPERM", "EACCES"].includes(error.code)) throw error;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
120
131
|
finally { await handle?.close().catch(() => null); }
|
|
121
132
|
}
|
|
122
133
|
|
|
@@ -29,6 +29,8 @@ export const NATIVE_REASON_CODES = Object.freeze([
|
|
|
29
29
|
"prerelease_not_captured", "below_minimum_version", "known_bad_version",
|
|
30
30
|
"feature_probe_failed", "probe_timeout", "probe_version_mismatch", "protocol_mismatch",
|
|
31
31
|
"native_endpoint_unavailable", "native_session_unavailable",
|
|
32
|
+
// A caller can be denied access to a healthy receiver's local transport.
|
|
33
|
+
"transport_permission_denied",
|
|
32
34
|
"handshake_failed", "handshake_timeout", "handshake_version_mismatch",
|
|
33
35
|
"session_generation_stale", "client_process_unknown", "unsupported_shell",
|
|
34
36
|
// The current session's canonical workspace identity could not be verified.
|
|
@@ -6,6 +6,7 @@ import { AccError, CONFIG_FILENAME, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIO
|
|
|
6
6
|
failure, ok }
|
|
7
7
|
from "@agents-can-communicate/protocol";
|
|
8
8
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
9
|
+
import { shellQuote } from "@agents-can-communicate/adapter-sdk";
|
|
9
10
|
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
10
11
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
11
12
|
|
|
@@ -162,7 +163,10 @@ export function recordedText(message, delivery) {
|
|
|
162
163
|
? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
|
|
163
164
|
: item.outcome === "queued"
|
|
164
165
|
? item.errorCode === "transport_permission_denied"
|
|
165
|
-
? "live offer blocked by sender permissions; message remains queued;
|
|
166
|
+
? "live offer blocked by sender permissions; message remains queued; retry the same command "
|
|
167
|
+
+ "through approved execution"
|
|
168
|
+
+ (typeof message.clientMessageId === "string"
|
|
169
|
+
? ` with --client-message-id ${shellQuote(message.clientMessageId)}` : " with its original retry key")
|
|
166
170
|
: item.errorCode === "no_live_transport"
|
|
167
171
|
? `${item.recipientParticipantId} has no live transport`
|
|
168
172
|
+ `${item.nativeReasonCode ? ` (${describeNativeReason(item.nativeReasonCode,
|
|
@@ -97,6 +97,8 @@ export async function updateNativeSessions(adapters, { service, status, root, no
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const failures = {
|
|
100
|
+
transport_permission_denied: "this command cannot access the local delivery channel; "
|
|
101
|
+
+ "check the sender's command permissions",
|
|
100
102
|
client_process_unknown: "the client process could not be identified; start a new client session",
|
|
101
103
|
handshake_failed: "the session handshake failed; check the client's integration/channel setup",
|
|
102
104
|
handshake_timeout: "the session handshake timed out; check the local channel and retry on the next turn",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateNativeHandshake } from "@agents-can-communicate/adapter-sdk";
|
|
2
2
|
|
|
3
3
|
const MAX_REFRESH_LEASE_MS = 120_000;
|
|
4
|
+
const refused = (errorCode = "recipient_unavailable") => ({ refreshed: false, errorCode });
|
|
4
5
|
|
|
5
6
|
function sameBinding(current, expected, leaseUntil) {
|
|
6
7
|
return current !== null && current !== undefined
|
|
@@ -15,24 +16,26 @@ function sameBinding(current, expected, leaseUntil) {
|
|
|
15
16
|
|
|
16
17
|
export async function refreshExpiredBinding({ service, adapter, binding, runtimeDir,
|
|
17
18
|
clock, timeoutMs = 750 }) {
|
|
18
|
-
if (typeof adapter?.refreshNativeSession !== "function") return
|
|
19
|
+
if (typeof adapter?.refreshNativeSession !== "function") return refused();
|
|
19
20
|
const now = Date.parse(clock.now());
|
|
20
|
-
if (!Number.isFinite(now)) return
|
|
21
|
+
if (!Number.isFinite(now)) return refused();
|
|
21
22
|
try {
|
|
22
23
|
const handshake = await adapter.refreshNativeSession({ binding, runtimeDir, timeoutMs });
|
|
23
24
|
const validated = validateNativeHandshake(adapter, {
|
|
24
25
|
clientVersion: binding.clientVersion, handshake,
|
|
25
26
|
});
|
|
26
|
-
if (!validated.ok
|
|
27
|
+
if (!validated.ok) return refused(validated.reasonCode === "transport_permission_denied"
|
|
28
|
+
? "transport_permission_denied" : "recipient_unavailable");
|
|
29
|
+
if (validated.opaqueEndpointRef !== binding.opaqueEndpointRef) return refused();
|
|
27
30
|
const requestedLease = Date.parse(validated.leaseUntil);
|
|
28
|
-
if (!Number.isFinite(requestedLease) || requestedLease <= now) return
|
|
31
|
+
if (!Number.isFinite(requestedLease) || requestedLease <= now) return refused();
|
|
29
32
|
const leaseUntil = new Date(Math.min(requestedLease,
|
|
30
33
|
now + MAX_REFRESH_LEASE_MS)).toISOString();
|
|
31
34
|
await service.refreshDeliveryBinding({ sessionId: binding.sessionId,
|
|
32
35
|
generation: binding.generation, leaseUntil });
|
|
33
36
|
const current = await service.store?.ephemeral?.get("deliveryBinding", binding.sessionId);
|
|
34
|
-
return sameBinding(current, binding, leaseUntil);
|
|
37
|
+
return sameBinding(current, binding, leaseUntil) ? { refreshed: true, errorCode: null } : refused();
|
|
35
38
|
} catch {
|
|
36
|
-
return
|
|
39
|
+
return refused();
|
|
37
40
|
}
|
|
38
41
|
}
|
|
@@ -132,7 +132,12 @@ export function createDeliveryRouter({ service, adapters, clock, readLivePolicy,
|
|
|
132
132
|
if (Date.parse(binding.leaseUntil) <= Date.parse(now)) {
|
|
133
133
|
const refreshed = await refreshExpiredBinding({ service, adapter, binding,
|
|
134
134
|
runtimeDir: service.store?.root, clock });
|
|
135
|
-
if (!refreshed)
|
|
135
|
+
if (!refreshed.refreshed) {
|
|
136
|
+
if (refreshed.errorCode === "transport_permission_denied") {
|
|
137
|
+
await recordFailure(binding, message, participantId, "live-adapter", refreshed.errorCode);
|
|
138
|
+
}
|
|
139
|
+
return durable(participantId, refreshed.errorCode);
|
|
140
|
+
}
|
|
136
141
|
const current = (await service.listDeliveryBindings({
|
|
137
142
|
participantId, now: clock.now() })).filter(item => item.sessionId === binding.sessionId
|
|
138
143
|
&& item.generation === binding.generation
|
|
@@ -10,6 +10,7 @@ const REASONS = Object.freeze({
|
|
|
10
10
|
known_bad_version: "this client version has a known native delivery failure",
|
|
11
11
|
native_endpoint_unavailable: "the client's local delivery service is unavailable",
|
|
12
12
|
native_session_unavailable: "the local delivery service has no loaded client session",
|
|
13
|
+
transport_permission_denied: "the calling process lacks permission to access the local delivery channel",
|
|
13
14
|
feature_probe_failed: "the native protocol probe did not succeed",
|
|
14
15
|
probe_timeout: "the native protocol probe timed out",
|
|
15
16
|
probe_version_mismatch: "the local service and CLI versions differ",
|