agents-can-communicate 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +3 -2
  2. package/docs/CLI.md +12 -3
  3. package/docs/CONFIGURATION.md +5 -3
  4. package/docs/GETTING_STARTED.md +6 -3
  5. package/docs/TROUBLESHOOTING.md +7 -3
  6. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  7. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  8. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +22 -3
  9. package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +56 -4
  10. package/node_modules/@agents-can-communicate/adapter-codex/src/standalone-install.mjs +38 -0
  11. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  12. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  13. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  14. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  15. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  16. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  17. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +18 -3
  18. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +5 -1
  19. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +6 -3
  20. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  21. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  22. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  23. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  24. package/node_modules/@agents-can-communicate/installer/src/service-setup.mjs +7 -2
  25. package/node_modules/@agents-can-communicate/installer/src/verified-download.mjs +22 -0
  26. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  27. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  28. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  29. 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, an accepted choice can start a missing
72
- service when the managed standalone installation is present. The client still owns session
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
 
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 from an
264
- existing managed standalone installation. Dry runs, delivery off, automatic refresh, and
265
- message delivery do not start it. Uninstall does not stop the shared vendor daemon. The install summary names each client's requested policy,
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.
@@ -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. Explicit complete setup can prepare a definitely absent service on Codex 0.154.0
143
- or newer when the managed standalone installation is present. Message delivery does not
144
- start or stop it.
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`
@@ -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. When supported Codex service preparation succeeds, it
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. If the managed standalone prerequisite
64
- is missing, install reports that prerequisite instead of claiming readiness.
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
@@ -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 with complete consent can prepare a
102
- missing service when the managed standalone installation is present. If that prerequisite
103
- is missing or unsafe, doctor names the vendor action and does not claim readiness. A ready
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-codex",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
- try { codexHome = await realpath(codexHome); }
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 probeMaintenanceInstall(paths, run) {
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 { failMaintenance, maintenanceContext, probeMaintenanceInstall,
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 requires the managed standalone installation; install it with curl -fsSL https://chatgpt.com/codex/install.sh | sh using the selected HOME and CODEX_HOME, then retry acc install",
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 prepareNativeServiceSetup({ context, plan } = {}) {
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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Coordinate this Gemini CLI session with other AI agent sessions working in the same workspace.",
5
5
  "contextFileName": "skills/acc/SKILL.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
- if (!freshDecision && !expandsLegacyOptIn) {
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
- if (result.failed.length === 0) {
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 === "blocked") steps.push(service.diagnostic);
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?.completeSetup === true
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");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/hook-runner",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "accManagedUpdateProtocol": 2,
5
5
  "accStoreVersion": 6,
6
6
  "type": "module",