agents-can-communicate 0.8.3 → 0.8.5
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/bin/acc-update-worker.mjs +4 -1
- package/bin/entrypoints/acc-mcp.mjs +3 -0
- package/bin/entrypoints/acc.mjs +5 -0
- package/docs/ADAPTER_AUTHORING.md +3 -0
- package/docs/ARCHITECTURE.md +7 -2
- package/docs/CLI.md +7 -1
- package/docs/CONCEPTS.md +2 -2
- package/docs/CONFIGURATION.md +3 -2
- package/docs/GETTING_STARTED.md +12 -9
- package/docs/HOW_IT_WORKS.md +2 -1
- package/docs/TROUBLESHOOTING.md +18 -2
- package/docs/UPGRADING.md +35 -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/src/adapter.mjs +7 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +24 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +108 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +16 -6
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +2 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +33 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +61 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-discovery.mjs +82 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +59 -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/index.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +6 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +17 -7
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +12 -2
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +19 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +25 -8
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +13 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +19 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +53 -3
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +5 -1
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +7 -1
- 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/native-reason.mjs +22 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -3
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -3
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +123 -10
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +11 -1
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +3 -0
- 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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Private detached updater. It never opens workspace state or runs a model.
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { runWorker, recordWorkerFailure } from "@agents-can-communicate/cli/managed-worker";
|
|
5
6
|
const [root, ...extra] = process.argv.slice(2);
|
|
7
|
+
// The generation this worker belongs to, so it reclaims only as the active one.
|
|
8
|
+
const generationRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
9
|
if (typeof root === "string" && path.isAbsolute(root) && extra.length === 0) {
|
|
7
|
-
await runWorker(root, { wait: true }).catch(error => recordWorkerFailure(root, error));
|
|
10
|
+
await runWorker(root, { wait: true, generationRoot }).catch(error => recordWorkerFailure(root, error));
|
|
8
11
|
}
|
|
@@ -5,6 +5,7 @@ import { EXIT, createId } from "@agents-can-communicate/protocol";
|
|
|
5
5
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
6
6
|
import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
|
|
7
7
|
import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
|
|
8
|
+
import { registerDiscoveredSessions } from "@agents-can-communicate/hook-runner";
|
|
8
9
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
9
10
|
import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
10
11
|
from "@agents-can-communicate/cli";
|
|
@@ -60,6 +61,8 @@ await serve({
|
|
|
60
61
|
participantId,
|
|
61
62
|
descriptor,
|
|
62
63
|
runtimeDir: paths.root,
|
|
64
|
+
registerNativeSessions: () => registerDiscoveredSessions({ adapters,
|
|
65
|
+
workspaceId: descriptor.id, dataHome, env: process.env }),
|
|
63
66
|
},
|
|
64
67
|
});
|
|
65
68
|
|
package/bin/entrypoints/acc.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
|
|
|
5
5
|
import { createId } from "@agents-can-communicate/protocol";
|
|
6
6
|
import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
|
|
7
7
|
import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
|
|
8
|
+
import { registerDiscoveredSessions } from "@agents-can-communicate/hook-runner";
|
|
8
9
|
import { ALL_ADAPTERS, askConfirmation, main as runCli } from "@agents-can-communicate/cli";
|
|
9
10
|
|
|
10
11
|
export async function main({ managerRoot = null, packageRoot, managementOnly = false } = {}) {
|
|
@@ -27,6 +28,10 @@ const runtime = {
|
|
|
27
28
|
createDeliveryRouter({ service, adapters, clock,
|
|
28
29
|
readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome,
|
|
29
30
|
adapterId: adapter.id }) }),
|
|
31
|
+
// Chats a client holds before their first turn, registered before status
|
|
32
|
+
// is read so a peer can address them (#167).
|
|
33
|
+
registerNativeSessions: ({ workspaceId, dataHome }) =>
|
|
34
|
+
registerDiscoveredSessions({ adapters, workspaceId, dataHome, env: process.env }),
|
|
30
35
|
// Asked only by `acc config init`, and only when stdout is a terminal. There
|
|
31
36
|
// was no port here at all, so the question went to the fallback that always
|
|
32
37
|
// answers no: in a real terminal the command printed "not written" and never
|
|
@@ -192,6 +192,9 @@ transport that is technically different on some platform is the probe's fact to
|
|
|
192
192
|
`docs/design/2026-09-26-native-delivery-across-platforms.md` in the repository.
|
|
193
193
|
- Native methods return closed facts (`validateNativeActivationPlan()` closes the
|
|
194
194
|
activation plan) and never put vendor data - endpoints, sockets, raw errors - into core.
|
|
195
|
+
A refused handshake may add `launchOption`: the bare command-line option, such as
|
|
196
|
+
`--search`, that made the client refuse. Doctor and the sender name it; a value, a vendor
|
|
197
|
+
string or anything but an option token is rejected.
|
|
195
198
|
|
|
196
199
|
`nativeDelivery.policySource` accepts only `"installation-record"`, which is also the
|
|
197
200
|
default. Hooks and senders read the current recorded recipient consent, independently of
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -231,8 +231,13 @@ store contract this one does, and run the active generation otherwise. Apart fro
|
|
|
231
231
|
`SessionEnd`, only the client's confirmed death releases a pin, so a session whose client
|
|
232
232
|
process cannot be found is not pinned, and a pin that names no client is reaped. A
|
|
233
233
|
generation directory is reclaimed once no control pointer, live lease, live pin or staging
|
|
234
|
-
hold names it.
|
|
235
|
-
|
|
234
|
+
hold names it. An update is run by the installed code, so right after an activation the
|
|
235
|
+
activated generation's own reclaim runs from its files, and its worker reclaims on every later
|
|
236
|
+
pass; each pass is recorded in `reclaim.json`. The updating process still runs from the
|
|
237
|
+
generation it replaced, and its lease keeps that one through the pass. Such a pass, like one an
|
|
238
|
+
unknown holder postponed, is recorded as unfinished, and the next ACC command an hour or more
|
|
239
|
+
later starts the worker that finishes it. Parsed help, version, and update recovery remain
|
|
240
|
+
available when workspace admission is unavailable.
|
|
236
241
|
|
|
237
242
|
An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
|
|
238
243
|
stable package identity and the discovered integrity, and health-checks the staged runtime.
|
package/docs/CLI.md
CHANGED
|
@@ -122,7 +122,13 @@ participant for it again. A waiting participant's latest session stays listed as
|
|
|
122
122
|
with that number as `waiting`, and with `endReason` when the client said why the session
|
|
123
123
|
ended, such as `"clear"` for Claude Code's `/clear`. The text line names each one:
|
|
124
124
|
`waiting for a closed session: claude_code-b (cleared, 2)`. `acc status --all` lists every
|
|
125
|
-
session.
|
|
125
|
+
session.
|
|
126
|
+
|
|
127
|
+
Before it reads, `status` registers chats a client already holds but ACC has not seen, so a
|
|
128
|
+
peer can address a new Codex chat before anyone types in it. Codex's shared app-server daemon
|
|
129
|
+
lists them; a chat is registered when it is idle, is not a subagent, and ACC's hooks are enabled
|
|
130
|
+
and trusted in its directory. Its first turn, whether typed or started by a peer's message,
|
|
131
|
+
continues that session. `acc doctor` and the MCP `acc_status` tool do the same. Default `sync` reads
|
|
126
132
|
events after a 16-digit event cursor (100 by
|
|
127
133
|
default, up to 500). `--scope history` discovers historical message headers and reads
|
|
128
134
|
selected records; `--scope full` adds an unbounded snapshot for explicit workspace forensics.
|
package/docs/CONCEPTS.md
CHANGED
|
@@ -134,8 +134,8 @@ The durable inbox is universal. An adapter whose captures reach the installed cl
|
|
|
134
134
|
also offer messages at the next normal turn. Codex LocalDaemon and the Claude Code inbox
|
|
135
135
|
wake add experimental live delivery, off by default and subject to recipient policy and
|
|
136
136
|
current reachability. A Claude Code wake makes the session run a turn, and that turn's
|
|
137
|
-
next-turn hook shows the message. Codex can retain an eligible daemon thread
|
|
138
|
-
terminal exits. [Capabilities](CAPABILITIES.md) gives the current limits.
|
|
137
|
+
next-turn hook shows the message. Codex can retain an eligible daemon thread for about a minute
|
|
138
|
+
after its terminal exits. [Capabilities](CAPABILITIES.md) gives the current limits.
|
|
139
139
|
|
|
140
140
|
## Handoffs preserve explicit context
|
|
141
141
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -149,8 +149,9 @@ never overrides them. See
|
|
|
149
149
|
|
|
150
150
|
Codex LocalDaemon delivery separately requires Codex 0.152.1 or newer, a
|
|
151
151
|
current feature probe, and exact thread, canonical cwd, process, version and protocol
|
|
152
|
-
checks. On Codex 0.154.0
|
|
153
|
-
standalone package and prepare a definitely absent service.
|
|
152
|
+
checks. On Codex 0.154.0 through 0.157.0, explicit complete setup can download a missing
|
|
153
|
+
matching standalone package and prepare a definitely absent service. Codex 0.157.1 and newer
|
|
154
|
+
install the service's own package when it first starts, so setup only starts it. The setup choice names the
|
|
154
155
|
download. Existing service-start consent alone does not permit it. Codex files remain
|
|
155
156
|
under the selected `CODEX_HOME`; the existing CLI command and shell profiles are preserved.
|
|
156
157
|
Message delivery does not install Codex, start it, or stop it.
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -58,8 +58,9 @@ hooks wherever the client is at or after a captured version.
|
|
|
58
58
|
An interactive install asks one default-No question for all selected clients that need a
|
|
59
59
|
choice. Use `--delivery actionable|all` for explicit noninteractive consent. Use
|
|
60
60
|
`--delivery off` to disable incoming automatic requests. A dry run does not ask, write
|
|
61
|
-
configuration, download Codex, or start a service. On macOS or Linux with Codex 0.154.0
|
|
62
|
-
the choice includes downloading a missing standalone package from OpenAI
|
|
61
|
+
configuration, download Codex, or start a service. On macOS or Linux with Codex 0.154.0 through
|
|
62
|
+
0.157.0, the choice includes downloading a missing standalone package from OpenAI; Codex
|
|
63
|
+
0.157.1 and newer install the service's own package when it first starts. ACC selects the
|
|
63
64
|
matching version and keeps your existing npm or Homebrew command and shell profiles.
|
|
64
65
|
When supported Codex service preparation succeeds, it
|
|
65
66
|
means the infrastructure is ready. Start a Codex session and complete the client's hook and
|
|
@@ -146,13 +147,15 @@ Apple Silicon macOS. They are experimental and can spend tokens. Codex queues a
|
|
|
146
147
|
until a running turn finishes. A busy Claude Code session takes the wake between two tool
|
|
147
148
|
calls. Claude Code requires 2.1.282 or newer and recorded opt-in, and it wakes through the
|
|
148
149
|
inbox that each session opens itself. Codex requires 0.152.1 or newer, LocalDaemon
|
|
149
|
-
infrastructure, recorded opt-in, and a verified session.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
infrastructure, recorded opt-in, and a verified session. A new Codex chat becomes
|
|
151
|
+
addressable before its first message: `acc status` registers it. Supported explicit setup can
|
|
152
|
+
prepare a missing service. Start the client with your normal command. A loaded daemon
|
|
153
|
+
thread can receive messages for about a minute after its terminal exits, until the daemon
|
|
154
|
+
unloads it and ACC closes its session. Antigravity CLI 1.2.7 and later has one too: with
|
|
155
|
+
delivery enabled, ACC's context asks the agent to start a relay from its own shell, up to
|
|
156
|
+
three times while none is running, and you approve that command at the client's prompt.
|
|
157
|
+
Antigravity also asks before each shell command, so install adds a rule that lets ACC's own
|
|
158
|
+
commands run without that prompt, and a woken session can answer while you are away; see
|
|
156
159
|
[Antigravity CLI command approval](CONFIGURATION.md#antigravity-cli-command-approval).
|
|
157
160
|
[Capabilities](CAPABILITIES.md) explains policy, versions and fallback.
|
|
158
161
|
|
package/docs/HOW_IT_WORKS.md
CHANGED
|
@@ -115,7 +115,8 @@ All recipients have the same durable record, but adapters expose different accel
|
|
|
115
115
|
minimum of 0.152.1 plus a current probe and exact session checks allow delivery
|
|
116
116
|
to an independently opened thread. It is experimental, off by default, and can
|
|
117
117
|
spend tokens. Messages wait for a running turn to finish; a daemon-retained
|
|
118
|
-
thread can receive them after its terminal exits
|
|
118
|
+
thread can receive them for about a minute after its terminal exits, until the
|
|
119
|
+
daemon unloads it and ACC closes its session. ACC adds no launch arguments
|
|
119
120
|
and does not manage the vendor daemon during message delivery. Separately,
|
|
120
121
|
[confirmed update maintenance](UPGRADING.md#confirmed-client-service-maintenance) can restart it.
|
|
121
122
|
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -125,8 +125,9 @@ ambiguous recipients or failed identity checks retain durable inbox access.
|
|
|
125
125
|
`native_endpoint_unavailable` means the local service endpoint is missing or is not a safe socket;
|
|
126
126
|
`native_session_unavailable` means the service answered but has no loaded thread to probe.
|
|
127
127
|
An interactive install can save consent in either case. ACC preserves it through a temporary
|
|
128
|
-
outage. On Codex 0.154.0
|
|
129
|
-
standalone package and prepare a missing service.
|
|
128
|
+
outage. On Codex 0.154.0 through 0.157.0, an explicit install can download the missing official
|
|
129
|
+
standalone package and prepare a missing service. Codex 0.157.1 and newer install the
|
|
130
|
+
service's own package at its first start, and an explicit install only starts it. The single setup choice covers the
|
|
130
131
|
download. Older consent for service start receives one expanded choice. To change a saved
|
|
131
132
|
download refusal, run `acc install --adapter codex --delivery actionable` (or `all` to retain
|
|
132
133
|
that policy). ACC preserves an existing npm or Homebrew command and shell profiles.
|
|
@@ -135,6 +136,21 @@ doctor. Download failure keeps the installed integration and its inbox fallback.
|
|
|
135
136
|
service is infrastructure only. Open a new Codex session afterward and complete the
|
|
136
137
|
client-owned hook and trust review.
|
|
137
138
|
|
|
139
|
+
After a reboot no Codex service runs. Codex 0.157.1 and newer start it when the next Codex
|
|
140
|
+
session opens (`daemon_auto_start`), and doctor then says so instead of advising `acc install`.
|
|
141
|
+
A Codex chat that started without the service runs on its own embedded service until it closes,
|
|
142
|
+
and no peer can reach it live. ACC reports it as `client_session_embedded`: the chat shows the
|
|
143
|
+
user a notice once, a sender sees the cause beside `no live transport`, and doctor names it on
|
|
144
|
+
the session line. Open a new Codex chat while the service runs; start it with
|
|
145
|
+
`codex app-server daemon start` if Codex does not.
|
|
146
|
+
|
|
147
|
+
Some launch options make Codex run a chat embedded even while the service runs: `-c`/`--config`,
|
|
148
|
+
`--enable`, `--disable`, `--search`, `-p`/`--profile`, `--oss`, `--strict-config`,
|
|
149
|
+
`--dangerously-bypass-hook-trust` and `--no-daemon`, and also the `CODEX_EXEC_SERVER_URL`
|
|
150
|
+
environment variable. ACC names the option in the notice, the sender's line and doctor. For live
|
|
151
|
+
delivery, start Codex without it; put a setting you need in `config.toml` instead of `-c`,
|
|
152
|
+
`--enable`, `--disable`, `--search` or `--profile`.
|
|
153
|
+
|
|
138
154
|
If a reply stays queued with `transport_permission_denied`, the sender's permissions
|
|
139
155
|
blocked local transport. This is distinct from an unavailable recipient. The message was
|
|
140
156
|
recorded successfully; the error does not mean it was read or acknowledged.
|
package/docs/UPGRADING.md
CHANGED
|
@@ -39,6 +39,41 @@ npm_config_prefer_online=true acc update
|
|
|
39
39
|
|
|
40
40
|
From 0.8.3, the download does this itself.
|
|
41
41
|
|
|
42
|
+
0.8.4 changes what you see around Codex and after an update:
|
|
43
|
+
|
|
44
|
+
- **A new Codex chat is addressable before anyone types in it.** `acc status`, `acc doctor` and
|
|
45
|
+
the MCP `acc_status` tool register chats that Codex's daemon holds, when ACC's hooks are
|
|
46
|
+
enabled and trusted in their directory. The chat's first turn continues that session.
|
|
47
|
+
- **A Codex chat that runs without the daemon says so.** It is told once that live delivery is
|
|
48
|
+
off; a sender sees the cause beside `no live transport`. After a reboot, doctor advises
|
|
49
|
+
opening Codex rather than `acc install` when Codex starts its own service.
|
|
50
|
+
- **A new `CODEX_HOME` on Codex 0.157.1 or newer needs no standalone download.** `acc install`
|
|
51
|
+
only starts the service; Codex installs its own package.
|
|
52
|
+
- **One background pass after the update from 0.8.3.** The first client hook or agent command
|
|
53
|
+
after the update starts the new version's update worker once, even with automatic updates
|
|
54
|
+
off, to reclaim old runtime generations with the new rule, and records `reclaim.json` in the
|
|
55
|
+
runtime directory. These run through ACC's own launchers; an `acc` that npm installed
|
|
56
|
+
globally keeps the entry of the version npm installed, so it does not start this pass.
|
|
57
|
+
|
|
58
|
+
This release adds no store event type or field.
|
|
59
|
+
|
|
60
|
+
0.8.5 changes what a Codex chat that runs without the daemon is told:
|
|
61
|
+
|
|
62
|
+
- **The launch option that caused it is named.** Codex also runs a chat embedded, even while
|
|
63
|
+
its daemon runs, when it starts with `-c`/`--config`, `--enable`, `--disable`, `--search`,
|
|
64
|
+
`-p`/`--profile`, `--oss`, `--strict-config`, `--dangerously-bypass-hook-trust` or
|
|
65
|
+
`--no-daemon`. The notice, a sender's line and doctor name the option and advise starting
|
|
66
|
+
Codex without it; put a setting you need in `config.toml` instead.
|
|
67
|
+
- **You see the notice yourself.** Codex shows it once in the chat as a hook message
|
|
68
|
+
(`↳ Hook · ACC: …`); the model receives its own line, as before.
|
|
69
|
+
- **The previous runtime generation goes an hour after the update.** `acc update` runs from
|
|
70
|
+
the generation it replaces, so it cannot remove that one itself. It records the reclaim as
|
|
71
|
+
unfinished in `reclaim.json`, and the first ACC command an hour or more later removes the old
|
|
72
|
+
generation, with automatic updates on or off.
|
|
73
|
+
|
|
74
|
+
This release adds no store event type or field. A sender's delivery JSON may carry
|
|
75
|
+
`nativeLaunchOption` beside `nativeReasonCode`.
|
|
76
|
+
|
|
42
77
|
## From 0.7.x
|
|
43
78
|
|
|
44
79
|
Claude Code live delivery wakes each session through the inbox socket that Claude Code opens
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { defineAdapter, projectContext, projectContextResult }
|
|
2
2
|
from "@agents-can-communicate/adapter-sdk";
|
|
3
|
-
import { probeNativeDelivery, planNativeActivation, bindNativeSession, refreshNativeSession, retireNativeSession, offerMessage
|
|
3
|
+
import { probeNativeDelivery, planNativeActivation, bindNativeSession, refreshNativeSession, retireNativeSession, offerMessage,
|
|
4
|
+
nativeActivationHint } from "./native-delivery.mjs";
|
|
4
5
|
import certification from "../certification.json" with { type: "json" };
|
|
5
6
|
import { createCodexServiceSetup } from "./service-setup.mjs";
|
|
6
7
|
import { createCodexMaintenance } from "./maintenance.mjs";
|
|
8
|
+
import { createCodexDiscovery } from "./native-discovery.mjs";
|
|
7
9
|
export { sameMaintenanceIdentity } from "./maintenance.mjs";
|
|
8
10
|
|
|
9
11
|
import { CODEX_QUEUE_MINIMUM, PROTOCOL_CONTRACT } from "./app-server-client.mjs";
|
|
@@ -68,10 +70,14 @@ export function createCodexAdapter() {
|
|
|
68
70
|
},
|
|
69
71
|
probeNativeDelivery, planNativeActivation, bindNativeSession,
|
|
70
72
|
refreshNativeSession, retireNativeSession, offerMessage,
|
|
73
|
+
// A chat that runs embedded is told once why nothing reaches it live.
|
|
74
|
+
nativeActivationHint,
|
|
71
75
|
// A Codex sender reaches another Codex session through its control socket.
|
|
72
76
|
inboundSockets: context => [controlSocket(context)],
|
|
73
77
|
...createCodexMaintenance(),
|
|
74
78
|
...createCodexServiceSetup(),
|
|
79
|
+
// Chats the daemon holds before their first turn, for registration (#167).
|
|
80
|
+
...createCodexDiscovery(),
|
|
75
81
|
startSession: async () => ({ ok: true, changes: [], diagnostics: [] }),
|
|
76
82
|
endSession: async () => ({ ok: true, changes: [], diagnostics: [] }),
|
|
77
83
|
guardWrite: async () => ({ ok: true, changes: [], diagnostics: [] }),
|
|
@@ -9,7 +9,7 @@ import { openWebSocketPeer } from "./ws-json-rpc.mjs";
|
|
|
9
9
|
// The Codex App Server queue protocol, captured on codex-cli 0.152.1. Every
|
|
10
10
|
// method here is official and present in the generated schema: initialize,
|
|
11
11
|
// thread/loaded/list, thread/list, metadata-only thread/read, thread/queue/list,
|
|
12
|
-
// thread/queue/add. Thread history is never requested; metadata reads explicitly
|
|
12
|
+
// thread/queue/add, hooks/list. Thread history is never requested; metadata reads explicitly
|
|
13
13
|
// exclude turns. Closed safe results only; no vendor string escapes to core.
|
|
14
14
|
|
|
15
15
|
export const PROTOCOL_CONTRACT = "codex-app-server-thread-queue-v1";
|
|
@@ -109,6 +109,29 @@ function threadEntries(items) {
|
|
|
109
109
|
return items;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// What discovery may know about a loaded thread, and nothing more: never the
|
|
113
|
+
// preview, never a turn. A thread the daemon cannot describe is left out.
|
|
114
|
+
const THREAD_METADATA = ["id", "cwd", "status", "parentThreadId", "ephemeral"];
|
|
115
|
+
export async function loadedThreadMetadata(peer) {
|
|
116
|
+
const threads = [];
|
|
117
|
+
for (const threadId of await pageAll(peer, "thread/loaded/list", {})) {
|
|
118
|
+
if (typeof threadId !== "string" || threadId === "") throw protocolError();
|
|
119
|
+
const thread = (await peer.request("thread/read", { threadId, includeTurns: false }))?.thread;
|
|
120
|
+
if (thread?.id !== threadId) continue;
|
|
121
|
+
threads.push(Object.fromEntries(THREAD_METADATA.map(key => [key, thread[key] ?? null])));
|
|
122
|
+
}
|
|
123
|
+
return threads;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Each directory's hooks as this daemon would run them, with their enablement
|
|
127
|
+
// and trust (`hooks/list`, present in the 0.158.0 generated schema).
|
|
128
|
+
export async function hooksForDirectories(peer, cwds) {
|
|
129
|
+
const response = await peer.request("hooks/list", { cwds });
|
|
130
|
+
if (!Array.isArray(response?.data)) throw protocolError();
|
|
131
|
+
return response.data.map(entry => ({ cwd: entry?.cwd,
|
|
132
|
+
hooks: Array.isArray(entry?.hooks) ? entry.hooks : [] }));
|
|
133
|
+
}
|
|
134
|
+
|
|
112
135
|
export async function canonicalCwd(cwd) {
|
|
113
136
|
if (typeof cwd !== "string" || !path.isAbsolute(cwd) || cwd.includes("\0")) return null;
|
|
114
137
|
try {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { runMaintenanceCommand } from "./maintenance-host.mjs";
|
|
4
|
+
|
|
5
|
+
// Which process runs a hook tells a daemon-hosted thread from an embedded chat.
|
|
6
|
+
// Measured on 0.158.0: a daemon thread's hook runs under
|
|
7
|
+
// `codex app-server --listen unix:// --managed-daemon`, while a TUI that found
|
|
8
|
+
// no daemon (and did not start one) hosts its own app server and runs the hook
|
|
9
|
+
// itself - for the rest of its life, since a later daemon never adopts it.
|
|
10
|
+
|
|
11
|
+
// Every subcommand `codex --help` lists on 0.158.0 that runs no interactive
|
|
12
|
+
// chat. `resume` and `fork` open one, so they are absent.
|
|
13
|
+
const NOT_INTERACTIVE = new Set(["exec", "e", "review", "login", "logout", "mcp", "mcp-server",
|
|
14
|
+
"plugin", "app-server", "remote-control", "app", "completion", "update", "doctor", "sandbox",
|
|
15
|
+
"debug", "apply", "a", "queue", "archive", "delete", "migrate-rollouts", "unarchive", "cloud",
|
|
16
|
+
"exec-server", "features", "agents"]);
|
|
17
|
+
// Options whose value is the next word, so that word is not a subcommand.
|
|
18
|
+
const VALUE_OPTIONS = new Set(["-c", "--config", "-m", "--model", "-p", "--profile", "-s",
|
|
19
|
+
"--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "-i", "--image", "--add-dir", "--enable",
|
|
20
|
+
"--disable", "--local-provider"]);
|
|
21
|
+
|
|
22
|
+
export async function hostArgv(pid) {
|
|
23
|
+
const result = await runMaintenanceCommand("/bin/ps", ["-o", "args=", "-p", String(pid)],
|
|
24
|
+
{ timeout: 500 });
|
|
25
|
+
if (result.status !== 0) throw new Error("host process unreadable");
|
|
26
|
+
return result.stdout.trim().split(/\s+/).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The arguments after Codex itself, or null for any process that is not Codex:
|
|
30
|
+
// `codex ...`, or a node script named codex, as `node [options] codex.js ...`.
|
|
31
|
+
function codexArguments(argv) {
|
|
32
|
+
const [program, ...rest] = argv;
|
|
33
|
+
if (path.basename(program ?? "") === "codex") return rest;
|
|
34
|
+
if (path.basename(program ?? "") !== "node") return null;
|
|
35
|
+
const script = rest.findIndex(word => !word.startsWith("-"));
|
|
36
|
+
return script >= 0 && path.basename(rest[script]).replace(/\.[mc]?js$/, "") === "codex"
|
|
37
|
+
? rest.slice(script + 1) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether this command line is an interactive Codex TUI: no subcommand,
|
|
42
|
+
* `resume`, `fork`, or a prompt. `ps` drops quoting, so a prompt whose first
|
|
43
|
+
* word names a subcommand reads as not interactive - the safe direction, which
|
|
44
|
+
* only withholds a notice.
|
|
45
|
+
*/
|
|
46
|
+
export function interactiveHost(argv) {
|
|
47
|
+
const words = codexArguments(argv);
|
|
48
|
+
if (words === null) return false;
|
|
49
|
+
let value = false;
|
|
50
|
+
for (const word of words) {
|
|
51
|
+
if (value) { value = false; continue; }
|
|
52
|
+
if (word.startsWith("-")) { value = VALUE_OPTIONS.has(word); continue; }
|
|
53
|
+
return !NOT_INTERACTIVE.has(word);
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Options that make Codex run a chat on its own embedded app server even while
|
|
59
|
+
// the daemon runs. Measured on 0.159.1, which says so only behind its status
|
|
60
|
+
// line: "Running without the shared background server: command-line
|
|
61
|
+
// configuration overrides (-c, --enable, --disable, or --search) requires
|
|
62
|
+
// embedded mode." The same sentence names --profile, --oss, --strict-config,
|
|
63
|
+
// --dangerously-bypass-hook-trust and --no-daemon.
|
|
64
|
+
const EMBEDDING_OPTIONS = new Set(["-c", "--config", "--enable", "--disable", "--search", "-p",
|
|
65
|
+
"--profile", "--oss", "--strict-config", "--dangerously-bypass-hook-trust", "--no-daemon"]);
|
|
66
|
+
const OPENS_CHAT = new Set(["resume", "fork"]);
|
|
67
|
+
|
|
68
|
+
// `--config=x` and `-cx` carry their value; both name the option they spell.
|
|
69
|
+
const optionName = word => word.startsWith("--") ? word.split("=", 1)[0] : word.slice(0, 2);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The first option on this interactive command line that keeps the chat
|
|
73
|
+
* embedded, as typed, or null. Scanning stops at the prompt: `ps` drops quotes,
|
|
74
|
+
* so a later word may be the prompt's own text.
|
|
75
|
+
*/
|
|
76
|
+
export function embeddingOption(argv) {
|
|
77
|
+
const words = codexArguments(argv);
|
|
78
|
+
if (words === null) return null;
|
|
79
|
+
let value = false;
|
|
80
|
+
let opened = false;
|
|
81
|
+
for (const word of words) {
|
|
82
|
+
if (value) { value = false; continue; }
|
|
83
|
+
// `--` ends the options: what follows is the prompt, however it reads.
|
|
84
|
+
if (word === "--") return null;
|
|
85
|
+
if (word.startsWith("-")) {
|
|
86
|
+
if (EMBEDDING_OPTIONS.has(optionName(word))) return optionName(word);
|
|
87
|
+
value = VALUE_OPTIONS.has(word);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (opened || !OPENS_CHAT.has(word)) return null;
|
|
91
|
+
opened = true;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* For a host that was read and is an interactive TUI, `{ launchOption }` - the
|
|
98
|
+
* option that keeps it embedded, or null when its command line shows none.
|
|
99
|
+
* Null for any other host or an unreadable one; never throws.
|
|
100
|
+
*/
|
|
101
|
+
export async function embeddedHost(pid, argvOf = hostArgv) {
|
|
102
|
+
try {
|
|
103
|
+
const argv = await argvOf(pid);
|
|
104
|
+
return interactiveHost(argv) ? { launchOption: embeddingOption(argv) } : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -138,15 +138,25 @@ export function allowOutcome() {
|
|
|
138
138
|
/**
|
|
139
139
|
* Context for the next turn.
|
|
140
140
|
*
|
|
141
|
-
* This client wraps nothing: a UserPromptSubmit hook's stdout arrives at
|
|
142
|
-
* model as a `developer` role message, verbatim
|
|
143
|
-
* text
|
|
144
|
-
*
|
|
141
|
+
* This client wraps nothing: a UserPromptSubmit hook's plain stdout arrives at
|
|
142
|
+
* the model as a `developer` role message, verbatim, so the ordinary injection
|
|
143
|
+
* is plain text. Codex also parses a JSON answer - 0.147.0, 0.155.1 and 0.159.1
|
|
144
|
+
* took `hookSpecificOutput.additionalContext` into the same developer message
|
|
145
|
+
* and kept the envelope out of the conversation, measured 2026-09-29 - which
|
|
146
|
+
* is how a notice for the user rides beside the context.
|
|
145
147
|
*
|
|
146
148
|
* The developer role is the most direct channel of the four, which is a reason
|
|
147
149
|
* to be careful with what goes into it: anything a peer wrote must stay framed
|
|
148
150
|
* as data, because at this role the model reads text as instruction.
|
|
149
151
|
*/
|
|
150
|
-
export function injectOutcome(context) {
|
|
151
|
-
return { stdout: context, stderr: "", exitCode: 0 };
|
|
152
|
+
export function injectOutcome(context, notice) {
|
|
153
|
+
if (typeof notice?.userMessage !== "string") return { stdout: context, stderr: "", exitCode: 0 };
|
|
154
|
+
// A line the user must see rides as the hook's systemMessage, which Codex
|
|
155
|
+
// shows in the chat and never sends to the model; the context goes to the
|
|
156
|
+
// model as additionalContext, the same developer message plain text becomes.
|
|
157
|
+
// Measured with a probe hook on 0.147.0, 0.155.1 and 0.159.1. Only the ask's
|
|
158
|
+
// one turn prints JSON; every other turn keeps the plain text above.
|
|
159
|
+
return { stdout: JSON.stringify({ systemMessage: notice.userMessage,
|
|
160
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }),
|
|
161
|
+
stderr: "", exitCode: 0 };
|
|
152
162
|
}
|
|
@@ -32,6 +32,8 @@ const PLUGIN_NAME = "agents-can-communicate";
|
|
|
32
32
|
// again to confirm a root of ACC's own is accepted and reported enabled.
|
|
33
33
|
const MARKETPLACE = "acc-local";
|
|
34
34
|
const QUALIFIED = `${PLUGIN_NAME}@${MARKETPLACE}`;
|
|
35
|
+
// The id Codex reports for ACC's plugin, in `hooks/list` among other places.
|
|
36
|
+
export const CODEX_PLUGIN_ID = QUALIFIED;
|
|
35
37
|
const HOOK_REVIEW = "hook readiness unverified: open codex; check ACC is enabled in /plugins; in /hooks, review "
|
|
36
38
|
+ "each ACC hook and enable/trust its current definition if needed, then restart the session";
|
|
37
39
|
|
|
@@ -12,8 +12,19 @@ export const failMaintenance = reasonCode => { throw Object.assign(new Error(rea
|
|
|
12
12
|
const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
|
|
13
13
|
export const startTimeValid = value => typeof value === "string"
|
|
14
14
|
&& /^[A-Z][a-z]{2} [A-Z][a-z]{2} [ \d]\d \d\d:\d\d:\d\d \d{4}$/.test(value);
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// The two packages a Codex daemon runs from, measured. The standalone install
|
|
16
|
+
// keeps its PID in app-server.pid (0.154.0-0.159.0); a home with no standalone
|
|
17
|
+
// package gets the daemon's own package from `app-server daemon start` on
|
|
18
|
+
// 0.157.1 and newer, with its PID in daemon.pid (0.159.0, 2026-09-29). A home
|
|
19
|
+
// that has the standalone package keeps using it, so it is looked for first.
|
|
20
|
+
const DAEMON_LAYOUTS = Object.freeze([
|
|
21
|
+
{ name: "standalone", packageDir: "packages/standalone", executables: ["current/bin/codex", "current/codex"],
|
|
22
|
+
pidFile: "app-server-daemon/app-server.pid" },
|
|
23
|
+
{ name: "self-installed", packageDir: "packages/app-server-daemon", executables: ["current/bin/codex"],
|
|
24
|
+
pidFile: "app-server-daemon/daemon.pid" },
|
|
25
|
+
]);
|
|
26
|
+
export const managedExecutablePaths = codexHome => DAEMON_LAYOUTS.flatMap(layout =>
|
|
27
|
+
layout.executables.map(name => path.join(codexHome, layout.packageDir, name)));
|
|
17
28
|
|
|
18
29
|
// Where lsof lives: /usr/sbin on macOS, /usr/bin on Linux. A fixed list of
|
|
19
30
|
// absolute paths, never PATH, so a directory an operator can write to cannot
|
|
@@ -57,15 +68,28 @@ export async function maintenanceContext(context = {}) {
|
|
|
57
68
|
let codexHome = env.CODEX_HOME ?? path.join(home, ".codex");
|
|
58
69
|
if (!absolute(home) || !absolute(codexHome)) failMaintenance("invalid_service_home");
|
|
59
70
|
codexHome = await canonicalFuturePath(codexHome);
|
|
60
|
-
const
|
|
61
|
-
const managedPath = await metadataExists(binPath) ? binPath : legacyPath;
|
|
71
|
+
const { layout, managedPath } = await installedLayout(codexHome);
|
|
62
72
|
return { codexHome, socketPath: path.join(codexHome, "app-server-control/app-server-control.sock"),
|
|
63
|
-
pidPath: path.join(codexHome,
|
|
73
|
+
pidPath: path.join(codexHome, layout.pidFile), layout: layout.name,
|
|
74
|
+
packageDir: path.join(codexHome, layout.packageDir),
|
|
64
75
|
managedPath,
|
|
65
76
|
platform: context.platform ?? `${process.platform}-${process.arch}`,
|
|
66
77
|
options: { cwd: codexHome, env: { ...env, HOME: home, CODEX_HOME: codexHome } } };
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
// The first layout whose executable exists. With none installed, the
|
|
81
|
+
// standalone layout's legacy path, which service setup reports as missing.
|
|
82
|
+
async function installedLayout(codexHome) {
|
|
83
|
+
for (const layout of DAEMON_LAYOUTS) {
|
|
84
|
+
for (const name of layout.executables) {
|
|
85
|
+
const managedPath = path.join(codexHome, layout.packageDir, name);
|
|
86
|
+
if (await metadataExists(managedPath)) return { layout, managedPath };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const [standalone] = DAEMON_LAYOUTS;
|
|
90
|
+
return { layout: standalone, managedPath: path.join(codexHome, standalone.packageDir, "current/codex") };
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
export async function metadataExists(file) {
|
|
70
94
|
try { await lstat(file); return true; }
|
|
71
95
|
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
@@ -143,16 +167,16 @@ export function socketListedIn(stdout, socketPaths) {
|
|
|
143
167
|
|
|
144
168
|
// The daemon's command line as measured: 0.154.0 ran `<current path> app-server
|
|
145
169
|
// --listen unix://`; 0.157.1 runs the resolved releases path with
|
|
146
|
-
// `--managed-daemon` appended. The executable must live in the
|
|
147
|
-
// and be the managed binary itself, however
|
|
170
|
+
// `--managed-daemon` appended. The executable must live in the package tree of
|
|
171
|
+
// the installed layout and be the managed binary itself, however it is named.
|
|
148
172
|
const DAEMON_COMMAND = /^(\/.+?) app-server --listen unix:\/\/(?: --managed-daemon)?$/;
|
|
149
173
|
|
|
150
174
|
export async function verifyMaintenanceProcess(snapshot, paths, run, { lsof = resolveLsof } = {}) {
|
|
151
175
|
const process = await observeMaintenanceProcess(snapshot.pid, paths, run);
|
|
152
176
|
const executable = DAEMON_COMMAND.exec(process.command ?? "")?.[1] ?? null;
|
|
153
|
-
const
|
|
177
|
+
const packages = (paths.packageDir ?? path.join(paths.codexHome, "packages", "standalone")) + path.sep;
|
|
154
178
|
if (process.state !== "alive" || process.processStartTime !== snapshot.processStartTime
|
|
155
|
-
|| executable === null || !executable.startsWith(
|
|
179
|
+
|| executable === null || !executable.startsWith(packages)
|
|
156
180
|
|| await realpath(executable).catch(() => null) !== await realpath(paths.managedPath)) {
|
|
157
181
|
failMaintenance("daemon_identity_unavailable");
|
|
158
182
|
}
|