agents-can-communicate 0.5.5 → 0.5.7
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 +3 -2
- package/bin/entrypoints/acc-claude-channel.mjs +14 -4
- package/docs/CLI.md +12 -3
- package/docs/CONFIGURATION.md +5 -3
- package/docs/GETTING_STARTED.md +6 -3
- package/docs/TROUBLESHOOTING.md +7 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +9 -3
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +22 -3
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +56 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/standalone-install.mjs +38 -0
- 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/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +18 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +5 -1
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +6 -3
- 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/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/service-setup.mjs +7 -2
- package/node_modules/@agents-can-communicate/installer/src/verified-download.mjs +22 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,8 +68,9 @@ review the current ACC definitions in `/hooks`; changed hooks may need fresh tru
|
|
|
68
68
|
For Codex live delivery on Apple Silicon macOS, installation also configures outgoing
|
|
69
69
|
local socket access on Codex 0.153.4 or newer when using default workspace permissions.
|
|
70
70
|
Custom policies are preserved. One default-No choice covers all selected clients that need
|
|
71
|
-
live-delivery consent. On Codex 0.154.0 or newer,
|
|
72
|
-
|
|
71
|
+
live-delivery consent. On Codex 0.154.0 or newer, the choice includes downloading a missing
|
|
72
|
+
standalone package from OpenAI and starting its local service. ACC installs the matching
|
|
73
|
+
version and preserves your existing `codex` command and shell profiles. The client still owns session
|
|
73
74
|
startup, hook trust, and Claude Channels approval. Doctor reports service infrastructure,
|
|
74
75
|
session binding, and outgoing permissions separately.
|
|
75
76
|
|
|
@@ -11,11 +11,14 @@ import { randomBytes } from "node:crypto";
|
|
|
11
11
|
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
12
12
|
import { listSessionBindings } from "@agents-can-communicate/adapter-sdk";
|
|
13
13
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
14
|
+
import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
|
|
15
|
+
import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
|
|
16
|
+
import { recordAndOffer } from "@agents-can-communicate/mcp-server";
|
|
14
17
|
import { resolveClientPid } from "@agents-can-communicate/hook-runner/client-pid";
|
|
15
18
|
import { readProcessTable } from "@agents-can-communicate/hook-runner/process-table";
|
|
16
19
|
import { createId } from "@agents-can-communicate/protocol";
|
|
17
20
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
18
|
-
import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
21
|
+
import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
19
22
|
from "@agents-can-communicate/cli";
|
|
20
23
|
|
|
21
24
|
import { createAccChannel, endpointDir, routeAck, routeReply }
|
|
@@ -105,8 +108,8 @@ export async function resolveWithin({ resolve, deadline, intervalMs = BINDING_PO
|
|
|
105
108
|
async function compose() {
|
|
106
109
|
const descriptor = await discoverWorkspace({ cwd: process.cwd(), env: process.env,
|
|
107
110
|
gitProbe: createGitProbe() });
|
|
108
|
-
const
|
|
109
|
-
|
|
111
|
+
const dataHome = platformDataHome({ platform: process.platform, env: process.env });
|
|
112
|
+
const paths = runtimePaths({ dataHome, workspaceId: descriptor.id, workspaceRoots: descriptor.roots });
|
|
110
113
|
const store = await openFilesystemStore({ root: paths.root, clock, ids,
|
|
111
114
|
workspaceId: descriptor.id });
|
|
112
115
|
const service = createCoordinationService({ store, clock, ids });
|
|
@@ -126,11 +129,18 @@ async function compose() {
|
|
|
126
129
|
return startInertChannel({ write });
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
const deliveryRouter = createDeliveryRouter({ service, adapters: ALL_ADAPTERS(), clock,
|
|
133
|
+
readLivePolicy: ({ adapter }) => readInstalledLivePolicy({ dataHome, adapterId: adapter.id }) });
|
|
129
134
|
const channel = createAccChannel({
|
|
130
135
|
endpointDir: endpointDir(paths.root),
|
|
131
136
|
clientPid: session.clientPid,
|
|
132
137
|
write,
|
|
133
|
-
|
|
138
|
+
// The Channel tool records through its own bound session, then uses the
|
|
139
|
+
// same durable-first offer as ordinary MCP. A successful reply record alone
|
|
140
|
+
// does not deliver it to the peer, and a failed offer must not lose it.
|
|
141
|
+
routeReply: ({ messageId, body }) => recordAndOffer({ router: deliveryRouter,
|
|
142
|
+
selectMessage: value => value.reply,
|
|
143
|
+
record: () => routeReply({ service, session, messageId, body }) }),
|
|
134
144
|
routeAck: ({ messageId }) => routeAck({ service, session, messageId }),
|
|
135
145
|
// This process is the only party that knows the endpoint is still being
|
|
136
146
|
// served, and Claude publishes no heartbeat, so nothing else would move
|
package/docs/CLI.md
CHANGED
|
@@ -260,9 +260,18 @@ zsh PATH block and a shim that `exec`s the real client; `ACC_BYPASS=1` bypasses
|
|
|
260
260
|
activation. Codex LocalDaemon delivery uses recorded installation consent without changing
|
|
261
261
|
ordinary launch arguments. Its opt-in remains active when shim variables are absent or
|
|
262
262
|
bypassed; `acc install --adapter codex --delivery off` disables new native offers. On a
|
|
263
|
-
supported explicit install, complete consent can prepare a missing Codex service
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
supported explicit install, complete consent can prepare a missing Codex service. On macOS
|
|
264
|
+
arm64 with Codex 0.154.0 or newer, the same choice includes downloading a missing standalone
|
|
265
|
+
package from OpenAI. ACC selects the installed CLI version and verifies the official
|
|
266
|
+
installer's pinned checksum. The installer uses the selected `HOME` and `CODEX_HOME`.
|
|
267
|
+
Your existing npm or Homebrew command and shell profiles remain unchanged.
|
|
268
|
+
An older consent that covered service start receives one expanded choice for the download.
|
|
269
|
+
An accepted or declined answer persists. To change a declined answer, use
|
|
270
|
+
`acc install --adapter codex --delivery actionable` (or `all` for that policy).
|
|
271
|
+
Explicit `--delivery actionable|all` also approves a required download on supported clients.
|
|
272
|
+
Dry runs, delivery off, doctor, automatic refresh, and message delivery do not download
|
|
273
|
+
Codex or start its service. Uninstall does not remove Codex or stop the shared vendor daemon.
|
|
274
|
+
The install summary names each client's requested policy,
|
|
266
275
|
activation state and verified fallback. `doctor` separates protocol readiness, recorded
|
|
267
276
|
consent and a live channel in the current workspace, with a next step for missing activation.
|
|
268
277
|
A supported version or an installed plugin alone is not an active delivery channel.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -139,9 +139,11 @@ reports degraded reachability; it does not rewrite the installed consent.
|
|
|
139
139
|
|
|
140
140
|
Codex LocalDaemon delivery separately requires macOS arm64, Codex 0.152.1 or newer, a
|
|
141
141
|
current feature probe, and exact thread, canonical cwd, process, version and protocol
|
|
142
|
-
checks.
|
|
143
|
-
|
|
144
|
-
start
|
|
142
|
+
checks. On Codex 0.154.0 or newer, explicit complete setup can download a missing matching
|
|
143
|
+
standalone package and prepare a definitely absent service. The setup choice names the
|
|
144
|
+
download. Existing service-start consent alone does not permit it. Codex files remain
|
|
145
|
+
under the selected `CODEX_HOME`; the existing CLI command and shell profiles are preserved.
|
|
146
|
+
Message delivery does not install Codex, start it, or stop it.
|
|
145
147
|
Explicit update maintenance can restart a verified service after separate confirmation. Codex reads
|
|
146
148
|
consent from the installation record, not a shell-shim variable. Unavailable or ineligible
|
|
147
149
|
sessions retain durable inbox fallback. Use `acc install --adapter codex --delivery off`
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -58,10 +58,13 @@ hooks only where the exact client version and platform are certified.
|
|
|
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, or start a service.
|
|
61
|
+
configuration, download Codex, or start a service. On macOS arm64 with Codex 0.154.0 or newer,
|
|
62
|
+
the choice includes downloading a missing standalone package from OpenAI. ACC selects the
|
|
63
|
+
matching version and keeps your existing npm or Homebrew command and shell profiles.
|
|
64
|
+
When supported Codex service preparation succeeds, it
|
|
62
65
|
means the infrastructure is ready. Start a Codex session and complete the client's hook and
|
|
63
|
-
trust review to establish a session-bound channel.
|
|
64
|
-
|
|
66
|
+
trust review to establish a session-bound channel. An incomplete existing standalone
|
|
67
|
+
installation or unsupported CLI version still needs the vendor action named by install.
|
|
65
68
|
|
|
66
69
|
If you opt into Claude Code's experimental idle delivery, check Claude's Channels startup
|
|
67
70
|
notice for ACC and accept its development-channel warning when shown. If it reports Channels
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -98,9 +98,13 @@ ambiguous recipients or failed identity checks retain durable inbox access.
|
|
|
98
98
|
`native_endpoint_unavailable` means the local service endpoint is missing or is not a safe socket;
|
|
99
99
|
`native_session_unavailable` means the service answered but has no loaded thread to probe.
|
|
100
100
|
An interactive install can save consent in either case. ACC preserves it through a temporary
|
|
101
|
-
outage. On Codex 0.154.0 or newer, an explicit install
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
outage. On Codex 0.154.0 or newer, an explicit install can download the missing official
|
|
102
|
+
standalone package and prepare a missing service. The single setup choice covers the
|
|
103
|
+
download. Older consent for service start receives one expanded choice. To change a saved
|
|
104
|
+
download refusal, run `acc install --adapter codex --delivery actionable` (or `all` to retain
|
|
105
|
+
that policy). ACC preserves an existing npm or Homebrew command and shell profiles.
|
|
106
|
+
An unsafe or incomplete existing installation still requires the vendor action named by
|
|
107
|
+
doctor. Download failure keeps the installed integration and its inbox fallback. A ready
|
|
104
108
|
service is infrastructure only. Open a new Codex session afterward and complete the
|
|
105
109
|
client-owned hook and trust review.
|
|
106
110
|
|
|
@@ -196,11 +196,17 @@ export function createAccChannel({ endpointDir, socketDir = channelSocketDirecto
|
|
|
196
196
|
if (!nonEmpty(args.messageId)) throw new Error(`${name} requires messageId`);
|
|
197
197
|
if (name === "acc_reply" && !nonEmpty(args.body)) throw new Error("acc_reply requires body");
|
|
198
198
|
if (!seen.has(args.messageId)) throw new Error(`${args.messageId} has no delivered message`);
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const routed = name === "acc_reply"
|
|
200
|
+
? await routeReply({ messageId: args.messageId, body: args.body })
|
|
201
|
+
: await routeAck({ messageId: args.messageId });
|
|
201
202
|
observe({ event: name === "acc_reply" ? "reply_routed" : "ack_routed", at: clock(),
|
|
202
203
|
messageId: args.messageId });
|
|
203
|
-
|
|
204
|
+
if (name === "acc_reply" && routed?.recorded?.reply) {
|
|
205
|
+
const structuredContent = { status: "recorded", messageId: routed.recorded.reply.messageId,
|
|
206
|
+
delivery: routed.delivery };
|
|
207
|
+
return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent };
|
|
208
|
+
}
|
|
209
|
+
return { content: [{ type: "text", text: name === "acc_reply" ? "recorded" : "acknowledged" }] };
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
function channelTools() {
|
|
@@ -22,13 +22,27 @@ export function runMaintenanceCommand(command, args, options) {
|
|
|
22
22
|
(typeof error.code === "number" ? error.code : null) : 0, stdout, stderr })));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
async function canonicalFuturePath(file) {
|
|
26
|
+
const missing = []; let current = file;
|
|
27
|
+
while (true) {
|
|
28
|
+
try { return path.join(await realpath(current), ...missing); }
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== "ENOENT") throw error;
|
|
31
|
+
// A dangling symlink is an existing unknown destination, not a missing directory.
|
|
32
|
+
if (await metadataExists(current)) failMaintenance("invalid_service_home");
|
|
33
|
+
const parent = path.dirname(current);
|
|
34
|
+
if (parent === current) throw error;
|
|
35
|
+
missing.unshift(path.basename(current)); current = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
export async function maintenanceContext(context = {}) {
|
|
26
41
|
const env = context.env ?? process.env;
|
|
27
42
|
const home = context.home ?? env.HOME ?? os.homedir();
|
|
28
43
|
let codexHome = env.CODEX_HOME ?? path.join(home, ".codex");
|
|
29
44
|
if (!absolute(home) || !absolute(codexHome)) failMaintenance("invalid_service_home");
|
|
30
|
-
|
|
31
|
-
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
45
|
+
codexHome = await canonicalFuturePath(codexHome);
|
|
32
46
|
const [binPath, legacyPath] = managedExecutablePaths(codexHome);
|
|
33
47
|
const managedPath = await metadataExists(binPath) ? binPath : legacyPath;
|
|
34
48
|
return { codexHome, socketPath: path.join(codexHome, "app-server-control/app-server-control.sock"),
|
|
@@ -59,7 +73,7 @@ async function resolveCli(pathEnv) {
|
|
|
59
73
|
const cliVersion = response => response.status === 0
|
|
60
74
|
? /^codex-cli (\d+\.\d+\.\d+)\s*$/.exec(response.stdout)?.[1] ?? null : null;
|
|
61
75
|
|
|
62
|
-
export async function
|
|
76
|
+
export async function probeMaintenanceCli(paths, run) {
|
|
63
77
|
const cliPath = await resolveCli(paths.options.env.PATH);
|
|
64
78
|
const version = cliVersion(await run(cliPath, ["--version"], paths.options));
|
|
65
79
|
if (!parseStableVersion(version) || compareStableVersions(version, MINIMUM_MAINTENANCE_CLI) < 0) {
|
|
@@ -68,6 +82,11 @@ export async function probeMaintenanceInstall(paths, run) {
|
|
|
68
82
|
const help = await run(cliPath, ["app-server", "daemon", "--help"], paths.options);
|
|
69
83
|
if (help.status !== 0 || ["start", "stop", "version"].some(command =>
|
|
70
84
|
!new RegExp(`^\\s+${command}\\s+`, "m").test(help.stdout))) failMaintenance("maintenance_cli_unsupported");
|
|
85
|
+
return { cliPath, cliVersion: version };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function probeMaintenanceInstall(paths, run) {
|
|
89
|
+
const { cliPath, cliVersion: version } = await probeMaintenanceCli(paths, run);
|
|
71
90
|
const managedVersion = cliVersion(await run(paths.managedPath, ["--version"], paths.options));
|
|
72
91
|
if (managedVersion !== version) failMaintenance("managed_binary_mismatch");
|
|
73
92
|
return { cliPath, cliVersion: version, managedVersion };
|
|
@@ -2,17 +2,24 @@ import { lstat, realpath } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { socketIsReady } from "./native-endpoint.mjs";
|
|
4
4
|
import { probeNativeDelivery } from "./native-delivery.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { installCodexStandalone } from "./standalone-install.mjs";
|
|
6
|
+
import { failMaintenance, maintenanceContext, probeMaintenanceCli, probeMaintenanceInstall,
|
|
6
7
|
readMaintenancePid, runMaintenanceCommand, verifyMaintenanceProcess } from "./maintenance-host.mjs";
|
|
7
8
|
|
|
8
9
|
const keys = ["home", "codexHome", "cliPath", "cliVersion", "managedPath", "managedVersion",
|
|
9
10
|
"managedRealPath", "cliIdentity", "managedIdentity", "socketPath", "pidPath", "platform"];
|
|
11
|
+
const prerequisiteKeys = ["home", "codexHome", "cliPath", "cliVersion", "cliIdentity", "socketPath", "pidPath", "platform"];
|
|
10
12
|
const same = (a, b, fields = keys) => fields.every(key => a?.[key] === b?.[key]);
|
|
11
13
|
const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
|
|
12
14
|
const sessionNeeded = "Codex service ready; open a new Codex session to establish its delivery binding and review client hooks and permissions";
|
|
13
15
|
const diagnostics = {
|
|
14
16
|
native_endpoint_unavailable: "Prepare the missing Codex service with codex app-server daemon start, then open a new Codex session",
|
|
15
|
-
managed_install_missing: "Codex service
|
|
17
|
+
managed_install_missing: "Codex service needs its standalone package; run acc install to download the matching official Codex release and prepare the service",
|
|
18
|
+
managed_install_incomplete: "Codex standalone installation is incomplete; repair it with the official Codex installer, then retry acc install",
|
|
19
|
+
prerequisite_consent_required: "Codex standalone download was not approved. Run acc install --adapter codex --delivery actionable to allow the download and automatic peer requests",
|
|
20
|
+
prerequisite_download_failed: "Could not download the official Codex installer; check your connection and retry acc install",
|
|
21
|
+
prerequisite_integrity_failed: "The Codex installer checksum did not match the reviewed version; no installer was run",
|
|
22
|
+
prerequisite_install_failed: "The official Codex installation did not complete; check connectivity and the selected Codex home, then retry acc install",
|
|
16
23
|
managed_binary_mismatch: "Codex CLI and managed standalone versions differ; repair the vendor installation, then retry acc install",
|
|
17
24
|
maintenance_platform_unsupported: "Codex service preparation is captured only on darwin-arm64; configure the vendor service manually",
|
|
18
25
|
maintenance_cli_unsupported: "Codex cold service preparation requires codex-cli 0.154.0 or newer with daemon commands; update the vendor installation, then retry acc install",
|
|
@@ -25,7 +32,8 @@ function report(state, reasonCode, facts = {}) {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = probeNativeDelivery,
|
|
28
|
-
fs = { lstat, realpath }, contextPaths = maintenanceContext
|
|
35
|
+
fs = { lstat, realpath }, contextPaths = maintenanceContext,
|
|
36
|
+
installStandalone = installCodexStandalone } = {}) {
|
|
29
37
|
async function info(file) {
|
|
30
38
|
try { return await fs.lstat(file); }
|
|
31
39
|
catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
@@ -54,6 +62,22 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
54
62
|
cliIdentity: cli.identity, managedIdentity: managed.identity,
|
|
55
63
|
socketPath: paths.socketPath, pidPath: paths.pidPath, platform: paths.platform, ...installation };
|
|
56
64
|
}
|
|
65
|
+
async function missingPrerequisite(paths) {
|
|
66
|
+
for (const name of ["packages", "packages/standalone", "packages/standalone/bin", "packages/standalone/releases"]) {
|
|
67
|
+
const stat = await info(path.join(paths.codexHome, name));
|
|
68
|
+
if (stat && (!stat.isDirectory() || stat.isSymbolicLink() || !own(stat) || (stat.mode & 0o022))) {
|
|
69
|
+
failMaintenance("unsafe_service_directory");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (await info(path.join(paths.codexHome, "packages/standalone/current"))) failMaintenance("managed_install_incomplete");
|
|
73
|
+
if (await info(paths.pidPath) || await info(paths.socketPath)) failMaintenance("daemon_identity_unavailable");
|
|
74
|
+
const installation = await probeMaintenanceCli({ ...paths,
|
|
75
|
+
options: { ...paths.options, cwd: paths.options.env.HOME } }, run);
|
|
76
|
+
const cli = await executableIdentity(installation.cliPath);
|
|
77
|
+
return report("needed", "managed_install_missing", { requiresInstall: true,
|
|
78
|
+
home: paths.options.env.HOME, codexHome: paths.codexHome, cliIdentity: cli.identity,
|
|
79
|
+
socketPath: paths.socketPath, pidPath: paths.pidPath, platform: paths.platform, ...installation });
|
|
80
|
+
}
|
|
57
81
|
async function verify(paths, facts) {
|
|
58
82
|
if (!await socketIsReady(paths.socketPath)) failMaintenance("daemon_socket_unproven");
|
|
59
83
|
const version = await run(facts.cliPath, ["app-server", "daemon", "version"], paths.options);
|
|
@@ -86,6 +110,8 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
86
110
|
// service needs no new managed-install or cold-start prerequisite.
|
|
87
111
|
if (native.supported) return report("ready", native.reasonCode ?? null);
|
|
88
112
|
}
|
|
113
|
+
await safeDirectories(paths);
|
|
114
|
+
if (!await info(paths.managedPath)) return await missingPrerequisite(paths);
|
|
89
115
|
const facts = await installed(paths);
|
|
90
116
|
if (!await info(paths.socketPath) && !await info(paths.pidPath)) {
|
|
91
117
|
return report("needed", "native_endpoint_unavailable", facts);
|
|
@@ -96,7 +122,33 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
96
122
|
return report(reason.endsWith("_unsupported") ? "unsupported" : "blocked", reason);
|
|
97
123
|
}
|
|
98
124
|
}
|
|
99
|
-
async function
|
|
125
|
+
async function preparePrerequisite(context, plan, consent, download) {
|
|
126
|
+
let attempted = false;
|
|
127
|
+
try {
|
|
128
|
+
if (consent !== true) failMaintenance("prerequisite_consent_required");
|
|
129
|
+
if (plan.state !== "needed" || !prerequisiteKeys.every(key => typeof plan[key] === "string")) {
|
|
130
|
+
failMaintenance("invalid_service_setup_plan");
|
|
131
|
+
}
|
|
132
|
+
const beforeInstall = async () => {
|
|
133
|
+
const current = await inspect(context, { strict: true });
|
|
134
|
+
if (current.state !== "needed" || current.requiresInstall !== true
|
|
135
|
+
|| !same(plan, current, prerequisiteKeys)) failMaintenance("service_identity_changed");
|
|
136
|
+
};
|
|
137
|
+
await beforeInstall();
|
|
138
|
+
attempted = true;
|
|
139
|
+
await installStandalone(plan, { env: context?.env ?? process.env, beforeInstall, download });
|
|
140
|
+
const next = await inspect(context, { strict: true });
|
|
141
|
+
if (!["needed", "ready"].includes(next.state) || next.requiresInstall
|
|
142
|
+
|| !same(plan, next, prerequisiteKeys)) failMaintenance("service_identity_changed");
|
|
143
|
+
const result = await prepareNativeServiceSetup({ context, plan: next });
|
|
144
|
+
return { ...result, installedPrerequisite: true };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return { ...report(attempted ? "failed" : "blocked", error.reasonCode ?? "prerequisite_install_failed"),
|
|
147
|
+
started: false, installedPrerequisite: false };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function prepareNativeServiceSetup({ context, plan, installPrerequisites, download } = {}) {
|
|
151
|
+
if (plan?.requiresInstall === true) return preparePrerequisite(context, plan, installPrerequisites, download);
|
|
100
152
|
let started = false;
|
|
101
153
|
try {
|
|
102
154
|
if (plan?.state === "ready") {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { failMaintenance, runMaintenanceCommand } from "./maintenance-host.mjs";
|
|
5
|
+
|
|
6
|
+
// Pinned official installer: its noninteractive mode retains other installations.
|
|
7
|
+
// Keep the script and its behavior review separate from the selected CLI version.
|
|
8
|
+
export const CODEX_INSTALLER = Object.freeze({
|
|
9
|
+
url: "https://raw.githubusercontent.com/openai/codex/rust-v0.154.0/scripts/install/install.sh",
|
|
10
|
+
sha256: "ba92dd27e5c06f0d3bbc58bfa4b9cfb6599cd2742fbb1f92a2765e6c07dedb5a",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export async function installCodexStandalone(plan, { env, beforeInstall,
|
|
14
|
+
download, run = runMaintenanceCommand, source = CODEX_INSTALLER } = {}) {
|
|
15
|
+
let directory;
|
|
16
|
+
try {
|
|
17
|
+
if (typeof download !== "function") failMaintenance("prerequisite_download_unavailable");
|
|
18
|
+
const script = await download(source);
|
|
19
|
+
directory = await mkdtemp(path.join(os.tmpdir(), "acc-codex-installer-"));
|
|
20
|
+
const file = path.join(directory, "install.sh");
|
|
21
|
+
await writeFile(file, script, { mode: 0o600, flag: "wx" });
|
|
22
|
+
await beforeInstall();
|
|
23
|
+
const bin = path.join(plan.codexHome, "packages/standalone/bin");
|
|
24
|
+
const result = await run("/bin/sh", [file, "--release", plan.cliVersion], {
|
|
25
|
+
cwd: plan.home, timeout: 300_000,
|
|
26
|
+
env: { ...env, HOME: plan.home, CODEX_HOME: plan.codexHome,
|
|
27
|
+
CODEX_RELEASE: plan.cliVersion, CODEX_NON_INTERACTIVE: "1",
|
|
28
|
+
CODEX_INSTALLER_USE_RELEASES_OPENAI_COM: "true", CODEX_INSTALL_DIR: bin,
|
|
29
|
+
// The official installer skips profile edits when its bin is on PATH.
|
|
30
|
+
// This PATH belongs only to the installer; the user's command is retained.
|
|
31
|
+
PATH: `${bin}:/usr/bin:/bin:/usr/sbin:/sbin` },
|
|
32
|
+
});
|
|
33
|
+
if (result.status !== 0) failMaintenance("prerequisite_install_failed");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.reasonCode) throw error;
|
|
36
|
+
failMaintenance("prerequisite_install_failed");
|
|
37
|
+
} finally { if (directory) await rm(directory, { recursive: true, force: true }); }
|
|
38
|
+
}
|
|
@@ -20,6 +20,9 @@ function validDecision(record) {
|
|
|
20
20
|
if (!(decision !== null && typeof decision === "object"
|
|
21
21
|
&& DECISION_SOURCES.has(decision.source)
|
|
22
22
|
&& typeof decision.completeSetup === "boolean")) return false;
|
|
23
|
+
if (Object.hasOwn(decision, "installPrerequisites")
|
|
24
|
+
&& (typeof decision.installPrerequisites !== "boolean"
|
|
25
|
+
|| (decision.installPrerequisites && !decision.completeSetup))) return false;
|
|
23
26
|
|
|
24
27
|
const enabled = livePolicyOf(record) !== "off";
|
|
25
28
|
if (decision.source === "interactive-accepted") return decision.completeSetup && enabled;
|
|
@@ -34,7 +37,9 @@ function validDecision(record) {
|
|
|
34
37
|
export function decisionOf(record) {
|
|
35
38
|
return validDecision(record)
|
|
36
39
|
? { source: record.deliveryDecision.source,
|
|
37
|
-
completeSetup: record.deliveryDecision.completeSetup
|
|
40
|
+
completeSetup: record.deliveryDecision.completeSetup,
|
|
41
|
+
...(Object.hasOwn(record.deliveryDecision, "installPrerequisites")
|
|
42
|
+
? { installPrerequisites: record.deliveryDecision.installPrerequisites } : {}) }
|
|
38
43
|
: legacyDecision();
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -43,6 +48,7 @@ const isEligible = entry => entry.nativeDelivery?.state === "eligible"
|
|
|
43
48
|
|
|
44
49
|
const needsExpandedSetup = entry => ["needed", "blocked"]
|
|
45
50
|
.includes(entry.nativeServiceSetup?.state);
|
|
51
|
+
const needsPrerequisite = entry => entry.nativeServiceSetup?.requiresInstall === true;
|
|
46
52
|
|
|
47
53
|
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
48
54
|
&& runtime.isInteractive() === true;
|
|
@@ -60,6 +66,8 @@ function questionFor(entries) {
|
|
|
60
66
|
" Configure the required local permission grants.",
|
|
61
67
|
...(entries.some(entry => entry.adapterId === "codex")
|
|
62
68
|
? [" Start a missing supported Codex service."] : []),
|
|
69
|
+
...entries.filter(needsPrerequisite).map(entry =>
|
|
70
|
+
` Download the official Codex ${entry.nativeServiceSetup.cliVersion} standalone package for its service; retain your existing codex command.`),
|
|
63
71
|
...setup.map(note => ` ${note}`),
|
|
64
72
|
...(channels ? [" Allow Claude Code development Channels when prompted at startup."] : []),
|
|
65
73
|
" No: keep current delivery policies and use available next-turn hooks or acc inbox.",
|
|
@@ -88,6 +96,7 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
|
|
|
88
96
|
deliveryByAdapter[entry.adapterId] = explicit;
|
|
89
97
|
deliveryDecisionByAdapter[entry.adapterId] = {
|
|
90
98
|
source: "explicit-option", completeSetup: explicit !== "off",
|
|
99
|
+
...(needsPrerequisite(entry) ? { installPrerequisites: explicit !== "off" } : {}),
|
|
91
100
|
};
|
|
92
101
|
continue;
|
|
93
102
|
}
|
|
@@ -107,9 +116,12 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
|
|
|
107
116
|
const alreadyDecided = validDecision(record)
|
|
108
117
|
&& DELIBERATE_SOURCES.has(record.deliveryDecision.source);
|
|
109
118
|
const expandsLegacyOptIn = previous !== "off" && needsExpandedSetup(entry)
|
|
110
|
-
&& !decisionOf(record).completeSetup
|
|
119
|
+
&& !decisionOf(record).completeSetup
|
|
120
|
+
&& !(needsPrerequisite(entry) && decisionOf(record).installPrerequisites === false);
|
|
111
121
|
const freshDecision = previous === "off" && !alreadyDecided;
|
|
112
|
-
|
|
122
|
+
const expandsPrerequisite = previous !== "off" && needsPrerequisite(entry)
|
|
123
|
+
&& !Object.hasOwn(decisionOf(record), "installPrerequisites");
|
|
124
|
+
if (!freshDecision && !expandsLegacyOptIn && !expandsPrerequisite) {
|
|
113
125
|
deliveryDecisionByAdapter[entry.adapterId] ??= legacyDecision();
|
|
114
126
|
continue;
|
|
115
127
|
}
|
|
@@ -141,6 +153,9 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
|
|
|
141
153
|
: previous === "off"
|
|
142
154
|
? { source: "interactive-declined", completeSetup: false }
|
|
143
155
|
: decisionOf(record);
|
|
156
|
+
if (needsPrerequisite(entry)) {
|
|
157
|
+
deliveryDecisionByAdapter[entry.adapterId].installPrerequisites = yes;
|
|
158
|
+
}
|
|
144
159
|
}
|
|
145
160
|
}
|
|
146
161
|
|
|
@@ -57,7 +57,11 @@ export async function installManaged({ packageRoot, managerRoot, dataHome, home,
|
|
|
57
57
|
await writeControl(root, control);
|
|
58
58
|
const paths = await writeLaunchers(root, runtime.root);
|
|
59
59
|
const result = await apply({ ...paths, preserveVersions: true });
|
|
60
|
-
|
|
60
|
+
// Service readiness is separate from integration installation. Its failure
|
|
61
|
+
// must not fence a fully installed runtime or make the durable inbox unusable.
|
|
62
|
+
const integrationFailed = result.failed.some(failure => failure.stage !== "service-setup"
|
|
63
|
+
|| !result.operations?.some(operation => operation.adapterId === failure.adapterId && operation.applied === true));
|
|
64
|
+
if (!integrationFailed) {
|
|
61
65
|
const ready = await writeControl(root, { ...control, phase: "ready", pending: null });
|
|
62
66
|
await scheduleWorker(root, ready, { env });
|
|
63
67
|
} else {
|
|
@@ -36,15 +36,18 @@ export function nativeRemediation(entry) {
|
|
|
36
36
|
steps.push(`acc install --adapter ${entry.adapterId}`
|
|
37
37
|
+ " # complete the missing native launch setup from a supported shell");
|
|
38
38
|
} else if (native.configured && native.runtime !== "active"
|
|
39
|
-
&& !["needed", "blocked"].includes(service?.state)) {
|
|
39
|
+
&& !["needed", "blocked", "unsupported"].includes(service?.state)) {
|
|
40
40
|
steps.push(`${entry.displayName}: no verified live channel in this workspace; `
|
|
41
41
|
+ "open a new terminal, start a new client session and check its integration/channel prompts; "
|
|
42
42
|
+ "then run acc doctor here");
|
|
43
43
|
}
|
|
44
44
|
if (native.reasonCode === "native_endpoint_unavailable") {
|
|
45
|
-
if (service?.state
|
|
45
|
+
if (["blocked", "unsupported"].includes(service?.state)) steps.push(service.diagnostic);
|
|
46
46
|
else if (service?.state === "needed" && native.configured) {
|
|
47
|
-
steps.push(entry.deliveryDecision?.
|
|
47
|
+
steps.push(service.requiresInstall && entry.deliveryDecision?.installPrerequisites === false
|
|
48
|
+
? `acc install --adapter ${entry.adapterId} --delivery ${native.policy === "all" ? "all" : "actionable"}`
|
|
49
|
+
+ " # allow the official Codex download and complete service setup"
|
|
50
|
+
: entry.deliveryDecision?.completeSetup === true
|
|
48
51
|
? `acc install --adapter ${entry.adapterId} # prepare the missing supported local service`
|
|
49
52
|
: `acc install --adapter ${entry.adapterId} --delivery actionable`
|
|
50
53
|
+ " # approve complete automatic peer-request setup");
|
|
@@ -6,7 +6,12 @@ export async function applyServiceSetup({ adapter, context, operation, results }
|
|
|
6
6
|
let setup = plan;
|
|
7
7
|
if (["needed", "ready"].includes(plan.state)) {
|
|
8
8
|
try {
|
|
9
|
-
setup = await adapter.prepareNativeServiceSetup({ context, plan
|
|
9
|
+
setup = await adapter.prepareNativeServiceSetup({ context, plan,
|
|
10
|
+
installPrerequisites: operation.deliveryDecision.installPrerequisites === true,
|
|
11
|
+
download: async source => {
|
|
12
|
+
const { downloadVerifiedInstaller } = await import("./verified-download.mjs");
|
|
13
|
+
return downloadVerifiedInstaller(source);
|
|
14
|
+
} });
|
|
10
15
|
if (!setup || !["ready", "failed", "blocked"].includes(setup.state)) throw new Error("invalid setup result");
|
|
11
16
|
} catch {
|
|
12
17
|
setup = { state: "failed", started: false, reasonCode: "service_setup_failed",
|
|
@@ -24,7 +29,7 @@ export async function applyServiceSetup({ adapter, context, operation, results }
|
|
|
24
29
|
} else {
|
|
25
30
|
operation.needsAction.push(setup.diagnostic);
|
|
26
31
|
if (setup.state === "failed") results.failed.push({ adapterId: operation.adapterId,
|
|
27
|
-
error: setup.diagnostic, reasonCode: setup.reasonCode });
|
|
32
|
+
error: setup.diagnostic, reasonCode: setup.reasonCode, stage: "service-setup" });
|
|
28
33
|
}
|
|
29
34
|
operation.diagnostics.push(setup.diagnostic);
|
|
30
35
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const fail = reasonCode => { throw Object.assign(new Error(reasonCode), { reasonCode }); };
|
|
4
|
+
|
|
5
|
+
// Only the explicit installer supplies this port. Adapters keep vendor metadata
|
|
6
|
+
// and installation behavior, while hook-loaded code has no network implementation.
|
|
7
|
+
export async function downloadVerifiedInstaller(source, { fetch = globalThis.fetch } = {}) {
|
|
8
|
+
if (new URL(source.url).protocol !== "https:") fail("prerequisite_download_failed");
|
|
9
|
+
const response = await fetch(source.url, { redirect: "error", signal: AbortSignal.timeout(30_000) });
|
|
10
|
+
if (!response.ok || !response.body) fail("prerequisite_download_failed");
|
|
11
|
+
const chunks = []; let size = 0;
|
|
12
|
+
for await (const chunk of response.body) {
|
|
13
|
+
size += chunk.byteLength;
|
|
14
|
+
if (size > 131_072) fail("prerequisite_download_failed");
|
|
15
|
+
chunks.push(Buffer.from(chunk));
|
|
16
|
+
}
|
|
17
|
+
const script = Buffer.concat(chunks);
|
|
18
|
+
if (createHash("sha256").update(script).digest("hex") !== source.sha256) {
|
|
19
|
+
fail("prerequisite_integrity_failed");
|
|
20
|
+
}
|
|
21
|
+
return script;
|
|
22
|
+
}
|