agents-can-communicate 0.2.0 → 0.3.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 (86) hide show
  1. package/README.md +17 -7
  2. package/bin/acc-bootstrap.mjs +56 -0
  3. package/bin/acc-claude-channel.mjs +177 -0
  4. package/bin/acc.mjs +7 -2
  5. package/docs/ADAPTER_AUTHORING.md +34 -2
  6. package/docs/CAPABILITIES.md +25 -10
  7. package/docs/CLI.md +10 -3
  8. package/docs/CONFIGURATION.md +4 -0
  9. package/docs/HOW_IT_WORKS.md +277 -0
  10. package/docs/PROTOCOL.md +1 -1
  11. package/docs/RELEASING.md +7 -1
  12. package/docs/TROUBLESHOOTING.md +7 -1
  13. package/docs/index.md +3 -1
  14. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +196 -46
  15. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +237 -45
  16. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +3 -1
  17. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
  18. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
  19. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
  20. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
  21. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +4 -2
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +34 -2
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
  26. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -5
  27. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +171 -38
  28. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +3 -1
  29. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
  30. package/node_modules/@agents-can-communicate/adapter-codex/package.json +4 -2
  31. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +4 -2
  32. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +42 -7
  33. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
  34. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
  35. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
  36. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +44 -28
  37. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  38. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +4 -2
  39. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeAgent.json → BeforeAgent-0.57.0.json} +2 -2
  40. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool.json → BeforeTool-0.57.0.json} +2 -2
  41. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool-shell.json → BeforeTool-shell-0.57.0.json} +2 -2
  42. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionEnd.json → SessionEnd-0.57.0.json} +2 -2
  43. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionStart.json → SessionStart-0.57.0.json} +2 -2
  44. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +266 -39
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +6 -6
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +25 -11
  47. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  48. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +4 -2
  49. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +3 -1
  50. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  51. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +4 -2
  52. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +18 -0
  54. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +2 -0
  55. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +6 -2
  56. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
  57. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
  58. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
  59. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +17 -2
  60. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  61. package/node_modules/@agents-can-communicate/cli/src/args.mjs +2 -2
  62. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +67 -5
  63. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +103 -11
  64. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  65. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +52 -2
  66. package/node_modules/@agents-can-communicate/core/src/service.mjs +10 -0
  67. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  68. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +26 -12
  69. package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
  70. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
  71. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +123 -90
  72. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  73. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +48 -4
  74. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
  75. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +72 -3
  76. package/node_modules/@agents-can-communicate/installer/src/index.mjs +7 -0
  77. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
  78. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -2
  79. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +41 -6
  80. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
  81. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  82. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  83. package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
  84. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +18 -3
  85. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  86. package/package.json +2 -1
@@ -0,0 +1,229 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import net from "node:net";
3
+ import path from "node:path";
4
+
5
+ import { PROTOCOL_CONTRACT, endpointDir, isSocketSafe, readRegistration, routeAck, routeReply }
6
+ from "./channel.mjs";
7
+
8
+ // The five native-delivery adapter methods for Claude Code. They keep every
9
+ // Claude flag, protocol name, and endpoint detail inside this package and hand
10
+ // core only opaque facts: a probe verdict, an activation plan, a handshake with
11
+ // an opaque endpoint id, and an offer result. offerMessage is the sender-side
12
+ // half - it connects to the receiver's session endpoint and delivers one
13
+ // envelope; the Channel process itself turns acc_reply into a real ACC answer.
14
+
15
+ const MIN_VERSION = "2.1.258";
16
+ const CHANNEL_PLUGIN = "plugin:agents-can-communicate@acc-local";
17
+ const DEV_CHANNEL_FLAG = "--dangerously-load-development-channels";
18
+ // The bytes the installed 2.1.258 Mach-O carries for the Channel MCP protocol;
19
+ // a build without Channels has neither. Read-only, bounded.
20
+ const PROBE_NEEDLE = Buffer.from("notifications/claude/channel");
21
+ const PROBE_MAX_BYTES = 256 * 1024 * 1024;
22
+ const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
23
+
24
+ function compare(left, right) {
25
+ const a = left.split("+")[0].split(".").map(Number);
26
+ const b = right.split("+")[0].split(".").map(Number);
27
+ for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
28
+ return 0;
29
+ }
30
+
31
+ async function executableHasChannel(realExecutable) {
32
+ let handle;
33
+ try {
34
+ handle = await (await import("node:fs/promises")).open(realExecutable, "r");
35
+ } catch {
36
+ return false;
37
+ }
38
+ try {
39
+ const chunk = Buffer.alloc(1024 * 1024);
40
+ let carry = Buffer.alloc(0);
41
+ let read = 0;
42
+ let position = 0;
43
+ do {
44
+ ({ bytesRead: read } = await handle.read(chunk, 0, chunk.length, position));
45
+ if (read === 0) break;
46
+ const window = Buffer.concat([carry, chunk.subarray(0, read)]);
47
+ if (window.includes(PROBE_NEEDLE)) return true;
48
+ carry = window.subarray(Math.max(0, window.length - PROBE_NEEDLE.length));
49
+ position += read;
50
+ } while (position < PROBE_MAX_BYTES);
51
+ return false;
52
+ } finally {
53
+ await handle.close().catch(() => null);
54
+ }
55
+ }
56
+
57
+ // Read-only: a stable version at or above the minimum whose executable carries
58
+ // the Channel protocol. Never launches the client.
59
+ export async function probeNativeDelivery({ realExecutable, timeoutMs = 750,
60
+ hasChannel = executableHasChannel, readVersion = defaultReadVersion } = {}) {
61
+ const unsupported = reasonCode => ({ supported: false, clientVersion: null,
62
+ protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
63
+ if (typeof realExecutable !== "string" || realExecutable === "") return unsupported("feature_probe_failed");
64
+ const clientVersion = await withTimeout(readVersion(realExecutable, timeoutMs), timeoutMs)
65
+ .catch(() => null);
66
+ if (clientVersion === null || !STABLE_VERSION.test(clientVersion)) {
67
+ return { ...unsupported("feature_probe_failed"), clientVersion };
68
+ }
69
+ if (compare(clientVersion, MIN_VERSION) < 0) {
70
+ return { ...unsupported("below_minimum_version"), clientVersion };
71
+ }
72
+ const present = await withTimeout(Promise.resolve(hasChannel(realExecutable)), timeoutMs)
73
+ .catch(() => false);
74
+ if (!present) return { ...unsupported("protocol_mismatch"), clientVersion };
75
+ return { supported: true, clientVersion, protocolContract: PROTOCOL_CONTRACT,
76
+ executableFingerprint: null, modes: ["livePush", "idleWake", "busyQueue", "replyRoute"],
77
+ reasonCode: null };
78
+ }
79
+
80
+ function defaultReadVersion(realExecutable, timeoutMs) {
81
+ return new Promise(resolve => {
82
+ import("node:child_process").then(({ execFile }) => {
83
+ execFile(realExecutable, ["--version"], { timeout: timeoutMs, windowsHide: true },
84
+ (error, stdout, stderr) => {
85
+ if (error !== null) return resolve(null);
86
+ resolve(/(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/.exec(`${stdout}${stderr}`)?.[1] ?? null);
87
+ });
88
+ });
89
+ });
90
+ }
91
+
92
+ function withTimeout(work, ms) {
93
+ let timer = null;
94
+ const deadline = new Promise((_resolve, reject) => {
95
+ timer = setTimeout(() => reject(Object.assign(new Error("native probe timed out"),
96
+ { code: "ETIMEDOUT" })), ms);
97
+ });
98
+ return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
99
+ }
100
+
101
+ // The shell bootstrap adds only the captured Channel flag; the .mcp.json the
102
+ // adapter's own install writes is the native-config artifact. ACC never
103
+ // suppresses Claude's experimental warning.
104
+ export function planNativeActivation({ detection, livePolicy }) {
105
+ if (detection?.realExecutable === undefined || detection.realExecutable === null) {
106
+ return { eligible: false, reasonCode: "feature_probe_failed", mechanisms: [] };
107
+ }
108
+ void livePolicy;
109
+ return { eligible: true, reasonCode: null, mechanisms: [
110
+ { kind: "native-config", artifactIds: ["claude-channel-mcp"] },
111
+ { kind: "shell-bootstrap", command: "claude", realExecutable: detection.realExecutable,
112
+ prefixArgs: [DEV_CHANNEL_FLAG, CHANNEL_PLUGIN] },
113
+ ] };
114
+ }
115
+
116
+ async function currentRegistrations(runtimeDir) {
117
+ const dir = endpointDir(runtimeDir);
118
+ let names;
119
+ try { names = await readdir(dir); } catch { return []; }
120
+ const registrations = [];
121
+ for (const name of names) {
122
+ if (!name.endsWith(".json")) continue;
123
+ try {
124
+ const record = readRegistration(await readFile(path.join(dir, name), "utf8"));
125
+ registrations.push(record);
126
+ } catch { /* skip a malformed neighbour */ }
127
+ }
128
+ return registrations;
129
+ }
130
+
131
+ // Bind the exact Channel the hook-resolved Claude process owns: match clientPid,
132
+ // verify the protocol, and return an opaque endpoint id. Never selects the first
133
+ // registration; two Claude sessions cannot receive each other's endpoint.
134
+ export async function bindNativeSession({ clientPid, clientVersion, runtimeDir, timeoutMs = 750,
135
+ intervalMs = 50, now = () => Date.now(),
136
+ sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) }) {
137
+ const closed = reasonCode => ({ supported: false, clientVersion: clientVersion ?? null,
138
+ protocolContract: PROTOCOL_CONTRACT, modes: [], opaqueEndpointRef: null, leaseUntil: null,
139
+ reasonCode });
140
+ if (!Number.isInteger(clientPid) || clientPid <= 0) return closed("handshake_failed");
141
+ // Both sides of this handshake are started by SessionStart: the hook writes
142
+ // the session binding and then calls here, while the Channel is still waiting
143
+ // for that same binding before it can listen and register. So the endpoint
144
+ // reliably appears after this begins, and a single read gave up on a channel
145
+ // that was moments from ready - measured, a healthy endpoint landed on disk
146
+ // just after the hook had already recorded no binding at all.
147
+ //
148
+ // The budget was always passed in for this. It stops one interval short of
149
+ // the hook's own timer, which races the same budget: an answer that arrives
150
+ // together with that timer is a coin toss the session loses.
151
+ const deadline = now() + Math.max(0, timeoutMs - intervalMs);
152
+ for (;;) {
153
+ const matches = (await currentRegistrations(runtimeDir))
154
+ .filter(record => record.clientPid === clientPid
155
+ && record.protocolContract === PROTOCOL_CONTRACT
156
+ && Date.parse(record.leaseUntil) > Date.now());
157
+ // Exactly one, still: waiting must not relax the rule that two Claude
158
+ // sessions can never be handed each other's endpoint.
159
+ if (matches.length === 1 && isSocketSafe(matches[0].socketPath)) {
160
+ const [record] = matches;
161
+ return { supported: true, clientVersion: clientVersion ?? null,
162
+ protocolContract: PROTOCOL_CONTRACT, modes: [...record.modes],
163
+ opaqueEndpointRef: record.endpointId, leaseUntil: record.leaseUntil, reasonCode: null };
164
+ }
165
+ if (now() >= deadline) return closed("handshake_failed");
166
+ await sleep(intervalMs);
167
+ }
168
+ }
169
+
170
+ // Sender side: resolve the endpoint id to its registration under this
171
+ // workspace's runtime dir, connect to the session-scoped socket, and deliver one
172
+ // envelope. Returns only accepted/duplicate plus the bound client version.
173
+ export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 2_000,
174
+ connect = net.createConnection }) {
175
+ const rejected = safeErrorCode => ({ accepted: false, transport: "claude-channel",
176
+ clientVersion: binding?.clientVersion ?? null, safeErrorCode });
177
+ if (typeof runtimeDir !== "string" || binding?.opaqueEndpointRef === undefined) {
178
+ return rejected("recipient_unavailable");
179
+ }
180
+ const file = path.join(endpointDir(runtimeDir), `${binding.opaqueEndpointRef}.json`);
181
+ if (path.dirname(file) !== endpointDir(runtimeDir)) return rejected("recipient_unavailable");
182
+ let record;
183
+ try {
184
+ record = readRegistration(await readFile(file, "utf8"));
185
+ } catch {
186
+ return rejected("recipient_unavailable");
187
+ }
188
+ if (record.endpointId !== binding.opaqueEndpointRef
189
+ || record.protocolContract !== PROTOCOL_CONTRACT
190
+ || Date.parse(record.leaseUntil) <= Date.now()
191
+ || !(await stat(record.socketPath).then(s => s.isSocket(), () => false))
192
+ || !isSocketSafe(record.socketPath)) {
193
+ return rejected("recipient_unavailable");
194
+ }
195
+ return sendEnvelope({ record, message, binding, connect, timeoutMs, rejected });
196
+ }
197
+
198
+ function sendEnvelope({ record, message, binding, connect, timeoutMs, rejected }) {
199
+ const envelope = { nonce: record.nonce, messageId: message.messageId, kind: message.kind,
200
+ subject: message.subject ?? "", body: message.body ?? "",
201
+ ...(typeof message.inReplyTo === "string" ? { inReplyTo: message.inReplyTo } : {}) };
202
+ return new Promise(resolve => {
203
+ const socket = connect(record.socketPath);
204
+ let settled = false;
205
+ const finish = value => { if (settled) return; settled = true; clearTimeout(timer);
206
+ socket.destroy(); resolve(value); };
207
+ const timer = setTimeout(() => finish(rejected("transport_error")), timeoutMs);
208
+ socket.once("error", () => finish(rejected("recipient_unavailable")));
209
+ socket.once("connect", () => {
210
+ socket.write(`${JSON.stringify(envelope)}\n`);
211
+ let buffer = "";
212
+ socket.on("data", chunk => {
213
+ buffer += chunk.toString("utf8");
214
+ const newline = buffer.indexOf("\n");
215
+ if (newline === -1) return;
216
+ let response;
217
+ try { response = JSON.parse(buffer.slice(0, newline)); }
218
+ catch { return finish(rejected("transport_rejected")); }
219
+ if (response.accepted === true) {
220
+ return finish({ accepted: true, transport: "claude-channel",
221
+ clientVersion: binding.clientVersion });
222
+ }
223
+ finish(rejected("transport_rejected"));
224
+ });
225
+ });
226
+ });
227
+ }
228
+
229
+ export { MIN_VERSION, endpointDir, routeAck, routeReply };
@@ -12,7 +12,9 @@
12
12
  "idleBehavior": "fires when a client session starts",
13
13
  "busyBehavior": "fires before the first model turn",
14
14
  "authorityLevel": "advisory",
15
- "limitations": ["plugin hooks must be explicitly trusted by the user"],
15
+ "limitations": [
16
+ "plugin hooks must be explicitly trusted by the user"
17
+ ],
16
18
  "result": "pass"
17
19
  },
18
20
  {
@@ -27,7 +29,9 @@
27
29
  "idleBehavior": "fires when a client session exits",
28
30
  "busyBehavior": "does not run until the session exits",
29
31
  "authorityLevel": "advisory",
30
- "limitations": ["plugin hooks must be explicitly trusted by the user"],
32
+ "limitations": [
33
+ "plugin hooks must be explicitly trusted by the user"
34
+ ],
31
35
  "result": "pass"
32
36
  },
33
37
  {
@@ -42,7 +46,9 @@
42
46
  "idleBehavior": "waits for the next user prompt",
43
47
  "busyBehavior": "does not interrupt an in-progress turn",
44
48
  "authorityLevel": "context",
45
- "limitations": ["stdout arrives as an unwrapped developer-role message"],
49
+ "limitations": [
50
+ "stdout arrives as an unwrapped developer-role message"
51
+ ],
46
52
  "result": "pass"
47
53
  },
48
54
  {
@@ -57,7 +63,9 @@
57
63
  "idleBehavior": "no write exists to guard",
58
64
  "busyBehavior": "blocks an apply_patch call before disk mutation",
59
65
  "authorityLevel": "blocking",
60
- "limitations": ["runtime and unrecognised shell writes can bypass the guard"],
66
+ "limitations": [
67
+ "runtime and unrecognised shell writes can bypass the guard"
68
+ ],
61
69
  "result": "pass"
62
70
  },
63
71
  {
@@ -72,7 +80,9 @@
72
80
  "idleBehavior": "offers complete peer messages at the next prompt",
73
81
  "busyBehavior": "does not interrupt an in-progress turn",
74
82
  "authorityLevel": "context",
75
- "limitations": ["delivery requires the next normal user turn"],
83
+ "limitations": [
84
+ "delivery requires the next normal user turn"
85
+ ],
76
86
  "result": "pass"
77
87
  },
78
88
  {
@@ -112,6 +122,29 @@
112
122
  "idle, busy, reply, duplicate, and fallback branches were not observed"
113
123
  ],
114
124
  "result": "fail"
125
+ },
126
+ {
127
+ "client": "codex-cli",
128
+ "version": "0.152.1",
129
+ "platform": "darwin-arm64",
130
+ "observedAt": "2026-09-03T04:45:00.000Z",
131
+ "capability": "delivery.livePush",
132
+ "fixture": "fixtures/delivery/codex-cli-0.152.1-remote-workspace.json",
133
+ "provenance": "fixtures/certification-provenance.json",
134
+ "provenanceId": "native-delivery-0-152-1-remote-workspace",
135
+ "idleBehavior": "unobserved",
136
+ "busyBehavior": "unobserved",
137
+ "authorityLevel": "experimental",
138
+ "limitations": [
139
+ "release capture on the installed tarball, with the vendor daemon already running from a directory other than the session's",
140
+ "native delivery requires codex --remote unix://, and in that mode the session runs inside the daemon",
141
+ "the SessionStart hook payload reported cwd as the daemon's directory rather than the client's: the client was working in a temporary capture project while the payload named the daemon's own checkout",
142
+ "the App Server's own thread/list recorded that same daemon directory for the live thread, so the session's workspace is not available from anything ACC can reach",
143
+ "ACC placed the session in an unrelated workspace and injected that workspace's peers into it",
144
+ "no delivery branch could be observed because the session was never addressable from the workspace it was actually in",
145
+ "the earlier 0.152.1 pass started the daemon itself in the session's own directory, so the two directories coincided and the substitution was invisible"
146
+ ],
147
+ "result": "fail"
115
148
  }
116
149
  ]
117
150
  }
@@ -2,64 +2,197 @@
2
2
  "schemaVersion": 1,
3
3
  "captures": [
4
4
  {
5
- "id": "session-start", "client": "codex-cli", "version": "0.147.0",
6
- "platform": "darwin-arm64", "observedAt": "2026-08-16",
5
+ "id": "session-start",
6
+ "client": "codex-cli",
7
+ "version": "0.147.0",
8
+ "platform": "darwin-arm64",
9
+ "observedAt": "2026-08-16",
7
10
  "fixture": "fixtures/SessionStart.json",
8
11
  "sha256": "1580157d6b48c10f6de5f155aa1d7c68d1695409a5a178eb36c85ce5e01de956",
9
- "event": "SessionStart", "tool": null,
10
- "claims": [{ "capability": "lifecycle.sessionStart", "result": "pass",
11
- "outcome": { "kind": "event-observed", "idle": "fires when a client session starts",
12
- "busy": "fires before the first model turn", "authority": "advisory",
13
- "limitations": ["plugin hooks must be explicitly trusted by the user"] } }]
12
+ "event": "SessionStart",
13
+ "tool": null,
14
+ "claims": [
15
+ {
16
+ "capability": "lifecycle.sessionStart",
17
+ "result": "pass",
18
+ "outcome": {
19
+ "kind": "event-observed",
20
+ "idle": "fires when a client session starts",
21
+ "busy": "fires before the first model turn",
22
+ "authority": "advisory",
23
+ "limitations": [
24
+ "plugin hooks must be explicitly trusted by the user"
25
+ ]
26
+ }
27
+ }
28
+ ]
14
29
  },
15
30
  {
16
- "id": "session-end", "client": "codex-cli", "version": "0.147.0",
17
- "platform": "darwin-arm64", "observedAt": "2026-08-16",
31
+ "id": "session-end",
32
+ "client": "codex-cli",
33
+ "version": "0.147.0",
34
+ "platform": "darwin-arm64",
35
+ "observedAt": "2026-08-16",
18
36
  "fixture": "fixtures/SessionEnd.json",
19
37
  "sha256": "e1468c9d5cd2ff5408184158004e9877684f1ae2815239691bbaa386327fe58e",
20
- "event": "SessionEnd", "tool": null,
21
- "claims": [{ "capability": "lifecycle.sessionEnd", "result": "pass",
22
- "outcome": { "kind": "event-observed", "idle": "fires when a client session exits",
23
- "busy": "does not run until the session exits", "authority": "advisory",
24
- "limitations": ["plugin hooks must be explicitly trusted by the user"] } }]
38
+ "event": "SessionEnd",
39
+ "tool": null,
40
+ "claims": [
41
+ {
42
+ "capability": "lifecycle.sessionEnd",
43
+ "result": "pass",
44
+ "outcome": {
45
+ "kind": "event-observed",
46
+ "idle": "fires when a client session exits",
47
+ "busy": "does not run until the session exits",
48
+ "authority": "advisory",
49
+ "limitations": [
50
+ "plugin hooks must be explicitly trusted by the user"
51
+ ]
52
+ }
53
+ }
54
+ ]
25
55
  },
26
56
  {
27
- "id": "user-prompt-submit", "client": "codex-cli", "version": "0.147.0",
28
- "platform": "darwin-arm64", "observedAt": "2026-08-16",
57
+ "id": "user-prompt-submit",
58
+ "client": "codex-cli",
59
+ "version": "0.147.0",
60
+ "platform": "darwin-arm64",
61
+ "observedAt": "2026-08-16",
29
62
  "fixture": "fixtures/UserPromptSubmit.json",
30
63
  "sha256": "1774062b17ea917cf67d8adca3408525a928c5b9b88adda1ff6b92f3e89a459f",
31
- "event": "UserPromptSubmit", "tool": null,
64
+ "event": "UserPromptSubmit",
65
+ "tool": null,
32
66
  "claims": [
33
- { "capability": "context.beforeTurnInjection", "result": "pass",
34
- "outcome": { "kind": "model-context-observed", "idle": "waits for the next user prompt",
35
- "busy": "does not interrupt an in-progress turn", "authority": "context",
36
- "limitations": ["stdout arrives as an unwrapped developer-role message"] } },
37
- { "capability": "delivery.nextTurn", "result": "pass",
38
- "outcome": { "kind": "model-context-observed", "idle": "offers complete peer messages at the next prompt",
39
- "busy": "does not interrupt an in-progress turn", "authority": "context",
40
- "limitations": ["delivery requires the next normal user turn"] } }
67
+ {
68
+ "capability": "context.beforeTurnInjection",
69
+ "result": "pass",
70
+ "outcome": {
71
+ "kind": "model-context-observed",
72
+ "idle": "waits for the next user prompt",
73
+ "busy": "does not interrupt an in-progress turn",
74
+ "authority": "context",
75
+ "limitations": [
76
+ "stdout arrives as an unwrapped developer-role message"
77
+ ]
78
+ }
79
+ },
80
+ {
81
+ "capability": "delivery.nextTurn",
82
+ "result": "pass",
83
+ "outcome": {
84
+ "kind": "model-context-observed",
85
+ "idle": "offers complete peer messages at the next prompt",
86
+ "busy": "does not interrupt an in-progress turn",
87
+ "authority": "context",
88
+ "limitations": [
89
+ "delivery requires the next normal user turn"
90
+ ]
91
+ }
92
+ }
41
93
  ]
42
94
  },
43
95
  {
44
- "id": "pre-tool-use", "client": "codex-cli", "version": "0.147.0",
45
- "platform": "darwin-arm64", "observedAt": "2026-08-16",
96
+ "id": "pre-tool-use",
97
+ "client": "codex-cli",
98
+ "version": "0.147.0",
99
+ "platform": "darwin-arm64",
100
+ "observedAt": "2026-08-16",
46
101
  "fixture": "fixtures/PreToolUse.json",
47
102
  "sha256": "8d3785cfcffbe9affef6c10c1dbbeaf5a0b7062eaed4414e4a66c073f0397a77",
48
- "event": "PreToolUse", "tool": "apply_patch",
49
- "claims": [{ "capability": "guards.beforeWrite", "result": "pass",
50
- "outcome": { "kind": "tool-denied-before-mutation", "idle": "no write exists to guard",
51
- "busy": "blocks an apply_patch call before disk mutation", "authority": "blocking",
52
- "limitations": ["runtime and unrecognised shell writes can bypass the guard"] } }]
103
+ "event": "PreToolUse",
104
+ "tool": "apply_patch",
105
+ "claims": [
106
+ {
107
+ "capability": "guards.beforeWrite",
108
+ "result": "pass",
109
+ "outcome": {
110
+ "kind": "tool-denied-before-mutation",
111
+ "idle": "no write exists to guard",
112
+ "busy": "blocks an apply_patch call before disk mutation",
113
+ "authority": "blocking",
114
+ "limitations": [
115
+ "runtime and unrecognised shell writes can bypass the guard"
116
+ ]
117
+ }
118
+ }
119
+ ]
53
120
  },
54
121
  {
55
- "id": "native-delivery-0-152-0", "client": "codex-cli", "version": "0.152.0",
56
- "platform": "darwin-arm64", "observedAt": "2026-09-01T16:20:40.386Z",
122
+ "id": "native-delivery-0-152-0",
123
+ "client": "codex-cli",
124
+ "version": "0.152.0",
125
+ "platform": "darwin-arm64",
126
+ "observedAt": "2026-09-01T16:20:40.386Z",
57
127
  "fixture": "fixtures/delivery/codex-cli-0.152.0.json",
58
- "sha256": "e7879ed95374a4e0450a3c8c0e291357a8c9cf41f6bee90978d920a14f9dc954",
59
- "event": null, "tool": null,
128
+ "sha256": "1f3a57d5ab6fecf827fd54a1fd452c3c7a8d0ac199dd2af025f1954aa83b3cf8",
129
+ "event": null,
130
+ "tool": null,
131
+ "claims": [
132
+ {
133
+ "capability": "delivery.livePush",
134
+ "result": "fail",
135
+ "outcome": {
136
+ "kind": "native-capture-failed"
137
+ }
138
+ },
139
+ {
140
+ "capability": "delivery.replyRoute",
141
+ "result": "fail",
142
+ "outcome": {
143
+ "kind": "native-capture-failed"
144
+ }
145
+ }
146
+ ]
147
+ },
148
+ {
149
+ "id": "native-delivery-0-152-1",
150
+ "client": "codex-cli",
151
+ "version": "0.152.1",
152
+ "platform": "darwin-arm64",
153
+ "observedAt": "2026-09-02T21:32:19.320Z",
154
+ "fixture": "fixtures/delivery/codex-cli-0.152.1.json",
155
+ "sha256": "8254ce1ec25926228109edfa20f970c0036d3c2e012a0dd58af610afc57f7a39",
156
+ "event": null,
157
+ "tool": null,
158
+ "claims": [
159
+ {
160
+ "capability": "delivery.livePush",
161
+ "result": "pass",
162
+ "outcome": {
163
+ "kind": "native-delivery-observed",
164
+ "idle": "offered",
165
+ "busy": "queued_after_turn",
166
+ "authority": "experimental",
167
+ "limitations": [
168
+ "captured on darwin-arm64 only; Linux and Windows remain uncaptured",
169
+ "no daemon pre-existed; codex app-server daemon start created it for this capture and daemon stop removed it",
170
+ "the daemon speaks JSON-RPC over WebSocket on its control Unix socket; a submission queued on the idle thread started a turn by itself, and one queued during a turn was presented after that turn as a user prompt",
171
+ "the model answered through the ACC CLI (acc reply), which proves the product loop but not a native reply route; delivery.replyRoute stays uncertified",
172
+ "thread/queue/add has no native idempotency once a submission has been consumed: a retried clientUserMessageId created a second submission and a short turn but no second ACC answer; idempotency holds while the submission is still queued, where thread/queue/list exposes it"
173
+ ]
174
+ }
175
+ }
176
+ ]
177
+ },
178
+ {
179
+ "id": "native-delivery-0-152-1-remote-workspace",
180
+ "client": "codex-cli",
181
+ "version": "0.152.1",
182
+ "platform": "darwin-arm64",
183
+ "observedAt": "2026-09-03T04:45:00.000Z",
184
+ "fixture": "fixtures/delivery/codex-cli-0.152.1-remote-workspace.json",
185
+ "sha256": "e4f419111d254223f3930eeee61c30c447bb68dd51a4faf81be2a33b144bbaca",
186
+ "event": null,
187
+ "tool": null,
60
188
  "claims": [
61
- { "capability": "delivery.livePush", "result": "fail", "outcome": { "kind": "native-capture-failed" } },
62
- { "capability": "delivery.replyRoute", "result": "fail", "outcome": { "kind": "native-capture-failed" } }
189
+ {
190
+ "capability": "delivery.livePush",
191
+ "result": "fail",
192
+ "outcome": {
193
+ "kind": "native-capture-failed"
194
+ }
195
+ }
63
196
  ]
64
197
  }
65
198
  ]
@@ -3,9 +3,11 @@
3
3
  "version": "0.152.0",
4
4
  "platform": "darwin-arm64",
5
5
  "observedAt": "2026-09-01T16:20:40.386Z",
6
- "capability": "native_delivery_existing_session",
6
+ "capability": "native_delivery",
7
7
  "result": "fail",
8
8
  "fixture": "codex-cli-0.152.0-existing-session",
9
+ "launchMode": "no-client-launched",
10
+ "protocolContract": "codex-app-server-turn-start-v0",
9
11
  "idle": "unobserved",
10
12
  "busy": "unobserved",
11
13
  "reply": "unobserved",
@@ -0,0 +1,25 @@
1
+ {
2
+ "client": "codex-cli",
3
+ "version": "0.152.1",
4
+ "platform": "darwin-arm64",
5
+ "observedAt": "2026-09-03T04:45:00.000Z",
6
+ "capability": "native_delivery",
7
+ "result": "fail",
8
+ "fixture": "codex-cli-0.152.1-remote-workspace",
9
+ "launchMode": "ordinary-command-with-install-time-bootstrap",
10
+ "protocolContract": "codex-app-server-thread-queue-v1",
11
+ "idle": "unobserved",
12
+ "busy": "unobserved",
13
+ "reply": "unobserved",
14
+ "duplicate": "unobserved",
15
+ "fallback": "unobserved",
16
+ "limitations": [
17
+ "release capture on the installed tarball, with the vendor daemon already running from a directory other than the session's",
18
+ "native delivery requires codex --remote unix://, and in that mode the session runs inside the daemon",
19
+ "the SessionStart hook payload reported cwd as the daemon's directory rather than the client's: the client was working in a temporary capture project while the payload named the daemon's own checkout",
20
+ "the App Server's own thread/list recorded that same daemon directory for the live thread, so the session's workspace is not available from anything ACC can reach",
21
+ "ACC placed the session in an unrelated workspace and injected that workspace's peers into it",
22
+ "no delivery branch could be observed because the session was never addressable from the workspace it was actually in",
23
+ "the earlier 0.152.1 pass started the daemon itself in the session's own directory, so the two directories coincided and the substitution was invisible"
24
+ ]
25
+ }
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-codex",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/adapter.mjs"
7
+ ".": "./src/adapter.mjs",
8
+ "./app-server": "./src/app-server-client.mjs"
8
9
  },
9
10
  "files": [
10
11
  "certification.json",
@@ -14,6 +15,7 @@
14
15
  "fixtures/UserPromptSubmit.json",
15
16
  "fixtures/PreToolUse.json",
16
17
  "fixtures/delivery/codex-cli-0.152.0.json",
18
+ "fixtures/delivery/codex-cli-0.152.1-remote-workspace.json",
17
19
  "src/",
18
20
  "plugin/"
19
21
  ]
@@ -70,8 +70,10 @@ Participant names come from `{{ACC}} status --json`. A request is not an order.
70
70
  ## Treat delivery as evidence
71
71
 
72
72
  Every send records durably before delivery is attempted. A queued diagnostic means
73
- the message is safe in the recipient's inbox. Exact-certified clients may offer it
74
- at the next normal turn; no shipped adapter currently has certified live push.
73
+ the message is safe in the recipient's inbox. It may then be offered at the next
74
+ normal turn, or, on a client with native delivery enabled, pushed into the running
75
+ session. Delivery is behaviour, not a promise: a queued message is safe; an offered
76
+ message reached a transport but is not proof the model read it.
75
77
 
76
78
  `offered` is not read, `retrieved` is not model attention, and a reply resolves
77
79
  the communication obligation rather than proving the requested action is complete.