agents-can-communicate 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/entrypoints/acc-mcp.mjs +0 -1
- package/bin/entrypoints/acc.mjs +0 -1
- package/docs/ADAPTER_AUTHORING.md +36 -17
- package/docs/ARCHITECTURE.md +9 -6
- package/docs/CAPABILITIES.md +23 -13
- package/docs/CLI.md +6 -3
- package/docs/PROTOCOL.md +2 -2
- package/docs/RELEASING.md +6 -4
- package/docs/TROUBLESHOOTING.md +2 -2
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +2 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +5 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +12 -17
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +5 -5
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -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/adapter-sdk/src/certification.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +85 -77
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +4 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +5 -6
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +17 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +14 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +9 -41
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +3 -4
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +3 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +3 -4
- 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
|
@@ -3,7 +3,7 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { access, lstat, open, realpath } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { compareVersions, versionOrder } from "./app-server-client.mjs";
|
|
7
7
|
|
|
8
8
|
export const MINIMUM_MAINTENANCE_CLI = "0.154.0";
|
|
9
9
|
export const absolute = value => typeof value === "string" && path.isAbsolute(value)
|
|
@@ -15,6 +15,20 @@ export const startTimeValid = value => typeof value === "string"
|
|
|
15
15
|
export const managedExecutablePaths = codexHome => ["bin/codex", "codex"].map(name =>
|
|
16
16
|
path.join(codexHome, "packages/standalone/current", name));
|
|
17
17
|
|
|
18
|
+
// Where lsof lives: /usr/sbin on macOS, /usr/bin on Linux. A fixed list of
|
|
19
|
+
// absolute paths, never PATH, so a directory an operator can write to cannot
|
|
20
|
+
// supply the tool that proves socket ownership. The first executable wins;
|
|
21
|
+
// none means the socket cannot be proven and verification fails closed.
|
|
22
|
+
export const LSOF_CANDIDATES = Object.freeze(["/usr/sbin/lsof", "/usr/bin/lsof"]);
|
|
23
|
+
|
|
24
|
+
export async function resolveLsof({ access: check = access } = {}) {
|
|
25
|
+
for (const candidate of LSOF_CANDIDATES) {
|
|
26
|
+
try { await check(candidate, constants.X_OK); return candidate; }
|
|
27
|
+
catch { /* the next candidate may be the one this host has */ }
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
export function runMaintenanceCommand(command, args, options) {
|
|
19
33
|
return new Promise(resolve => execFile(command, args,
|
|
20
34
|
{ timeout: 20_000, ...options, maxBuffer: 131_072, windowsHide: true },
|
|
@@ -71,12 +85,12 @@ async function resolveCli(pathEnv) {
|
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
const cliVersion = response => response.status === 0
|
|
74
|
-
? /^codex-cli (\d+\.\d+\.\d+)\s*$/.exec(response.stdout)?.[1] ?? null : null;
|
|
88
|
+
? /^codex-cli (\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)\s*$/.exec(response.stdout)?.[1] ?? null : null;
|
|
75
89
|
|
|
76
90
|
export async function probeMaintenanceCli(paths, run) {
|
|
77
91
|
const cliPath = await resolveCli(paths.options.env.PATH);
|
|
78
92
|
const version = cliVersion(await run(cliPath, ["--version"], paths.options));
|
|
79
|
-
if (!
|
|
93
|
+
if (!versionOrder(version) || compareVersions(version, MINIMUM_MAINTENANCE_CLI) < 0) {
|
|
80
94
|
failMaintenance("maintenance_cli_unsupported");
|
|
81
95
|
}
|
|
82
96
|
const help = await run(cliPath, ["app-server", "daemon", "--help"], paths.options);
|
|
@@ -117,17 +131,39 @@ export async function observeMaintenanceProcess(pid, paths, run) {
|
|
|
117
131
|
return { state: "alive", processStartTime: match[1], command: match[2] };
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
|
|
134
|
+
/** Whether `lsof -Fn` output lists a socket at one of these paths. macOS prints
|
|
135
|
+
* the path alone; Linux appends the socket type (`/path type=STREAM`, measured
|
|
136
|
+
* on the ubuntu CI job), and a connected peer may carry more after it. The name
|
|
137
|
+
* is the text before the first ` type=`, compared whole. */
|
|
138
|
+
export function socketListedIn(stdout, socketPaths) {
|
|
139
|
+
const names = new Set(typeof socketPaths === "string" ? [socketPaths] : socketPaths);
|
|
140
|
+
return stdout.split("\n").some(line => line.startsWith("n")
|
|
141
|
+
&& names.has(line.slice(1).split(" type=")[0]));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The daemon's command line as measured: 0.154.0 ran `<current path> app-server
|
|
145
|
+
// --listen unix://`; 0.157.1 runs the resolved releases path with
|
|
146
|
+
// `--managed-daemon` appended. The executable must live in the standalone tree
|
|
147
|
+
// and be the managed binary itself, however the tree names it.
|
|
148
|
+
const DAEMON_COMMAND = /^(\/.+?) app-server --listen unix:\/\/(?: --managed-daemon)?$/;
|
|
149
|
+
|
|
150
|
+
export async function verifyMaintenanceProcess(snapshot, paths, run, { lsof = resolveLsof } = {}) {
|
|
121
151
|
const process = await observeMaintenanceProcess(snapshot.pid, paths, run);
|
|
122
|
-
const
|
|
123
|
-
const
|
|
152
|
+
const executable = DAEMON_COMMAND.exec(process.command ?? "")?.[1] ?? null;
|
|
153
|
+
const standalone = path.join(paths.codexHome, "packages", "standalone") + path.sep;
|
|
124
154
|
if (process.state !== "alive" || process.processStartTime !== snapshot.processStartTime
|
|
125
|
-
|| !
|
|
126
|
-
|| await realpath(executable) !== await realpath(paths.managedPath)) {
|
|
155
|
+
|| executable === null || !executable.startsWith(standalone)
|
|
156
|
+
|| await realpath(executable).catch(() => null) !== await realpath(paths.managedPath)) {
|
|
127
157
|
failMaintenance("daemon_identity_unavailable");
|
|
128
158
|
}
|
|
129
|
-
|
|
130
|
-
|
|
159
|
+
// 0.157.1 lists the socket by the path it listens on, not by the symlink it
|
|
160
|
+
// reports; either names the same socket.
|
|
161
|
+
const listening = [paths.socketPath, await realpath(paths.socketPath).catch(() => null)]
|
|
162
|
+
.filter(value => value !== null);
|
|
163
|
+
const lsofPath = await lsof();
|
|
164
|
+
if (lsofPath === null) failMaintenance("daemon_socket_unproven");
|
|
165
|
+
const sockets = await run(lsofPath, ["-n", "-a", "-p", String(snapshot.pid), "-U", "-Fn"], paths.options);
|
|
166
|
+
if (sockets.status !== 0 || !socketListedIn(sockets.stdout, listening)) {
|
|
131
167
|
failMaintenance("daemon_socket_unproven");
|
|
132
168
|
}
|
|
133
169
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { openCodexAppServer,
|
|
3
|
-
import {
|
|
2
|
+
import { openCodexAppServer, versionOrder } from "./app-server-client.mjs";
|
|
3
|
+
import { readySocketPath } from "./native-endpoint.mjs";
|
|
4
4
|
import { absolute, approvedProcessIsDead, failMaintenance, maintenanceContext,
|
|
5
5
|
managedExecutablePaths, metadataExists, probeMaintenanceInstall, readMaintenancePid, runMaintenanceCommand,
|
|
6
6
|
startTimeValid, verifyMaintenanceProcess } from "./maintenance-host.mjs";
|
|
@@ -16,7 +16,7 @@ function validApproval(value) {
|
|
|
16
16
|
return value?.serviceId === "codex-app-server" && ["ready", "busy"].includes(value.state)
|
|
17
17
|
&& Number.isSafeInteger(value.pid) && value.pid > 1 && startTimeValid(value.processStartTime)
|
|
18
18
|
&& ["codexHome", "cliPath", "managedPath", "socketPath"].every(key => absolute(value[key]))
|
|
19
|
-
&& ["cliVersion", "serverVersion", "managedVersion"].every(key =>
|
|
19
|
+
&& ["cliVersion", "serverVersion", "managedVersion"].every(key => versionOrder(value[key]))
|
|
20
20
|
&& value.managedVersion === value.cliVersion
|
|
21
21
|
&& managedExecutablePaths(value.codexHome).includes(value.managedPath)
|
|
22
22
|
&& value.socketPath === path.join(value.codexHome, "app-server-control/app-server-control.sock");
|
|
@@ -36,8 +36,7 @@ export function createCodexMaintenance({ run = runMaintenanceCommand, open = ope
|
|
|
36
36
|
const paths = await maintenanceContext(context);
|
|
37
37
|
Object.assign(snapshot, { codexHome: paths.codexHome, socketPath: paths.socketPath, managedPath: paths.managedPath });
|
|
38
38
|
if (!await metadataExists(paths.socketPath) && !await metadataExists(paths.pidPath)) return null;
|
|
39
|
-
if (paths.
|
|
40
|
-
if (!await socketIsReady(paths.socketPath)) failMaintenance("daemon_socket_unproven");
|
|
39
|
+
if (!await readySocketPath(paths.socketPath)) failMaintenance("daemon_socket_unproven");
|
|
41
40
|
Object.assign(snapshot, await probeMaintenanceInstall(paths, run));
|
|
42
41
|
const version = await run(snapshot.cliPath, ["app-server", "daemon", "version"], paths.options);
|
|
43
42
|
if (version.status !== 0) failMaintenance("daemon_version_unavailable");
|
|
@@ -45,7 +44,7 @@ export function createCodexMaintenance({ run = runMaintenanceCommand, open = ope
|
|
|
45
44
|
if (observed.status !== "running" || observed.backend !== "pid") failMaintenance("maintenance_backend_unsupported");
|
|
46
45
|
if (observed.managedCodexPath !== snapshot.managedPath || observed.socketPath !== snapshot.socketPath
|
|
47
46
|
|| observed.cliVersion !== snapshot.cliVersion || observed.managedCodexVersion !== snapshot.managedVersion
|
|
48
|
-
|| !
|
|
47
|
+
|| !versionOrder(observed.appServerVersion)) failMaintenance("daemon_identity_unavailable");
|
|
49
48
|
snapshot.serverVersion = observed.appServerVersion;
|
|
50
49
|
Object.assign(snapshot, await readMaintenancePid(paths.pidPath));
|
|
51
50
|
await verifyMaintenanceProcess(snapshot, paths, run);
|
|
@@ -83,7 +82,6 @@ export function createCodexMaintenance({ run = runMaintenanceCommand, open = ope
|
|
|
83
82
|
try {
|
|
84
83
|
const pinned = { ...context, env: { ...(context?.env ?? process.env), CODEX_HOME: expected.codexHome } };
|
|
85
84
|
const paths = await maintenanceContext(pinned);
|
|
86
|
-
if (paths.platform !== "darwin-arm64") return refused("maintenance_platform_unsupported");
|
|
87
85
|
const install = { ...paths, ...await probeMaintenanceInstall(paths, run) };
|
|
88
86
|
if (!sameFields(expected, install, installationKeys)) return refused("service_identity_changed");
|
|
89
87
|
const existing = await inspectMaintenance(pinned);
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { realpath } from "node:fs/promises";
|
|
2
1
|
import { decisionBody } from "@agents-can-communicate/adapter-sdk";
|
|
3
2
|
|
|
4
3
|
import { CODEX_QUEUE_MINIMUM, MINIMUM_VERSION, PROTOCOL_CONTRACT, QUEUE_MODES,
|
|
5
|
-
addCodexQueueMessage, canonicalCwd,
|
|
6
|
-
locateCodexThread, openCodexAppServer,
|
|
7
|
-
|
|
8
|
-
import { newEndpointId, readNativeEndpoint, removeNativeEndpoint, socketIsReady,
|
|
4
|
+
addCodexQueueMessage, canonicalCwd, compareVersions, controlSocketPath,
|
|
5
|
+
locateCodexThread, openCodexAppServer, probeCodexQueue, safeReason, serverVersionOf,
|
|
6
|
+
versionOrder } from "./app-server-client.mjs";
|
|
7
|
+
import { newEndpointId, readNativeEndpoint, readySocketPath, removeNativeEndpoint, socketIsReady,
|
|
9
8
|
writeNativeEndpoint } from "./native-endpoint.mjs";
|
|
10
9
|
|
|
11
10
|
// The receiver's hook supplies thread and cwd. Core holds only a random endpoint
|
|
@@ -35,8 +34,8 @@ export async function probeNativeDelivery({ timeoutMs = 750, env = process.env,
|
|
|
35
34
|
open = openCodexAppServer } = {}) {
|
|
36
35
|
const unsupported = (reasonCode, clientVersion = null) => ({ supported: false, clientVersion,
|
|
37
36
|
protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
|
|
38
|
-
const socketPath = controlSocketPath(env);
|
|
39
|
-
if (
|
|
37
|
+
const socketPath = await readySocketPath(controlSocketPath(env));
|
|
38
|
+
if (socketPath === null) return unsupported("native_endpoint_unavailable");
|
|
40
39
|
try {
|
|
41
40
|
return await usingPeer(socketPath, timeoutMs, open, async peer => {
|
|
42
41
|
const probe = await probeCodexQueue(peer);
|
|
@@ -46,7 +45,12 @@ export async function probeNativeDelivery({ timeoutMs = 750, env = process.env,
|
|
|
46
45
|
modes: [...QUEUE_MODES], reasonCode: null };
|
|
47
46
|
});
|
|
48
47
|
} catch (error) {
|
|
49
|
-
|
|
48
|
+
if (error?.code === "ETIMEDOUT") return unsupported("probe_timeout");
|
|
49
|
+
// A service that died without cleaning up leaves its socket file behind,
|
|
50
|
+
// and a connect to it is refused. That is a missing service, the one fact
|
|
51
|
+
// whose advice (start the daemon) fixes it, not a failed feature probe.
|
|
52
|
+
return unsupported(safeReason(error) === "transport_unavailable"
|
|
53
|
+
? "native_endpoint_unavailable" : "feature_probe_failed");
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -85,7 +89,7 @@ export async function verifyReceiver(peer, endpoint, { probe = probeCodexQueue,
|
|
|
85
89
|
: result.reasonCode === "below_minimum_version" ? "handshake_version_mismatch"
|
|
86
90
|
: "protocol_mismatch", servingVersion: null };
|
|
87
91
|
}
|
|
88
|
-
if (
|
|
92
|
+
if (compareVersions(result.serverVersion, CODEX_QUEUE_MINIMUM) < 0) {
|
|
89
93
|
return { reasonCode: "handshake_version_mismatch", servingVersion: null };
|
|
90
94
|
}
|
|
91
95
|
const located = await locate(peer, { threadId: endpoint.threadId, cwd: endpoint.cwd });
|
|
@@ -99,12 +103,12 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
|
|
|
99
103
|
env = process.env, timeoutMs = 750, now = Date.now, open = openCodexAppServer } = {}) {
|
|
100
104
|
const rejected = reason => closed(clientVersion, reason);
|
|
101
105
|
if (!Number.isInteger(clientPid) || clientPid <= 0) return rejected("client_process_unknown");
|
|
102
|
-
if (
|
|
106
|
+
if (versionOrder(clientVersion) === null) return rejected("version_unavailable");
|
|
103
107
|
if (typeof event?.sessionId !== "string" || event.sessionId === "") return rejected("handshake_failed");
|
|
104
108
|
const cwd = await canonicalCwd(event.cwd);
|
|
105
109
|
if (cwd === null) return rejected("workspace_identity_unavailable");
|
|
106
|
-
const socketPath = await
|
|
107
|
-
if (
|
|
110
|
+
const socketPath = await readySocketPath(controlSocketPath(env));
|
|
111
|
+
if (socketPath === null) return rejected("handshake_failed");
|
|
108
112
|
try {
|
|
109
113
|
return await usingPeer(socketPath, timeoutMs, open, async peer => {
|
|
110
114
|
const endpoint = { schemaVersion: 1, endpointId: newEndpointId(), socketPath,
|
|
@@ -3,7 +3,7 @@ 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
5
|
|
|
6
|
-
import { PROTOCOL_CONTRACT,
|
|
6
|
+
import { PROTOCOL_CONTRACT, versionOrder } from "./app-server-client.mjs";
|
|
7
7
|
|
|
8
8
|
const KEYS = ["schemaVersion", "endpointId", "socketPath", "threadId", "cwd",
|
|
9
9
|
"clientVersion", "protocolContract", "leaseUntil"];
|
|
@@ -21,7 +21,7 @@ function valid(record) {
|
|
|
21
21
|
&& record.schemaVersion === 1 && ENDPOINT.test(record.endpointId)
|
|
22
22
|
&& absolute(record.socketPath) && absolute(record.cwd)
|
|
23
23
|
&& typeof record.threadId === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(record.threadId)
|
|
24
|
-
&&
|
|
24
|
+
&& versionOrder(record.clientVersion) !== null
|
|
25
25
|
&& record.protocolContract === PROTOCOL_CONTRACT
|
|
26
26
|
&& typeof record.leaseUntil === "string"
|
|
27
27
|
&& /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(record.leaseUntil)
|
|
@@ -49,6 +49,16 @@ export async function socketIsReady(socketPath) {
|
|
|
49
49
|
} catch { return false; }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** The socket a control socket path leads to, or null. Codex 0.157.1 keeps a
|
|
53
|
+
* symlink at the path it reports and the socket itself under /private/tmp
|
|
54
|
+
* (measured 2026-09-26), so the path is followed once and the socket at its
|
|
55
|
+
* end is held to socketIsReady: an owned socket, never itself a link. */
|
|
56
|
+
export async function readySocketPath(socketPath) {
|
|
57
|
+
if (!absolute(socketPath)) return null;
|
|
58
|
+
const resolved = await realpath(socketPath).catch(() => null);
|
|
59
|
+
return resolved !== null && await socketIsReady(resolved) ? resolved : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
// A fresh, immutable registration for one hook binding. Refresh observes the
|
|
53
63
|
// same registration again; it cannot overwrite a successor's random endpoint.
|
|
54
64
|
export async function writeNativeEndpoint({ runtimeDir, record }) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { lstat, realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { socketIsReady } from "./native-endpoint.mjs";
|
|
3
|
+
import { readySocketPath, socketIsReady } from "./native-endpoint.mjs";
|
|
4
4
|
import { probeNativeDelivery } from "./native-delivery.mjs";
|
|
5
5
|
import { installCodexStandalone } from "./standalone-install.mjs";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { refusesConnections } from "./ws-json-rpc.mjs";
|
|
7
|
+
import { approvedProcessIsDead, failMaintenance, maintenanceContext, probeMaintenanceCli,
|
|
8
|
+
probeMaintenanceInstall, readMaintenancePid, runMaintenanceCommand, verifyMaintenanceProcess }
|
|
9
|
+
from "./maintenance-host.mjs";
|
|
8
10
|
|
|
9
11
|
const keys = ["home", "codexHome", "cliPath", "cliVersion", "managedPath", "managedVersion",
|
|
10
12
|
"managedRealPath", "cliIdentity", "managedIdentity", "socketPath", "pidPath", "platform"];
|
|
@@ -14,6 +16,7 @@ const own = info => typeof process.getuid !== "function" || info.uid === process
|
|
|
14
16
|
const sessionNeeded = "Codex service ready; open a new Codex session to establish its delivery binding and review client hooks and permissions";
|
|
15
17
|
const diagnostics = {
|
|
16
18
|
native_endpoint_unavailable: "Prepare the missing Codex service with codex app-server daemon start, then open a new Codex session",
|
|
19
|
+
service_stopped: "The Codex service stopped and left its files behind; acc install starts it in place with codex app-server daemon start, which replaces them, then open a new Codex session",
|
|
17
20
|
managed_install_missing: "Codex service needs its standalone package; run acc install to download the matching official Codex release and prepare the service",
|
|
18
21
|
managed_install_incomplete: "Codex standalone installation is incomplete; repair it with the official Codex installer, then retry acc install",
|
|
19
22
|
prerequisite_consent_required: "Codex standalone download was not approved. Run acc install --adapter codex --delivery actionable to allow the download and automatic peer requests",
|
|
@@ -21,7 +24,6 @@ const diagnostics = {
|
|
|
21
24
|
prerequisite_integrity_failed: "The Codex installer checksum did not match the reviewed version; no installer was run",
|
|
22
25
|
prerequisite_install_failed: "The official Codex installation did not complete; check connectivity and the selected Codex home, then retry acc install",
|
|
23
26
|
managed_binary_mismatch: "Codex CLI and managed standalone versions differ; repair the vendor installation, then retry acc install",
|
|
24
|
-
maintenance_platform_unsupported: "Codex service preparation is captured only on darwin-arm64; configure the vendor service manually",
|
|
25
27
|
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",
|
|
26
28
|
};
|
|
27
29
|
function report(state, reasonCode, facts = {}) {
|
|
@@ -38,6 +40,20 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
38
40
|
try { return await fs.lstat(file); }
|
|
39
41
|
catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
40
42
|
}
|
|
43
|
+
// A service that stopped without cleaning up: its pid file names a process
|
|
44
|
+
// that is gone, and its socket refuses every connection; either may be gone
|
|
45
|
+
// already. Prepared like an absent one: Codex's own start replaces both
|
|
46
|
+
// files (measured on 0.155.1), and every identity check follows it.
|
|
47
|
+
async function serviceStopped(paths) {
|
|
48
|
+
if (await info(paths.pidPath)) {
|
|
49
|
+
const recorded = await readMaintenancePid(paths.pidPath).catch(() => null);
|
|
50
|
+
if (recorded === null || !await approvedProcessIsDead(recorded, paths, run)) return false;
|
|
51
|
+
}
|
|
52
|
+
// Absent, or a symlink whose socket is gone (what 0.157.1 leaves after a
|
|
53
|
+
// reboot cleared /private/tmp), or a socket nothing listens on.
|
|
54
|
+
const target = await fs.realpath(paths.socketPath).catch(() => null);
|
|
55
|
+
return target === null || (await socketIsReady(target) && await refusesConnections(target));
|
|
56
|
+
}
|
|
41
57
|
async function safeDirectories(paths) {
|
|
42
58
|
for (const dir of [paths.codexHome, path.dirname(paths.socketPath), path.dirname(paths.pidPath)]) {
|
|
43
59
|
const stat = await info(dir);
|
|
@@ -79,7 +95,7 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
79
95
|
socketPath: paths.socketPath, pidPath: paths.pidPath, platform: paths.platform, ...installation });
|
|
80
96
|
}
|
|
81
97
|
async function verify(paths, facts) {
|
|
82
|
-
if (!await
|
|
98
|
+
if (!await readySocketPath(paths.socketPath)) failMaintenance("daemon_socket_unproven");
|
|
83
99
|
const version = await run(facts.cliPath, ["app-server", "daemon", "version"], paths.options);
|
|
84
100
|
if (version.status !== 0) failMaintenance("daemon_version_unavailable");
|
|
85
101
|
const observed = JSON.parse(version.stdout);
|
|
@@ -103,7 +119,6 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
103
119
|
async function inspect(context, { strict = false } = {}) {
|
|
104
120
|
try {
|
|
105
121
|
const paths = await contextPaths(context);
|
|
106
|
-
if (paths.platform !== "darwin-arm64") failMaintenance("maintenance_platform_unsupported");
|
|
107
122
|
if (!strict) {
|
|
108
123
|
const native = await probe({ env: paths.options.env, timeoutMs: 1_500 });
|
|
109
124
|
// Preserve the existing delivery support matrix: a healthy older
|
|
@@ -116,6 +131,7 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
116
131
|
if (!await info(paths.socketPath) && !await info(paths.pidPath)) {
|
|
117
132
|
return report("needed", "native_endpoint_unavailable", facts);
|
|
118
133
|
}
|
|
134
|
+
if (await serviceStopped(paths)) return report("needed", "service_stopped", facts);
|
|
119
135
|
return await verify(paths, facts);
|
|
120
136
|
} catch (error) {
|
|
121
137
|
const reason = error.reasonCode ?? "service_inspection_failed";
|
|
@@ -166,9 +182,10 @@ export function createCodexServiceSetup({ run = runMaintenanceCommand, probe = p
|
|
|
166
182
|
if (current.state !== "needed") return { ...current, state: "blocked", started };
|
|
167
183
|
const paths = await contextPaths(context);
|
|
168
184
|
if (!same(plan, await installed(paths))) failMaintenance("service_identity_changed");
|
|
169
|
-
// Last check immediately before the only mutating command.
|
|
170
|
-
//
|
|
171
|
-
|
|
185
|
+
// Last check immediately before the only mutating command. ACC deletes no
|
|
186
|
+
// metadata and never stops or restarts a process on this path; a service
|
|
187
|
+
// whose files are all that is left of it is started over them.
|
|
188
|
+
if ((await info(paths.socketPath) || await info(paths.pidPath)) && !await serviceStopped(paths)) {
|
|
172
189
|
return { ...await verify(paths, current), started };
|
|
173
190
|
}
|
|
174
191
|
started = true;
|
|
@@ -63,6 +63,18 @@ export function acceptKey(key) {
|
|
|
63
63
|
return createHash("sha1").update(`${key}${GUID}`).digest("base64");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** True only when a connect to the socket is refused: a file with nobody
|
|
67
|
+
* listening. Accepting, timing out, or any other error is not proof of that. */
|
|
68
|
+
export function refusesConnections(socketPath, { timeoutMs = 1_000 } = {}) {
|
|
69
|
+
return new Promise(resolve => {
|
|
70
|
+
const socket = net.createConnection({ path: socketPath });
|
|
71
|
+
const settle = refused => { clearTimeout(timer); socket.destroy(); resolve(refused); };
|
|
72
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
73
|
+
socket.once("connect", () => settle(false));
|
|
74
|
+
socket.once("error", error => settle(error?.code === "ECONNREFUSED"));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
export function openWebSocketPeer({ socketPath, timeoutMs, path = "/", host = "localhost",
|
|
67
79
|
retainNotifications = true }) {
|
|
68
80
|
const notifications = [];
|
|
@@ -25,13 +25,13 @@ const TRIPLE = /^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
|
25
25
|
* version ACC has already observed is not an older client. Anything unreadable
|
|
26
26
|
* - a probe that failed, "unknown", a vendor string - returns null.
|
|
27
27
|
*/
|
|
28
|
-
function versionOrder(text) {
|
|
28
|
+
export function versionOrder(text) {
|
|
29
29
|
const match = typeof text === "string" && text.toLowerCase() !== "unknown"
|
|
30
30
|
? TRIPLE.exec(text) : null;
|
|
31
31
|
return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function compareVersionOrder(left, right) {
|
|
34
|
+
export function compareVersionOrder(left, right) {
|
|
35
35
|
if (left === null || right === null) return left === right ? 0 : left === null ? -1 : 1;
|
|
36
36
|
for (let index = 0; index < 3; index += 1) {
|
|
37
37
|
if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Capability contract, context projection, config ownership, and the binding
|
|
2
2
|
// that survives between two ephemeral hook processes.
|
|
3
3
|
export { CAPABILITY_SHAPE, assertCapabilities, defineAdapter } from "./capabilities.mjs";
|
|
4
|
-
export { capabilityEvidence, effectiveCapabilities, validateCertification
|
|
4
|
+
export { capabilityEvidence, compareVersionOrder, effectiveCapabilities, validateCertification,
|
|
5
|
+
versionOrder }
|
|
5
6
|
from "./certification.mjs";
|
|
6
|
-
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES,
|
|
7
|
+
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_REASON_CODES,
|
|
7
8
|
compareStableVersions, evaluateNativeEligibility, evaluateVersionContract, parseStableVersion,
|
|
8
9
|
validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
|
|
9
10
|
from "./native-delivery.mjs";
|