agents-can-communicate 0.4.4 → 0.5.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 +5 -3
- package/bin/entrypoints/acc-hook.mjs +58 -5
- package/docs/ARCHITECTURE.md +9 -4
- package/docs/CLI.md +12 -8
- package/docs/GETTING_STARTED.md +1 -1
- package/docs/PROTOCOL.md +7 -0
- package/docs/RELEASING.md +1 -1
- package/docs/SECURITY_MODEL.md +4 -2
- package/docs/TROUBLESHOOTING.md +9 -5
- package/docs/UPGRADING.md +86 -28
- 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/src/adapter.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +67 -22
- package/node_modules/@agents-can-communicate/adapter-codex/src/permission-ownership.mjs +19 -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/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/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +63 -16
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +16 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +7 -1
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +5 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +187 -11
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/diagnostics.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +17 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +14 -6
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +44 -25
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +46 -29
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance-state.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance-worker.mjs +9 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +52 -4
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/management-recovery.mjs +18 -9
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +125 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/retire.mjs +56 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/staging.mjs +95 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +8 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +18 -8
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +83 -7
- 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/router.mjs +44 -2
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +10 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +55 -6
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- 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/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +38 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ state, commonly your home directory, cannot be used as a workspace; start the cl
|
|
|
80
80
|
project directory. See [Troubleshooting](docs/TROUBLESHOOTING.md).
|
|
81
81
|
|
|
82
82
|
Already using ACC? Follow the [upgrade guide](docs/UPGRADING.md), including the 0.4.x →
|
|
83
|
-
0.
|
|
83
|
+
0.5.0 update and the data-format boundary when moving from 0.3.1.
|
|
84
84
|
|
|
85
85
|
## When messages arrive
|
|
86
86
|
|
|
@@ -141,8 +141,10 @@ One npm package. ACC needs no separate account, model API key, or hosted service
|
|
|
141
141
|
## Update or remove ACC
|
|
142
142
|
|
|
143
143
|
Initial installation enables automatic updates. ACC downloads stable releases in the
|
|
144
|
-
background
|
|
145
|
-
|
|
144
|
+
background, then switches the runtime and refreshes integrations once nothing blocks. A
|
|
145
|
+
live client or ACC process blocks only while the store contract it declares differs from
|
|
146
|
+
the incoming version's or is unknown; one declaring a matching contract keeps running
|
|
147
|
+
through the switch. When a verified Codex service needs a restart,
|
|
146
148
|
`acc update` asks once, then completes maintenance in a separate process. Open clients
|
|
147
149
|
disconnect; `acc doctor` reports progress and the result. See [update and recovery details](docs/UPGRADING.md#confirmed-client-service-maintenance).
|
|
148
150
|
Restart or resume clients afterward and complete any requested hook or plugin trust review.
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
// someone's session stops working. Unknown adapter, malformed payload, broken
|
|
7
7
|
// store, missing binding - all of them end in "allow, exit 0".
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
9
10
|
|
|
10
11
|
import { createId } from "@agents-can-communicate/protocol";
|
|
11
12
|
import { runHook } from "@agents-can-communicate/hook-runner";
|
|
13
|
+
import { hookEntrypointFor, resolvePinnedGeneration } from "@agents-can-communicate/cli";
|
|
12
14
|
|
|
13
15
|
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
14
16
|
import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
@@ -38,15 +40,66 @@ const readStdin = () => new Promise(resolve => {
|
|
|
38
40
|
|
|
39
41
|
import { completeHookOutput } from "./hook-output.mjs";
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
// A pin is keyed by harness session id, which only an adapter's own
|
|
44
|
+
// normalisation can find inside a payload. This runs the same normalisation
|
|
45
|
+
// `runHook` will run a moment later - cheap, in-memory, no store touched - so
|
|
46
|
+
// a hook that has no pin to look up pays nothing beyond it. Any adapter or
|
|
47
|
+
// payload that will not yield a session id simply has no pin to find, same as
|
|
48
|
+
// today's no-pin behaviour.
|
|
49
|
+
async function harnessSessionIdFor(adapterId, payload) {
|
|
50
|
+
try {
|
|
51
|
+
const event = await adapters[adapterId]?.normalizeHook(payload);
|
|
52
|
+
return typeof event?.sessionId === "string" ? event.sessionId : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
43
57
|
|
|
44
|
-
|
|
58
|
+
// A pinned generation is only ever another build of this same codebase, but
|
|
59
|
+
// it can still be broken on disk - deleted mid-write, a corrupt install. This
|
|
60
|
+
// tries it and reports failure rather than letting an import error or a
|
|
61
|
+
// throwing `main` escape: the caller then runs the turn on the active
|
|
62
|
+
// generation instead, exactly as if there had been no pin.
|
|
63
|
+
//
|
|
64
|
+
// `delegated` is the circuit breaker on chaining: a hook already running
|
|
65
|
+
// because it was delegated to never looks for a pin of its own. Ordinary
|
|
66
|
+
// resolution already stops itself after one hop - the generation a pin names
|
|
67
|
+
// compares its own pin against the exact root it was just handed, so they
|
|
68
|
+
// match and it returns null - but that self-termination leans on a single
|
|
69
|
+
// pin record staying the value both reads saw. `delegated` does not: it
|
|
70
|
+
// forbids a second hop outright, independent of anything a pin (rewritten
|
|
71
|
+
// mid-resolution by another hook for the same session, or simply wrong)
|
|
72
|
+
// could say. Unbounded delegation would be a hang, worse than any of the
|
|
73
|
+
// failures this file already falls open from.
|
|
74
|
+
async function delegateToPin({ managerRoot, packageRoot, adapterId, payload, delegated }) {
|
|
75
|
+
if (delegated || managerRoot === null || packageRoot === null) return false;
|
|
76
|
+
const harnessSessionId = await harnessSessionIdFor(adapterId, payload);
|
|
77
|
+
if (harnessSessionId === null) return false;
|
|
78
|
+
const pinned = await resolvePinnedGeneration({ root: managerRoot, harnessSessionId, active: packageRoot });
|
|
79
|
+
if (pinned === null) return false;
|
|
45
80
|
try {
|
|
46
|
-
|
|
81
|
+
const runtime = await import(pathToFileURL(hookEntrypointFor(pinned)).href);
|
|
82
|
+
await runtime.main({ managerRoot, packageRoot: pinned, payload, delegated: true });
|
|
83
|
+
return true;
|
|
47
84
|
} catch {
|
|
48
|
-
|
|
85
|
+
return false;
|
|
49
86
|
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function main({ managerRoot = null, packageRoot = null, payload, delegated = false } = {}) {
|
|
90
|
+
const [adapterId] = process.argv.slice(2);
|
|
91
|
+
|
|
92
|
+
// A delegating caller already read stdin once for the whole process and
|
|
93
|
+
// hands its parsed payload down; only the outermost call reads it here.
|
|
94
|
+
if (payload === undefined) {
|
|
95
|
+
try {
|
|
96
|
+
payload = JSON.parse(await readStdin());
|
|
97
|
+
} catch {
|
|
98
|
+
payload = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (await delegateToPin({ managerRoot, packageRoot, adapterId, payload, delegated })) return;
|
|
50
103
|
|
|
51
104
|
const result = await runHook({ adapterId, payload, adapters,
|
|
52
105
|
runtime: { clock: { now: () => new Date().toISOString() },
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -174,14 +174,19 @@ The CLI owns installation generations under `<dataHome>/acc/runtime`, outside wo
|
|
|
174
174
|
All five launch paths select a generation and publish an actual-process lease under one
|
|
175
175
|
admission mutex before loading workspace-capable code. Immutable launcher modules and
|
|
176
176
|
runtime directories preserve in-progress imports. Leases survive `main()` returning and
|
|
177
|
-
are removed only after confirmed process death.
|
|
178
|
-
|
|
177
|
+
are removed only after confirmed process death. A session is also pinned to the generation
|
|
178
|
+
it started with: its hooks delegate to that generation while the pin declares the same
|
|
179
|
+
store contract this one does, and run the active generation otherwise. A generation
|
|
180
|
+
directory is reclaimed once no control pointer, live lease, live pin or staging hold names
|
|
181
|
+
it. Parsed help, version, and update recovery remain available when workspace admission is
|
|
182
|
+
unavailable.
|
|
179
183
|
|
|
180
184
|
An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
|
|
181
185
|
stable package identity and the discovered integrity, and health-checks the staged runtime.
|
|
182
186
|
Installer detection and planning happen before exclusive admission. The worker then checks
|
|
183
|
-
ACC process leases and native binding PIDs
|
|
184
|
-
|
|
187
|
+
ACC process leases and native binding PIDs and waits only for those whose declared store
|
|
188
|
+
contract differs from the incoming generation's or is absent, records `activating`, applies
|
|
189
|
+
every integration, and only publishes the new active pointer after all applications succeed. Partial refresh
|
|
185
190
|
remains fenced and repairs forward, with the old active pointer retained but normal
|
|
186
191
|
workspace admission unavailable until recovery completes. Native bindings cease holding
|
|
187
192
|
after observed SessionEnd cleanup or confirmed process death; the vendor daemon may remain
|
package/docs/CLI.md
CHANGED
|
@@ -274,17 +274,21 @@ An initial `acc install` enables automatic updates. Reinstalling preserves an ex
|
|
|
274
274
|
`acc update --auto off` choice. A full uninstall pauses updates; reinstalling restores
|
|
275
275
|
the remembered setting. A background worker checks stable releases
|
|
276
276
|
at most once a day, downloads and verifies a separate runtime, then refreshes installed
|
|
277
|
-
integrations and skills
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
integrations and skills once no hold blocks. A runtime lease or native binding blocks only
|
|
278
|
+
while the store contract it declares differs from the incoming version's, or while it
|
|
279
|
+
declares none; records written before 0.5.0 declare none, so the first update after
|
|
280
|
+
upgrading still waits for them. ACC leases end on confirmed process exit; a native binding
|
|
281
|
+
can clear on observed SessionEnd even while its vendor daemon remains alive, or on
|
|
282
|
+
confirmed process death. Unknown PIDs remain holds. `finish`, presence TTL, and delivery off are not observed native end. Hooks do not
|
|
281
283
|
wait for network work. `ACC_NO_UPDATE_CHECK=1` disables update networking and background
|
|
282
284
|
scheduling.
|
|
283
285
|
|
|
284
|
-
`acc update` requests the update immediately. If a verified Codex service
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
`acc update` requests the update immediately. If a verified Codex service holds a native
|
|
287
|
+
binding that blocks activation, or serves a version its captured native-delivery contract
|
|
288
|
+
refuses, it asks once to restart that service.
|
|
289
|
+
After confirmation, a detached worker waits for idle work and for any ACC process whose
|
|
290
|
+
declared store contract still holds the update, refreshes integrations, restarts the
|
|
291
|
+
service and verifies the result. Open clients disconnect; use
|
|
288
292
|
`acc doctor` for progress and resume the clients afterward. `--yes` supplies explicit
|
|
289
293
|
noninteractive restart consent. Without consent, or with unknown ownership, the update
|
|
290
294
|
remains pending. See [maintenance limits and recovery](UPGRADING.md#confirmed-client-service-maintenance). `--check` only checks and cannot be combined with
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -15,7 +15,7 @@ pair or session-bound ACC MCP tools. See [CLI ownership](CLI.md#coordinate-from-
|
|
|
15
15
|
|
|
16
16
|
ACC requires macOS or Linux and Node.js 24 or newer.
|
|
17
17
|
|
|
18
|
-
Already using ACC? Follow the [upgrade guide](UPGRADING.md) for the 0.4.x → 0.
|
|
18
|
+
Already using ACC? Follow the [upgrade guide](UPGRADING.md) for the 0.4.x → 0.5.0
|
|
19
19
|
update or the data-format boundary when upgrading from 0.3.1.
|
|
20
20
|
|
|
21
21
|
```bash
|
package/docs/PROTOCOL.md
CHANGED
|
@@ -238,6 +238,13 @@ sessionId · generation · adapterId · clientVersion · availableModes
|
|
|
238
238
|
livePolicy · opaqueEndpointRef · leaseUntil
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
+
`clientVersion` is the version that answered the handshake and was admitted by the
|
|
242
|
+
adapter's captured contract — the process that will serve this binding's live pushes,
|
|
243
|
+
which is not always the version the client's own executable reports. A background
|
|
244
|
+
service that updated under its CLI serves the updated build, and the binding names that
|
|
245
|
+
build. The version the executable reported is recorded separately, beside the session's
|
|
246
|
+
owner record.
|
|
247
|
+
|
|
241
248
|
The recipient owns `livePolicy` because native push may start a model turn:
|
|
242
249
|
|
|
243
250
|
- `off`: inbox and normal next-turn paths only;
|
package/docs/RELEASING.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Releasing
|
|
2
2
|
|
|
3
3
|
Use this procedure to build one auditable npm artifact, verify that exact tarball, and keep
|
|
4
|
-
its evidence tied to the commit that supplied its bytes. The package is currently `0.
|
|
4
|
+
its evidence tied to the commit that supplied its bytes. The package is currently `0.5.0`;
|
|
5
5
|
the commands derive the version from `package.json` so the filename cannot drift.
|
|
6
6
|
|
|
7
7
|
```mermaid
|
package/docs/SECURITY_MODEL.md
CHANGED
|
@@ -121,8 +121,10 @@ sha512 integrity against registry metadata, installs with lifecycle scripts disa
|
|
|
121
121
|
validates the staged runtime before activation. These checks retain the registry and package
|
|
122
122
|
author as a software-supply trust boundary; they do not establish that package code is safe.
|
|
123
123
|
|
|
124
|
-
ACC process leases
|
|
125
|
-
|
|
124
|
+
ACC process leases and native bindings record the store contract they speak, and hold
|
|
125
|
+
activation while that contract differs from the incoming version's or is absent. A lease
|
|
126
|
+
ends on confirmed exit, a native binding on observed SessionEnd cleanup or confirmed process
|
|
127
|
+
death; unknown PIDs and undeclared contracts remain conservative holds.
|
|
126
128
|
`acc finish`, presence TTL, and delivery off do not establish native lifecycle end.
|
|
127
129
|
Normal installation and message delivery do not manage vendor services. Explicit
|
|
128
130
|
`acc update` maintenance requires one separate confirmation (or `--yes`) for the recorded
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -236,11 +236,15 @@ Next: [Getting started](GETTING_STARTED.md) · [Capabilities](CAPABILITIES.md)
|
|
|
236
236
|
|
|
237
237
|
## An automatic update is pending
|
|
238
238
|
|
|
239
|
-
Run `acc doctor` to see the update policy and pending notice.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
Run `acc doctor` to see the update policy and pending notice. A hold keeps an update
|
|
240
|
+
pending only while the store contract it declares differs from the incoming version's or
|
|
241
|
+
is unknown; records written before 0.5.0 declare none, so the first update after upgrading
|
|
242
|
+
still waits for all of them. `acc update` names each remaining hold with its process and
|
|
243
|
+
its declared contract. ACC process leases, including persistent MCP servers, end on
|
|
244
|
+
confirmed process exit. Native bindings clear on observed SessionEnd or confirmed process
|
|
245
|
+
death; a vendor daemon may remain alive after SessionEnd. Unknown PIDs remain holds until
|
|
246
|
+
lifecycle cleanup. Close the client sessions and ACC processes the notice names;
|
|
247
|
+
`finish`, presence TTL, and delivery off do not prove native end.
|
|
244
248
|
`acc update` can offer a confirmed restart of an eligible Codex service. Unknown or
|
|
245
249
|
unrelated holds still require lifecycle cleanup or confirmed process exit; safety holds
|
|
246
250
|
do not expire merely by elapsed time. See
|
package/docs/UPGRADING.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# Upgrading to 0.
|
|
1
|
+
# Upgrading to 0.5.0
|
|
2
2
|
|
|
3
|
-
## From 0.4.3, 0.4.2, 0.4.1 or 0.4.0
|
|
3
|
+
## From 0.4.4, 0.4.3, 0.4.2, 0.4.1 or 0.4.0
|
|
4
4
|
|
|
5
|
-
This
|
|
6
|
-
to
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
This release keeps the existing workspace data format. It changes when an update is
|
|
6
|
+
allowed to take effect: activation is gated on the store contract a running process
|
|
7
|
+
declares rather than on which version it happens to be, sessions finish on the generation
|
|
8
|
+
they started with, unreferenced generation directories are reclaimed, and `acc uninstall`
|
|
9
|
+
retires the holds its own install published. It carries the previous 0.4.x confirmed
|
|
10
|
+
service maintenance, outgoing sandbox permissions, delivery diagnostics, session recovery
|
|
11
|
+
and integration fixes.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
acc update
|
|
@@ -16,8 +16,8 @@ acc version
|
|
|
16
16
|
acc doctor
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
After publication, `acc version` should report 0.
|
|
20
|
-
select 0.
|
|
19
|
+
After publication, `acc version` should report 0.5.0. If a pin keeps an older version,
|
|
20
|
+
select 0.5.0 or clear it first. A managed update refreshes installed integrations and
|
|
21
21
|
skills; use the npm instructions below for unmanaged or pre-0.4 installations. Start
|
|
22
22
|
clients again and complete any hook/trust review they request. With existing live consent,
|
|
23
23
|
Codex 0.153.4 or newer on macOS arm64 receives local socket permissions when its workspace
|
|
@@ -44,16 +44,70 @@ In Grok, public status supplies the session's own CLI arguments through the next
|
|
|
44
44
|
hook. Grok still uses explicit inbox reads; this patch adds no external wake or guards.
|
|
45
45
|
Start clients in a project directory, rather than a home directory containing ACC state.
|
|
46
46
|
|
|
47
|
+
## What still waits for a process to exit, and what no longer does
|
|
48
|
+
|
|
49
|
+
Earlier releases held a verified update until every live ACC process and every native
|
|
50
|
+
client binding was gone. Each of those holds now states the store contract it speaks, and
|
|
51
|
+
only two kinds keep an update pending:
|
|
52
|
+
|
|
53
|
+
- A hold whose declared store contract differs from the incoming version's. This is the
|
|
54
|
+
case the gate exists for, and a release that changes the store contract still needs
|
|
55
|
+
every live process to exit.
|
|
56
|
+
- A hold that declares no contract, which includes every record written by a release
|
|
57
|
+
before this one. Unknown cannot be compared, so it stays a conservative wait.
|
|
58
|
+
|
|
59
|
+
A hold declaring the same store contract as the incoming version proceeds. Several open
|
|
60
|
+
Claude Code sessions, a Codex daemon and an idle ACC MCP server no longer have to be
|
|
61
|
+
closed together to move between releases that share a contract.
|
|
62
|
+
|
|
63
|
+
The first update after installing this release still waits for every process, because the
|
|
64
|
+
holds it has to judge were written before the contract field existed. Close the relevant
|
|
65
|
+
clients and persistent ACC processes once, or accept the eligible Codex service
|
|
66
|
+
maintenance offer described below. Updates after that do not need it.
|
|
67
|
+
|
|
68
|
+
`acc update` names each remaining hold with its process and its declared contract, so a
|
|
69
|
+
wait now states its reason rather than only the PID. `acc doctor` reports the same
|
|
70
|
+
pending notice.
|
|
71
|
+
|
|
72
|
+
A session that is open while an activation completes keeps running the generation it
|
|
73
|
+
started with, including its hooks, until it ends; it does not get half of one version and
|
|
74
|
+
half of another. A session whose pin names a generation with a different store contract
|
|
75
|
+
runs the active generation instead, which is the same behaviour as having no pin.
|
|
76
|
+
|
|
77
|
+
A restart offer for a client's background service is now made only when the version that
|
|
78
|
+
service is actually running fails that client's captured contract, or when it still holds
|
|
79
|
+
a native binding. A Codex CLI that updated while its daemon kept serving the previous
|
|
80
|
+
build is an ordinary state, and it no longer produces a prompt to disconnect open clients.
|
|
81
|
+
|
|
82
|
+
Generation directories that nothing references are removed after an activation instead of
|
|
83
|
+
accumulating. A directory is kept while any control pointer, live runtime lease, live
|
|
84
|
+
session pin or in-flight staging hold names it, and an unreadable holder postpones the
|
|
85
|
+
whole sweep rather than being assumed dead.
|
|
86
|
+
|
|
87
|
+
`acc uninstall` retires the binding records and session pins its own install published,
|
|
88
|
+
so they stop holding a later activation, when the removal leaves no client installed. It
|
|
89
|
+
never signals or terminates a client process. Removing one client while another stays
|
|
90
|
+
installed retires nothing, because a binding record names the generation that published
|
|
91
|
+
it and no client id. Runtime leases are left in place on purpose: a lease is also what
|
|
92
|
+
protects the generation a running process is executing from, and clearing it would let
|
|
93
|
+
that directory be reclaimed out from under the process. A live ACC process therefore
|
|
94
|
+
keeps holding activation across an uninstall until it exits.
|
|
95
|
+
|
|
47
96
|
## Confirmed client service maintenance
|
|
48
97
|
|
|
49
|
-
On macOS arm64, `acc update` can ask once to restart a verified Codex service that
|
|
50
|
-
activation or
|
|
51
|
-
contract
|
|
98
|
+
On macOS arm64, `acc update` can ask once to restart a verified Codex service that holds a
|
|
99
|
+
native binding blocking activation, or that is serving a version outside the captured
|
|
100
|
+
native-delivery contract. A serving version that satisfies that contract is left alone,
|
|
101
|
+
even when the installed CLI has moved to a newer build. The captured maintenance contract
|
|
102
|
+
uses Codex CLI 0.154.0 and its `pid` backend, with compatible commands and protocol
|
|
52
103
|
checked again at runtime. The installed CLI and managed executable must already match;
|
|
53
104
|
ACC does not replace vendor binaries or install an operating-system service.
|
|
54
105
|
|
|
55
106
|
After confirmation, a detached ACC worker waits up to 15 minutes for idle Codex turns,
|
|
56
|
-
empty queues
|
|
107
|
+
empty queues, the requesting command to exit, and any other ACC process still holding the
|
|
108
|
+
update. It judges those holds by the same contract rule the activation path uses, so a
|
|
109
|
+
process declaring the incoming version's store contract does not delay the restart, and
|
|
110
|
+
one declaring a differing or unknown contract does. It rechecks the approved candidate, settings,
|
|
57
111
|
PID/start time, executable, socket ownership and versions before stopping the service.
|
|
58
112
|
It refreshes integrations, starts the service again and verifies the result. If refreshing
|
|
59
113
|
integrations fails, it still restores the service and reports the incomplete update.
|
|
@@ -78,7 +132,7 @@ an update even when a native service or feature probe is temporarily unavailable
|
|
|
78
132
|
|
|
79
133
|
An old installation can already be stuck with both a pre-0.4.4 pending runtime and the
|
|
80
134
|
old updater that waits indefinitely. That code cannot discover this fix. Bootstrap the
|
|
81
|
-
new launcher once with `npm install --global agents-can-communicate@0.
|
|
135
|
+
new launcher once with `npm install --global agents-can-communicate@0.5.0`, then run
|
|
82
136
|
`acc update`. The new launcher stages its verified runtime and retires only an exactly
|
|
83
137
|
identified obsolete ACC background updater while no integrations are being written.
|
|
84
138
|
This does not terminate an unrelated client or bypass its lifetime hold.
|
|
@@ -104,7 +158,7 @@ restores access; deleting or editing the record is not a remedy.
|
|
|
104
158
|
2. Install the released package, then refresh the client integrations:
|
|
105
159
|
|
|
106
160
|
```bash
|
|
107
|
-
npm install --global agents-can-communicate@0.
|
|
161
|
+
npm install --global agents-can-communicate@0.5.0
|
|
108
162
|
acc version
|
|
109
163
|
acc install
|
|
110
164
|
```
|
|
@@ -139,13 +193,15 @@ an independent background process, normally at most once a day, downloads and ve
|
|
|
139
193
|
separate runtime copy, and refreshes the installed integrations and skills before switching.
|
|
140
194
|
The global npm package provides a launcher; the active runtime lives under ACC's data home.
|
|
141
195
|
|
|
142
|
-
Running ACC processes, including idle MCP servers, hold
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
196
|
+
Running ACC processes, including idle MCP servers, still publish a hold for as long as they
|
|
197
|
+
live, and native bindings still hold until observed client SessionEnd cleanup or confirmed
|
|
198
|
+
process death; the vendor daemon can remain running after that lifecycle event. What
|
|
199
|
+
changed is which of those holds keeps an update pending: only one whose declared store
|
|
200
|
+
contract differs from the incoming version's, or that declares none at all. Unknown PIDs
|
|
201
|
+
remain conservative holds until lifecycle cleanup. `acc finish`, presence TTL, and delivery
|
|
202
|
+
off alone still do not prove native end. When a hold does block, close the relevant client
|
|
203
|
+
sessions and persistent ACC processes to release it, or accept the eligible Codex service
|
|
204
|
+
maintenance offer in `acc update`. Background updates never request fresh restart consent.
|
|
149
205
|
Hooks never wait for a network download. If integration refresh temporarily prevents
|
|
150
206
|
coordination, a hook lets the client continue; its next genuine user turn can restore a
|
|
151
207
|
missing binding.
|
|
@@ -155,7 +211,7 @@ acc update # download and apply, or report what is keeping it
|
|
|
155
211
|
acc update --check # check without installing or changing update settings
|
|
156
212
|
acc update --auto off # disable background updates
|
|
157
213
|
acc update --auto on # enable them again
|
|
158
|
-
acc update --pin 0.
|
|
214
|
+
acc update --pin 0.5.0 # stay on this exact stable version
|
|
159
215
|
acc update --pin none # follow stable releases again
|
|
160
216
|
```
|
|
161
217
|
|
|
@@ -173,9 +229,11 @@ lifecycle scripts. `acc doctor` reports the automatic-update policy and pending
|
|
|
173
229
|
A failed download or verification keeps the active runtime. An interrupted integration
|
|
174
230
|
refresh keeps workspace admission closed until `acc update` finishes the refresh. It does
|
|
175
231
|
not roll workspace history back. A failed refresh exits with code `4` and reports the adapter
|
|
176
|
-
and configuration problem to fix before retrying; a verified update waiting for
|
|
177
|
-
remains pending.
|
|
178
|
-
|
|
232
|
+
and configuration problem to fix before retrying; a verified update waiting for a blocking
|
|
233
|
+
hold remains pending. A previous runtime version is retained for as long as anything can
|
|
234
|
+
still reach it — a control pointer, a live ACC process, or an open session pinned to it —
|
|
235
|
+
so existing launch paths survive the change; once nothing references it, its directory is
|
|
236
|
+
reclaimed after the next activation instead of accumulating.
|
|
179
237
|
|
|
180
238
|
Client trust is still controlled by the client. Follow any reported activation or hook
|
|
181
239
|
review steps; copied files do not prove the client has activated them. The initial
|
|
@@ -5,15 +5,17 @@ import certification from "../certification.json" with { type: "json" };
|
|
|
5
5
|
import { createCodexMaintenance } from "./maintenance.mjs";
|
|
6
6
|
export { sameMaintenanceIdentity } from "./maintenance.mjs";
|
|
7
7
|
|
|
8
|
-
import { PROTOCOL_CONTRACT } from "./app-server-client.mjs";
|
|
8
|
+
import { CODEX_QUEUE_MINIMUM, PROTOCOL_CONTRACT } from "./app-server-client.mjs";
|
|
9
9
|
import { allowOutcome, denyOutcome, injectOutcome, normalizeCodexHook }
|
|
10
10
|
from "./hooks.mjs";
|
|
11
11
|
import { planCodexInstall, detectCodex, installCodexPlugin, preflightCodexUninstall,
|
|
12
12
|
uninstallCodexPlugin } from "./install.mjs";
|
|
13
13
|
// Public native wiring is enabled only with installed-product capture evidence.
|
|
14
14
|
|
|
15
|
+
// Re-exported so existing importers of CODEX_QUEUE_MINIMUM keep resolving; the
|
|
16
|
+
// value now lives in app-server-client.mjs alongside the other version rules.
|
|
17
|
+
export { CODEX_QUEUE_MINIMUM };
|
|
15
18
|
export const CODEX_VERSION = "0.147.0";
|
|
16
|
-
export const CODEX_QUEUE_MINIMUM = "0.152.1";
|
|
17
19
|
export const CODEX_DELIVERY_FALLBACK = Object.freeze({
|
|
18
20
|
diagnostic: "Codex native delivery requires codex-cli 0.152.1 or newer on darwin-arm64, "
|
|
19
21
|
+ "recorded recipient consent and a reachable LocalDaemon session with exact thread, cwd, "
|
|
@@ -12,6 +12,10 @@ import { openWebSocketPeer } from "./ws-json-rpc.mjs";
|
|
|
12
12
|
|
|
13
13
|
export const PROTOCOL_CONTRACT = "codex-app-server-thread-queue-v1";
|
|
14
14
|
export const MINIMUM_VERSION = "0.152.1";
|
|
15
|
+
// The captured native-delivery contract's floor. Shared with MINIMUM_VERSION
|
|
16
|
+
// today, but named separately: this is the version a binding was verified
|
|
17
|
+
// against, not the probe's own support floor.
|
|
18
|
+
export const CODEX_QUEUE_MINIMUM = MINIMUM_VERSION;
|
|
15
19
|
export const QUEUE_MODES = Object.freeze(["livePush", "idleWake", "busyQueue"]);
|
|
16
20
|
const CLIENT_INFO = Object.freeze({ name: "agents-can-communicate", version: "0.2.0" });
|
|
17
21
|
const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
2
|
import { decisionBody } from "@agents-can-communicate/adapter-sdk";
|
|
3
3
|
|
|
4
|
-
import { MINIMUM_VERSION, PROTOCOL_CONTRACT, QUEUE_MODES,
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { CODEX_QUEUE_MINIMUM, MINIMUM_VERSION, PROTOCOL_CONTRACT, QUEUE_MODES,
|
|
5
|
+
addCodexQueueMessage, canonicalCwd, compareStableVersions, controlSocketPath,
|
|
6
|
+
locateCodexThread, openCodexAppServer, parseStableVersion, probeCodexQueue,
|
|
7
|
+
safeReason, serverVersionOf } from "./app-server-client.mjs";
|
|
7
8
|
import { newEndpointId, readNativeEndpoint, removeNativeEndpoint, socketIsReady,
|
|
8
9
|
writeNativeEndpoint } from "./native-endpoint.mjs";
|
|
9
10
|
|
|
@@ -62,13 +63,35 @@ export function planNativeActivation({ detection }) {
|
|
|
62
63
|
] };
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
// A daemon that restarted onto a newer build still satisfies the captured
|
|
67
|
+
// contract. The thread it may have lost is reported separately by
|
|
68
|
+
// locateCodexThread, so refusing on the version alone only hides the real
|
|
69
|
+
// reason. Returns the version that actually answered as `servingVersion`
|
|
70
|
+
// (null when refused) instead of mutating the endpoint it was handed, so a
|
|
71
|
+
// caller can decide what to persist on the one path that owns persistence,
|
|
72
|
+
// after its own validation has already run.
|
|
73
|
+
export async function verifyReceiver(peer, endpoint, { probe = probeCodexQueue,
|
|
74
|
+
locate = locateCodexThread } = {}) {
|
|
75
|
+
const result = await probe(peer, { threadId: endpoint.threadId });
|
|
76
|
+
if (!result.supported) {
|
|
77
|
+
// probeCodexQueue applies the same floor and answers first, so a daemon
|
|
78
|
+
// serving below the minimum arrives here as a refused probe rather than
|
|
79
|
+
// reaching the explicit check below. Reporting that as a protocol
|
|
80
|
+
// mismatch named the wrong condition everywhere it surfaced - doctor,
|
|
81
|
+
// the offer's safe error code, the recorded failure - and left the
|
|
82
|
+
// version answer this function already knows how to give unreachable.
|
|
83
|
+
return { reasonCode: result.reasonCode === "probe_timeout" ? "handshake_timeout"
|
|
84
|
+
: result.reasonCode === "below_minimum_version" ? "handshake_version_mismatch"
|
|
85
|
+
: "protocol_mismatch", servingVersion: null };
|
|
86
|
+
}
|
|
87
|
+
if (compareStableVersions(result.serverVersion, CODEX_QUEUE_MINIMUM) < 0) {
|
|
88
|
+
return { reasonCode: "handshake_version_mismatch", servingVersion: null };
|
|
89
|
+
}
|
|
90
|
+
const located = await locate(peer, { threadId: endpoint.threadId, cwd: endpoint.cwd });
|
|
91
|
+
return located.found
|
|
92
|
+
? { reasonCode: null, servingVersion: result.serverVersion }
|
|
93
|
+
: { reasonCode: located.reasonCode === "cwd_mismatch" ? "workspace_identity_unavailable" : "handshake_failed",
|
|
94
|
+
servingVersion: null };
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
export async function bindNativeSession({ event, clientPid, clientVersion, runtimeDir,
|
|
@@ -86,20 +109,41 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
|
|
|
86
109
|
const endpoint = { schemaVersion: 1, endpointId: newEndpointId(), socketPath,
|
|
87
110
|
threadId: event.sessionId, cwd, clientVersion, protocolContract: PROTOCOL_CONTRACT,
|
|
88
111
|
leaseUntil: new Date(now() + LEASE_MS).toISOString() };
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
|
|
113
|
+
if (reasonCode !== null) return rejected(reasonCode);
|
|
114
|
+
// The daemon may have restarted onto a different build since the
|
|
115
|
+
// caller's claimed clientVersion was captured; persist what actually
|
|
116
|
+
// answered so the binding and its later reads reflect the serving
|
|
117
|
+
// process, not the caller's claim.
|
|
118
|
+
const verified = { ...endpoint, clientVersion: servingVersion };
|
|
119
|
+
await writeNativeEndpoint({ runtimeDir, record: verified });
|
|
120
|
+
return handshake(verified, now);
|
|
93
121
|
});
|
|
94
122
|
} catch (error) {
|
|
95
123
|
return rejected(error?.code === "ETIMEDOUT" ? "handshake_timeout" : "handshake_failed");
|
|
96
124
|
}
|
|
97
125
|
}
|
|
98
126
|
|
|
127
|
+
// Resolving the binding's opaque reference is the whole of the identity check:
|
|
128
|
+
// the endpoint id is 128 random bits minted per bind, readNativeEndpoint
|
|
129
|
+
// re-checks that the record it read carries that exact id, and no two bindings
|
|
130
|
+
// can name the same record.
|
|
131
|
+
//
|
|
132
|
+
// The two records' clientVersion snapshots used to be compared here as well.
|
|
133
|
+
// That was never identity - the id already settles that - it was a third
|
|
134
|
+
// version rule, and the only one applied to a stored snapshot rather than to
|
|
135
|
+
// the process now answering. It is deliberately gone. It cannot refuse
|
|
136
|
+
// anything verifyReceiver does not refuse better two lines on, against the
|
|
137
|
+
// version that actually answers, with the thread and cwd, before anything is
|
|
138
|
+
// queued; and against the snapshot it could only ever be vacuous, because a
|
|
139
|
+
// record is written solely from a serving version that already cleared
|
|
140
|
+
// CODEX_QUEUE_MINIMUM. What it did do was fire on the case 0.5.0 exists to
|
|
141
|
+
// support: the endpoint records what the daemon serves, the binding recorded
|
|
142
|
+
// what `codex --version` printed, and a daemon that had updated under its CLI
|
|
143
|
+
// was refused here on that difference alone.
|
|
99
144
|
async function receiverFor(binding, runtimeDir) {
|
|
100
145
|
const endpoint = await readNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
|
|
101
|
-
return endpoint
|
|
102
|
-
&& await socketIsReady(endpoint?.socketPath) ? endpoint : null;
|
|
146
|
+
return endpoint !== null && await socketIsReady(endpoint.socketPath) ? endpoint : null;
|
|
103
147
|
}
|
|
104
148
|
|
|
105
149
|
export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 750,
|
|
@@ -109,8 +153,9 @@ export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 75
|
|
|
109
153
|
if (endpoint === null) return rejected("handshake_failed");
|
|
110
154
|
try {
|
|
111
155
|
return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
|
|
112
|
-
const
|
|
113
|
-
return
|
|
156
|
+
const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
|
|
157
|
+
return reasonCode === null
|
|
158
|
+
? handshake({ ...endpoint, clientVersion: servingVersion }, now) : rejected(reasonCode);
|
|
114
159
|
});
|
|
115
160
|
} catch (error) {
|
|
116
161
|
return rejected(error?.code === "ETIMEDOUT" ? "handshake_timeout" : "handshake_failed");
|
|
@@ -125,12 +170,12 @@ export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 5
|
|
|
125
170
|
if (endpoint === null) return rejected("recipient_unavailable");
|
|
126
171
|
try {
|
|
127
172
|
return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
:
|
|
173
|
+
const { reasonCode, servingVersion } = await verifyReceiver(peer, endpoint);
|
|
174
|
+
if (reasonCode !== null) return rejected(reasonCode === "handshake_timeout" ? "transport_error"
|
|
175
|
+
: reasonCode === "handshake_version_mismatch" ? "unsupported_client_version" : "recipient_unavailable");
|
|
131
176
|
await addCodexQueueMessage(peer, { threadId: endpoint.threadId,
|
|
132
177
|
messageId: message.messageId, text: renderText(message) });
|
|
133
|
-
return { accepted: true, transport: "codex-app-server", clientVersion:
|
|
178
|
+
return { accepted: true, transport: "codex-app-server", clientVersion: servingVersion };
|
|
134
179
|
});
|
|
135
180
|
} catch (error) {
|
|
136
181
|
if (["EPERM", "EACCES"].includes(error?.code)) return rejected("transport_permission_denied");
|