agents-can-communicate 0.5.4 → 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.
- package/README.md +3 -2
- package/docs/ARCHITECTURE.md +6 -1
- package/docs/CLI.md +12 -3
- package/docs/CONFIGURATION.md +5 -3
- package/docs/GETTING_STARTED.md +6 -3
- package/docs/TROUBLESHOOTING.md +25 -5
- package/docs/UPGRADING.md +32 -6
- 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/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/generation-files.mjs +25 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +1 -22
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +5 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +2 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +23 -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
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -174,7 +174,12 @@ The CLI owns installation generations under `<dataHome>/acc/runtime`, outside wo
|
|
|
174
174
|
All five launch paths select a generation and publish an actual-process lease under one
|
|
175
175
|
admission mutex before loading workspace-capable code. Immutable launcher modules and
|
|
176
176
|
runtime directories preserve in-progress imports. Leases survive `main()` returning and
|
|
177
|
-
are removed only after confirmed process death.
|
|
177
|
+
are removed only after confirmed process death. Older updaters can omit the store contract
|
|
178
|
+
when publishing a newer generation. The reader recovers missing contracts only from the
|
|
179
|
+
referenced generation, after checking its package identity and content hash, including file
|
|
180
|
+
modes. It preserves explicit declarations and does not rewrite live leases. Missing or
|
|
181
|
+
changed generation files and unknown native bindings remain unverified holds.
|
|
182
|
+
A session is also pinned to the generation
|
|
178
183
|
it started with: its hooks delegate to that generation while the pin declares the same
|
|
179
184
|
store contract this one does, and run the active generation otherwise. A generation
|
|
180
185
|
directory is reclaimed once no control pointer, live lease, live pin or staging hold names
|
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
|
|
|
@@ -238,8 +242,9 @@ Next: [Getting started](GETTING_STARTED.md) · [Capabilities](CAPABILITIES.md)
|
|
|
238
242
|
|
|
239
243
|
Run `acc doctor` to see the update policy and pending notice. A hold keeps an update
|
|
240
244
|
pending only while the store contract it declares differs from the incoming version's or
|
|
241
|
-
is unknown
|
|
242
|
-
|
|
245
|
+
is unknown. ACC can recover a missing contract from the process's unchanged managed
|
|
246
|
+
generation when that generation declares one. A generation that predates the declaration
|
|
247
|
+
still needs its processes to exit. `acc update` names each remaining hold with its process and
|
|
243
248
|
its declared contract. ACC process leases, including persistent MCP servers, end on
|
|
244
249
|
confirmed process exit. Native bindings clear on observed SessionEnd or confirmed process
|
|
245
250
|
death; a vendor daemon may remain alive after SessionEnd. Unknown PIDs remain holds until
|
|
@@ -250,6 +255,21 @@ unrelated holds still require lifecycle cleanup or confirmed process exit; safet
|
|
|
250
255
|
do not expire merely by elapsed time. See
|
|
251
256
|
[maintenance and recovery](UPGRADING.md#confirmed-client-service-maintenance).
|
|
252
257
|
|
|
258
|
+
If ACC 0.5.3 or 0.5.4 reports `acc-claude-channel; contract unknown` after an earlier
|
|
259
|
+
0.4.x upgrade, its active pointer may have lost the contract field. This can affect newly
|
|
260
|
+
opened Channels too. A pending old updater retries its downloaded release without looking
|
|
261
|
+
for a newer fix. Install the current global CLI, then use that CLI to complete the update:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
npm install -g agents-can-communicate@latest
|
|
265
|
+
acc update
|
|
266
|
+
acc doctor
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The new reader can activate a compatible release while those Channels remain open.
|
|
270
|
+
It still waits for different contracts, unknown native bindings, or generation files it
|
|
271
|
+
cannot verify. See [legacy contract recovery](UPGRADING.md#recover-missing-runtime-contracts).
|
|
272
|
+
|
|
253
273
|
Use `acc update` to retry a failed download or finish an interrupted integration refresh.
|
|
254
274
|
A download failure keeps the working version. A partial integration refresh blocks
|
|
255
275
|
workspace commands until recovery completes, while hooks let the client continue without
|
package/docs/UPGRADING.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Upgrading to 0.5.0
|
|
2
2
|
|
|
3
|
+
## Recover missing runtime contracts
|
|
4
|
+
|
|
5
|
+
ACC 0.5.5 fixes an upgrade defect that can leave 0.5.3 or 0.5.4 waiting for
|
|
6
|
+
`acc-claude-channel` processes with `contract unknown`. The 0.4.x updater can activate
|
|
7
|
+
a newer generation without copying its store contract into the active pointer. Channels
|
|
8
|
+
started afterward copy that incomplete pointer even though their runtime declares a contract.
|
|
9
|
+
|
|
10
|
+
For an installation already waiting on an older pending release, run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g agents-can-communicate@latest
|
|
14
|
+
acc update
|
|
15
|
+
acc version
|
|
16
|
+
acc doctor
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The global CLI supplies the new management reader before runtime activation. This step is
|
|
20
|
+
needed because the older updater retries its pending release before checking npm for another
|
|
21
|
+
version. If a version pin selects an older release, clear it or select 0.5.5 first.
|
|
22
|
+
|
|
23
|
+
The reader verifies the referenced generation's package identity, complete contents and
|
|
24
|
+
file modes before recovering a missing contract. Compatible Channels can keep running on
|
|
25
|
+
their original generation during activation. Explicit differing contracts and unknown
|
|
26
|
+
native bindings still block. A generation without a contract, missing files, or changed
|
|
27
|
+
contents remains unknown. ACC does not terminate these processes or edit their lease records.
|
|
28
|
+
|
|
3
29
|
## From 0.4.4, 0.4.3, 0.4.2, 0.4.1 or 0.4.0
|
|
4
30
|
|
|
5
31
|
This release keeps the existing workspace data format. It changes when an update is
|
|
@@ -56,17 +82,17 @@ only two kinds keep an update pending:
|
|
|
56
82
|
- A hold whose declared store contract differs from the incoming version's. This is the
|
|
57
83
|
case the gate exists for, and a release that changes the store contract still needs
|
|
58
84
|
every live process to exit.
|
|
59
|
-
- A hold
|
|
60
|
-
|
|
85
|
+
- A hold with no verifiable contract. Older native bindings and generations that predate
|
|
86
|
+
the declaration remain unknown. A missing field can be recovered when the referenced
|
|
87
|
+
managed generation declares its contract and passes integrity verification.
|
|
61
88
|
|
|
62
89
|
A hold declaring the same store contract as the incoming version proceeds. Several open
|
|
63
90
|
Claude Code sessions, a Codex daemon and an idle ACC MCP server no longer have to be
|
|
64
91
|
closed together to move between releases that share a contract.
|
|
65
92
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
maintenance offer described below. Updates after that do not need it.
|
|
93
|
+
Processes still running a generation from before the contract field existed must exit once.
|
|
94
|
+
Close the relevant clients and persistent ACC processes, or accept the eligible Codex service
|
|
95
|
+
maintenance offer described below. Later compatible runtimes do not require that restart.
|
|
70
96
|
|
|
71
97
|
`acc update` names each remaining hold with its process and its declared contract, so a
|
|
72
98
|
wait now states its reason rather than only the PID. `acc doctor` reports the same
|
|
@@ -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
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export async function collectFile(source, relative, collected) {
|
|
6
|
+
const stat = await lstat(source);
|
|
7
|
+
if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
|
|
8
|
+
if (stat.isDirectory()) {
|
|
9
|
+
for (const entry of (await readdir(source)).sort()) {
|
|
10
|
+
await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
|
|
11
|
+
}
|
|
12
|
+
} else if (stat.isFile()) {
|
|
13
|
+
collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
|
|
14
|
+
} else throw new Error(`unsupported runtime file: ${relative}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** This format also names existing generations; keep it stable across upgrades. */
|
|
18
|
+
export function fingerprint(files) {
|
|
19
|
+
const hash = createHash("sha256");
|
|
20
|
+
for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
|
|
21
|
+
hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
|
|
22
|
+
hash.update(file.bytes);
|
|
23
|
+
}
|
|
24
|
+
return hash.digest("hex");
|
|
25
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm }
|
|
3
2
|
from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
5
|
+
import { collectFile, fingerprint } from "./generation-files.mjs";
|
|
6
6
|
import { attachStagingTemp, holdStagedGeneration, releaseStagingHold } from "./staging.mjs";
|
|
7
7
|
|
|
8
8
|
const NAME = "agents-can-communicate";
|
|
@@ -37,18 +37,6 @@ function declaredPath(value) {
|
|
|
37
37
|
return value.replace(/\/+$/, "");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
async function collectFile(source, relative, collected) {
|
|
41
|
-
const stat = await lstat(source);
|
|
42
|
-
if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
|
|
43
|
-
if (stat.isDirectory()) {
|
|
44
|
-
for (const entry of (await readdir(source)).sort()) {
|
|
45
|
-
await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
|
|
46
|
-
}
|
|
47
|
-
} else if (stat.isFile()) {
|
|
48
|
-
collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
|
|
49
|
-
} else throw new Error(`unsupported runtime file: ${relative}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
40
|
async function checkDeclaredComponents(source, relative) {
|
|
53
41
|
let current = source;
|
|
54
42
|
for (const component of relative.split("/")) {
|
|
@@ -84,15 +72,6 @@ async function collectPackage(source, prefix, collected, expectedName, version)
|
|
|
84
72
|
return manifest;
|
|
85
73
|
}
|
|
86
74
|
|
|
87
|
-
function fingerprint(files) {
|
|
88
|
-
const hash = createHash("sha256");
|
|
89
|
-
for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
|
|
90
|
-
hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
|
|
91
|
-
hash.update(file.bytes);
|
|
92
|
-
}
|
|
93
|
-
return hash.digest("hex");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
75
|
async function existingMatches(target, files) {
|
|
97
76
|
try {
|
|
98
77
|
const actual = new Map();
|
|
@@ -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 {
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { ENTRY_KINDS } from "./entry.mjs";
|
|
5
5
|
import { managedDirectory, syncDirectory } from "./state.mjs";
|
|
6
6
|
|
|
7
|
-
const MODULES = ["entry.mjs", "state.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
|
|
7
|
+
const MODULES = ["entry.mjs", "state.mjs", "generation-files.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
|
|
8
8
|
async function durableFile(file, bytes, mode = 0o600) {
|
|
9
9
|
const handle = await open(file, "wx", mode);
|
|
10
10
|
try { await handle.writeFile(bytes); await handle.sync(); }
|
|
@@ -49,11 +49,11 @@ export async function listRuntimeHolds(root, { pidIsAlive = defaultPidIsAlive }
|
|
|
49
49
|
|| typeof lease.token !== "string" || name !== `${lease.token}.json`
|
|
50
50
|
|| typeof lease.kind !== "string" || !lease.kind
|
|
51
51
|
|| !Number.isFinite(Date.parse(lease.createdAt))) throw new Error("invalid runtime lease");
|
|
52
|
-
await validateRuntime(root, lease.runtime);
|
|
52
|
+
const runtime = await validateRuntime(root, lease.runtime);
|
|
53
53
|
if (await confirmedDead(lease.pid, pidIsAlive)) {
|
|
54
54
|
await rm(file, { force: true });
|
|
55
55
|
removed = true;
|
|
56
|
-
} else holds.push(lease);
|
|
56
|
+
} else holds.push({ ...lease, runtime });
|
|
57
57
|
}
|
|
58
58
|
if (removed) await syncDirectory(directory);
|
|
59
59
|
return holds;
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { collectFile, fingerprint } from "./generation-files.mjs";
|
|
5
6
|
|
|
6
7
|
/** Resolve existing ancestors without creating an uninitialized manager. */
|
|
7
8
|
export async function canonicalManagerRoot(root) {
|
|
@@ -74,6 +75,25 @@ export async function writeManagedJson(file, value) {
|
|
|
74
75
|
const nonempty = value => typeof value === "string" && value.length > 0;
|
|
75
76
|
const nullableString = value => value === null || nonempty(value);
|
|
76
77
|
|
|
78
|
+
/** Older updaters dropped the contract while publishing a newer generation.
|
|
79
|
+
* Recover only from that exact, unchanged generation, never from the active
|
|
80
|
+
* version or a loose manifest. Explicit lease declarations remain historical
|
|
81
|
+
* facts. Missing or damaged files leave an unknown hold without failing hooks.
|
|
82
|
+
*/
|
|
83
|
+
async function recoverStoreVersion(generations, runtime) {
|
|
84
|
+
if (path.dirname(runtime.root) !== generations
|
|
85
|
+
|| !/^\d+\.\d+\.\d+-[a-f0-9]{24}$/.test(path.basename(runtime.root))) return null;
|
|
86
|
+
try {
|
|
87
|
+
const files = new Map();
|
|
88
|
+
await collectFile(runtime.root, "", files);
|
|
89
|
+
if (path.basename(runtime.root) !== `${runtime.version}-${fingerprint(files).slice(0, 24)}`) return null;
|
|
90
|
+
const manifest = JSON.parse(files.get("package.json")?.bytes.toString("utf8"));
|
|
91
|
+
return manifest.name === "agents-can-communicate" && manifest.version === runtime.version
|
|
92
|
+
&& Number.isSafeInteger(manifest.accStoreVersion) && manifest.accStoreVersion > 0
|
|
93
|
+
? manifest.accStoreVersion : null;
|
|
94
|
+
} catch { return null; }
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
export async function validateRuntime(root, runtime) {
|
|
78
98
|
if (!runtime || !nonempty(runtime.version) || typeof runtime.root !== "string"
|
|
79
99
|
|| !path.isAbsolute(runtime.root)) throw new Error("invalid control generation");
|
|
@@ -95,7 +115,9 @@ export async function validateRuntime(root, runtime) {
|
|
|
95
115
|
if (await managedDirectory(generations)) {
|
|
96
116
|
if (await realpath(generations) !== generations) throw new Error("invalid control generations directory");
|
|
97
117
|
}
|
|
98
|
-
|
|
118
|
+
const normalized = { version: runtime.version, root: resolved, storeVersion: runtime.storeVersion ?? null };
|
|
119
|
+
normalized.storeVersion ??= await recoverStoreVersion(generations, normalized);
|
|
120
|
+
return normalized;
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
async function validateControl(root, value) {
|
|
@@ -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
|
+
}
|