agents-can-communicate 0.8.5 → 0.9.0
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 +11 -7
- package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
- package/bin/entrypoints/acc-hook.mjs +33 -19
- package/bin/entrypoints/hook-output.mjs +14 -5
- package/docs/ADAPTER_AUTHORING.md +29 -0
- package/docs/CAPABILITIES.md +7 -6
- package/docs/CLI.md +5 -0
- package/docs/CONFIGURATION.md +15 -10
- package/docs/DESIGN_DECISIONS.md +0 -2
- package/docs/GETTING_STARTED.md +39 -6
- package/docs/HOW_IT_WORKS.md +13 -8
- package/docs/SECURITY_MODEL.md +26 -5
- package/docs/TROUBLESHOOTING.md +63 -8
- package/docs/UPGRADING.md +33 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -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 +13 -2
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
- 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 +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
- package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +42 -11
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +18 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +36 -18
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
- 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-gemini-cli/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +65 -10
- package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
- package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
- package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +11 -3
- package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
- package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
- package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
- package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
- package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
- 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/hook-runner/src/client-pid.mjs +65 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +154 -28
- package/node_modules/@agents-can-communicate/installer/package.json +3 -2
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
- package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -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/protocol/src/config.mjs +6 -2
- package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
- package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
- package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
- package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
- package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
- package/package.json +1 -5
|
@@ -156,19 +156,44 @@ async function removeChannelConfig(versionsRoot) {
|
|
|
156
156
|
{ force: true })));
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// The bundle's commands are `sh "${CLAUDE_PLUGIN_ROOT}/hooks/acc-hook.sh" <event>`.
|
|
160
|
+
const SHELL_HOOK = /^sh "\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\/acc-hook\.sh" (\w+)$/;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Claude Code on Windows runs a hook's shell form through Git Bash, or through
|
|
164
|
+
* PowerShell where Git is absent, and neither can be relied on to have `sh`.
|
|
165
|
+
* The exec form - `command` plus `args`, Claude Code 2.1.139 and later - starts
|
|
166
|
+
* the pinned node directly on the Node shim, with no shell to quote for.
|
|
167
|
+
*/
|
|
168
|
+
async function execFormHooks(file, node) {
|
|
169
|
+
const document = JSON.parse(await readFile(file, "utf8"));
|
|
170
|
+
for (const groups of Object.values(document.hooks)) {
|
|
171
|
+
for (const group of groups) {
|
|
172
|
+
group.hooks = group.hooks.map(hook => {
|
|
173
|
+
const [, event] = SHELL_HOOK.exec(hook.command ?? "") ?? [];
|
|
174
|
+
if (event === undefined) return hook;
|
|
175
|
+
return { type: "command", command: node,
|
|
176
|
+
args: ["${CLAUDE_PLUGIN_ROOT}/hooks/acc-hook.mjs", event] };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
await writeFile(file, `${JSON.stringify(document, null, 2)}\n`);
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
/** A plugin tree with the shim written and the skill's command baked in. */
|
|
160
|
-
async function layOutPlugin(target, { runner, node, cli }) {
|
|
184
|
+
async function layOutPlugin(target, { runner, node, cli, hostPlatform }) {
|
|
161
185
|
await rm(target, { recursive: true, force: true });
|
|
162
186
|
await cp(bundle, target, { recursive: true });
|
|
163
187
|
// The skill ships with a placeholder where the command belongs: `acc` is not
|
|
164
188
|
// on PATH everywhere, and an agent that cannot run it improvises. The shim
|
|
165
189
|
// carries the pinning so each example can name one path. No data home is
|
|
166
190
|
// pinned here: an operator's own ACC_DATA_HOME still wins for this client.
|
|
167
|
-
const cliShim = await writeCliShim({ dir: target, cli, node });
|
|
168
|
-
await bakeSkillCommand({ root: target, cliShim });
|
|
191
|
+
const cliShim = await writeCliShim({ dir: target, cli, node, platform: hostPlatform });
|
|
192
|
+
await bakeSkillCommand({ root: target, cliShim, platform: hostPlatform });
|
|
169
193
|
// The bundle's hooks.json names this script, and nothing else writes it.
|
|
170
194
|
await writeHookShim({ dir: path.join(target, "hooks"), adapterId: "claude_code",
|
|
171
|
-
runner, node });
|
|
195
|
+
runner, node, platform: hostPlatform });
|
|
196
|
+
if (hostPlatform === "win32") await execFormHooks(path.join(target, "hooks", "hooks.json"), node);
|
|
172
197
|
// The copy the client reads says which ACC wrote it. The shipped manifest
|
|
173
198
|
// carries no version, so there is nothing in the repository to fall out of
|
|
174
199
|
// step - which is how every client came to report 0.1.6 while running 0.1.9.
|
|
@@ -177,7 +202,7 @@ async function layOutPlugin(target, { runner, node, cli }) {
|
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null,
|
|
180
|
-
node = process.execPath, now = new Date() }) {
|
|
205
|
+
node = process.execPath, now = new Date(), hostPlatform = process.platform }) {
|
|
181
206
|
// Everything this will merge into, read before a byte is written. A settings
|
|
182
207
|
// file that will not parse used to be discovered after the plugin tree was
|
|
183
208
|
// already on disk, and the install then failed with nineteen files left
|
|
@@ -194,12 +219,12 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
|
|
|
194
219
|
const source = sourceDir(configDir);
|
|
195
220
|
const cached = cachePath(configDir, version);
|
|
196
221
|
|
|
197
|
-
await layOutPlugin(source, { runner, node, cli });
|
|
222
|
+
await layOutPlugin(source, { runner, node, cli, hostPlatform });
|
|
198
223
|
await writeJson(marketplaceFile(configDir), marketplaceManifest());
|
|
199
224
|
// The copy the client runs from. Written here rather than asking the user to
|
|
200
225
|
// run `claude plugin install`, exactly as the Codex adapter does, because the
|
|
201
226
|
// command's only effect is this copy plus the two registry entries below.
|
|
202
|
-
await layOutPlugin(cached, { runner, node, cli });
|
|
227
|
+
await layOutPlugin(cached, { runner, node, cli, hostPlatform });
|
|
203
228
|
// The copy just written, plus the one an upgrade moved off. A client caches a
|
|
204
229
|
// plugin under its version, so every upgrade would otherwise leave the previous
|
|
205
230
|
// release's tree beside this one - invisible while the version never moved,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
1
3
|
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { realpath, stat } from "node:fs/promises";
|
|
4
6
|
|
|
5
|
-
import { compareVersionOrder, versionOrder } from "@agents-can-communicate/adapter-sdk";
|
|
7
|
+
import { compareVersionOrder, isWindowsPlatform, versionOrder } from "@agents-can-communicate/adapter-sdk";
|
|
6
8
|
|
|
9
|
+
import { DAEMON_LAYOUTS } from "./daemon-layouts.mjs";
|
|
7
10
|
import { openWebSocketPeer } from "./ws-json-rpc.mjs";
|
|
8
11
|
|
|
9
12
|
// The Codex App Server queue protocol, captured on codex-cli 0.152.1. Every
|
|
@@ -42,8 +45,70 @@ export function isMethodMissing(error) {
|
|
|
42
45
|
|| (error?.code === INVALID_REQUEST && /unknown variant/.test(String(error?.message ?? "")));
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
const fileExists = file => stat(file).then(found => found.isFile(), () => false);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Windows: the command that relays stdio to the control socket, run from the
|
|
52
|
+
* package the daemon runs from, found the way maintenance finds it: the
|
|
53
|
+
* standalone install when the home has one, else the copy `app-server daemon
|
|
54
|
+
* start` installs (measured on 0.159.3). An .exe, so it starts without cmd.exe
|
|
55
|
+
* whatever the socket path holds. Null when the home holds neither.
|
|
56
|
+
*/
|
|
57
|
+
export async function proxyCommand(socketPath, { exists = fileExists } = {}) {
|
|
58
|
+
const codexHome = path.win32.dirname(path.win32.dirname(socketPath));
|
|
59
|
+
for (const layout of DAEMON_LAYOUTS) {
|
|
60
|
+
for (const name of layout.executables.filter(item => item.endsWith(".exe"))) {
|
|
61
|
+
const file = path.win32.join(codexHome, ...layout.packageDir.split("/"), ...name.split("/"));
|
|
62
|
+
if (await exists(file)) return { file, args: ["app-server", "proxy", "--sock", socketPath] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// `spawnProxy` is the seam the tests start a stand-in with.
|
|
69
|
+
async function startProxy(socketPath, { resolveProxy, spawnProxy }) {
|
|
70
|
+
if (spawnProxy !== undefined) return spawnProxy(socketPath);
|
|
71
|
+
const command = await resolveProxy(socketPath);
|
|
72
|
+
if (command === null) {
|
|
73
|
+
throw Object.assign(new Error(`no codex.exe under ${path.win32.dirname(path.win32.dirname(socketPath))}`
|
|
74
|
+
+ " to relay to its daemon"), { code: "ENOENT" });
|
|
75
|
+
}
|
|
76
|
+
return spawn(command.file, command.args, { stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A child's stdio, once it is started, as the duplex the WebSocket peer speaks
|
|
80
|
+
// over. The peer writes only after "connect".
|
|
81
|
+
function proxyStream(started) {
|
|
82
|
+
const stream = new EventEmitter();
|
|
83
|
+
stream.destroyed = false;
|
|
84
|
+
let child = null;
|
|
85
|
+
stream.write = data => (stream.destroyed || child === null ? false : child.stdin.write(data));
|
|
86
|
+
stream.destroy = () => {
|
|
87
|
+
if (stream.destroyed) return;
|
|
88
|
+
stream.destroyed = true;
|
|
89
|
+
child?.stdin.destroy();
|
|
90
|
+
child?.kill();
|
|
91
|
+
};
|
|
92
|
+
Promise.resolve(started).then(running => {
|
|
93
|
+
child = running;
|
|
94
|
+
if (stream.destroyed) {
|
|
95
|
+
child.kill();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
child.once("spawn", () => stream.emit("connect"));
|
|
99
|
+
child.stdout.on("data", data => stream.emit("data", data));
|
|
100
|
+
child.stdin.on("error", error => stream.emit("error", error));
|
|
101
|
+
child.once("error", error => stream.emit("error", error));
|
|
102
|
+
child.once("exit", () => stream.emit("close"));
|
|
103
|
+
}, error => stream.emit("error", error));
|
|
104
|
+
return stream;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function openCodexAppServer({ socketPath, timeoutMs = 5_000, platform = process.platform,
|
|
108
|
+
spawnProxy, resolveProxy = proxyCommand }) {
|
|
109
|
+
if (!isWindowsPlatform(platform)) return openWebSocketPeer({ socketPath, timeoutMs, retainNotifications: false });
|
|
110
|
+
return openWebSocketPeer({ timeoutMs, retainNotifications: false,
|
|
111
|
+
connect: () => proxyStream(startProxy(socketPath, { resolveProxy, spawnProxy })) });
|
|
47
112
|
}
|
|
48
113
|
|
|
49
114
|
export async function initializeCodex(peer) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The two packages a Codex daemon runs from, measured. The standalone install
|
|
2
|
+
// keeps its PID in app-server.pid (0.154.0-0.159.0); a home with no standalone
|
|
3
|
+
// package gets the daemon's own package from `app-server daemon start` on
|
|
4
|
+
// 0.157.1 and newer, with its PID in daemon.pid (0.159.0, 2026-09-29). A home
|
|
5
|
+
// that has the standalone package keeps using it, so it is looked for first.
|
|
6
|
+
export const DAEMON_LAYOUTS = Object.freeze([
|
|
7
|
+
{ name: "standalone", packageDir: "packages/standalone",
|
|
8
|
+
executables: ["current/bin/codex", "current/codex", "current/bin/codex.exe"],
|
|
9
|
+
pidFile: "app-server-daemon/app-server.pid" },
|
|
10
|
+
{ name: "self-installed", packageDir: "packages/app-server-daemon",
|
|
11
|
+
executables: ["current/bin/codex", "current/bin/codex.exe"],
|
|
12
|
+
pidFile: "app-server-daemon/daemon.pid" },
|
|
13
|
+
]);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readProcessArgs } from "@agents-can-communicate/adapter-sdk";
|
|
2
2
|
|
|
3
|
-
import { runMaintenanceCommand } from "./maintenance-host.mjs";
|
|
4
3
|
|
|
5
4
|
// Which process runs a hook tells a daemon-hosted thread from an embedded chat.
|
|
6
5
|
// Measured on 0.158.0: a daemon thread's hook runs under
|
|
@@ -20,20 +19,23 @@ const VALUE_OPTIONS = new Set(["-c", "--config", "-m", "--model", "-p", "--profi
|
|
|
20
19
|
"--disable", "--local-provider"]);
|
|
21
20
|
|
|
22
21
|
export async function hostArgv(pid) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return result.stdout.trim().split(/\s+/).filter(Boolean);
|
|
22
|
+
const argv = await readProcessArgs(pid, { ps: "/bin/ps", timeoutMs: 500 });
|
|
23
|
+
if (argv === null) throw new Error("host process unreadable");
|
|
24
|
+
return argv;
|
|
27
25
|
}
|
|
28
26
|
|
|
27
|
+
// Either separator, and Windows' `.exe` in any case: `codex`, `/usr/local/bin/codex`,
|
|
28
|
+
// `C:\Users\dana\AppData\Local\Programs\OpenAI\Codex\bin\codex.exe`.
|
|
29
|
+
const programName = word => (word ?? "").split(/[\\/]/).at(-1).replace(/\.exe$/i, "").toLowerCase();
|
|
30
|
+
|
|
29
31
|
// The arguments after Codex itself, or null for any process that is not Codex:
|
|
30
32
|
// `codex ...`, or a node script named codex, as `node [options] codex.js ...`.
|
|
31
33
|
function codexArguments(argv) {
|
|
32
34
|
const [program, ...rest] = argv;
|
|
33
|
-
if (
|
|
34
|
-
if (
|
|
35
|
+
if (programName(program) === "codex") return rest;
|
|
36
|
+
if (programName(program) !== "node") return null;
|
|
35
37
|
const script = rest.findIndex(word => !word.startsWith("-"));
|
|
36
|
-
return script >= 0 &&
|
|
38
|
+
return script >= 0 && programName(rest[script]).replace(/\.[mc]?js$/, "") === "codex"
|
|
37
39
|
? rest.slice(script + 1) : null;
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -93,15 +95,44 @@ export function embeddingOption(argv) {
|
|
|
93
95
|
return null;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Whether this command line is the Codex app's own app server: `codex
|
|
100
|
+
* app-server` that listens nowhere, run from inside an app bundle. Read on the
|
|
101
|
+
* Codex app in ChatGPT.app 26.928: every window serves its threads over stdio
|
|
102
|
+
* this way, never through the shared daemon (openai/codex#41014), so no other
|
|
103
|
+
* process can reach them.
|
|
104
|
+
*/
|
|
105
|
+
export function desktopAppServer(argv) {
|
|
106
|
+
const words = codexArguments(argv);
|
|
107
|
+
if (words === null || !/\.app\/Contents\//.test(argv[0] ?? "")) return false;
|
|
108
|
+
if (words.some(word => word === "--listen" || word.startsWith("--listen="))) return false;
|
|
109
|
+
// The app puts its config overrides before the subcommand:
|
|
110
|
+
// `codex -c features.code_mode_host=true app-server ...` (26.928).
|
|
111
|
+
return subcommandOf(words) === "app-server";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The first word that is neither an option nor an option's value.
|
|
115
|
+
function subcommandOf(words) {
|
|
116
|
+
let value = false;
|
|
117
|
+
for (const word of words) {
|
|
118
|
+
if (value) { value = false; continue; }
|
|
119
|
+
if (word.startsWith("-")) { value = VALUE_OPTIONS.has(word); continue; }
|
|
120
|
+
return word;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
/**
|
|
97
126
|
* For a host that was read and is an interactive TUI, `{ launchOption }` - the
|
|
98
|
-
* option that keeps it embedded, or null when its command line shows none.
|
|
127
|
+
* option that keeps it embedded, or null when its command line shows none. For
|
|
128
|
+
* the Codex app's own app server, `{ launchOption: null, desktop: true }`.
|
|
99
129
|
* Null for any other host or an unreadable one; never throws.
|
|
100
130
|
*/
|
|
101
131
|
export async function embeddedHost(pid, argvOf = hostArgv) {
|
|
102
132
|
try {
|
|
103
133
|
const argv = await argvOf(pid);
|
|
104
|
-
|
|
134
|
+
if (interactiveHost(argv)) return { launchOption: embeddingOption(argv) };
|
|
135
|
+
return desktopAppServer(argv) ? { launchOption: null, desktop: true } : null;
|
|
105
136
|
} catch {
|
|
106
137
|
return null;
|
|
107
138
|
}
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
|
|
7
7
|
import { bakeSkillCommand, blankJson, blankText, removeIfEmpty, removeInstalledTree,
|
|
8
8
|
keepVersions, ownVersion, stampPluginVersion,
|
|
9
|
-
tomlString, writeCliShim, writeForeignJson, writeHookShim }
|
|
9
|
+
tomlString, windowsHookCommand, writeCliShim, writeForeignJson, writeHookShim }
|
|
10
10
|
from "@agents-can-communicate/adapter-sdk";
|
|
11
11
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
12
12
|
import { inspectConfig, readConfig, sandboxOwnership, writeTomlBlock } from "./config-block.mjs";
|
|
@@ -92,11 +92,18 @@ async function readJson(file, fallback) {
|
|
|
92
92
|
// client copies an installed plugin into a cache of its own, so the command has
|
|
93
93
|
// to be absolute: a path relative to the bundle would not survive the copy.
|
|
94
94
|
const shellLiteral = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
95
|
-
|
|
95
|
+
// On Windows Codex runs `commandWindows` in place of `command`, through
|
|
96
|
+
// PowerShell: the pinned node on the Node shim.
|
|
97
|
+
const withShim = (wiring, shim, windows = null) => ({ ...wiring, hooks: Object.fromEntries(
|
|
96
98
|
Object.entries(wiring.hooks).map(([event, entries]) => [event, entries.map(entry => ({
|
|
97
99
|
...entry,
|
|
98
|
-
hooks: entry.hooks.map(hook =>
|
|
99
|
-
|
|
100
|
+
hooks: entry.hooks.map(hook => {
|
|
101
|
+
const event = hook.command.split(" ").pop();
|
|
102
|
+
if (windows === null) return { ...hook, command: `sh ${shellLiteral(shim)} ${event}` };
|
|
103
|
+
return { ...hook,
|
|
104
|
+
command: windowsHookCommand("portable", { node: windows.node, shim, args: [event] }),
|
|
105
|
+
commandWindows: windowsHookCommand("powershell", { node: windows.node, shim, args: [event] }) };
|
|
106
|
+
}),
|
|
100
107
|
}))])) });
|
|
101
108
|
|
|
102
109
|
const writeJson = async (file, value) => {
|
|
@@ -163,7 +170,8 @@ const sandboxReview = (config, file, stateRoot) =>
|
|
|
163
170
|
|
|
164
171
|
export async function installCodexPlugin({ home, agentsHome = home,
|
|
165
172
|
codexHome = path.join(home, ".codex"), dataHome, stateRoot, runner, node, cli, keepPreviousVersion = null,
|
|
166
|
-
requestedLivePolicy, livePolicy, clientVersion, platform, receiverSockets
|
|
173
|
+
requestedLivePolicy, livePolicy, clientVersion, platform, receiverSockets,
|
|
174
|
+
hostPlatform = process.platform }) {
|
|
167
175
|
// Read before writing, so a manifest that will not parse is found before a
|
|
168
176
|
// plugin tree is laid down that nothing will then be able to remove.
|
|
169
177
|
const existing = await readJson(marketplacePath(agentsHome), { name: MARKETPLACE,
|
|
@@ -194,12 +202,13 @@ export async function installCodexPlugin({ home, agentsHome = home,
|
|
|
194
202
|
// The skill ships with a placeholder where the command belongs: `acc` is
|
|
195
203
|
// not on PATH everywhere, and an agent that cannot run it improvises. The
|
|
196
204
|
// shim carries the pinning so each example can name one path.
|
|
197
|
-
const cliShim = await writeCliShim({ dir: target, cli, node, dataHome });
|
|
198
|
-
await bakeSkillCommand({ root: target, cliShim });
|
|
205
|
+
const cliShim = await writeCliShim({ dir: target, cli, node, dataHome, platform: hostPlatform });
|
|
206
|
+
await bakeSkillCommand({ root: target, cliShim, platform: hostPlatform });
|
|
199
207
|
const shim = await writeHookShim({ dir: target, adapterId: "codex",
|
|
200
|
-
dataHome, runner, node });
|
|
208
|
+
dataHome, runner, node, platform: hostPlatform });
|
|
201
209
|
await writeJson(path.join(target, "hooks.json"),
|
|
202
|
-
withShim(await readJson(path.join(bundle, "hooks.json"), { hooks: {} }), shim
|
|
210
|
+
withShim(await readJson(path.join(bundle, "hooks.json"), { hooks: {} }), shim,
|
|
211
|
+
hostPlatform === "win32" ? { node: node ?? process.execPath } : null));
|
|
203
212
|
|
|
204
213
|
const file = marketplacePath(agentsHome);
|
|
205
214
|
// Ownership is the entry's own name. Recording it as an extra key beside the
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { access, lstat,
|
|
3
|
+
import { access, lstat, realpath } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { closedTo, isWindowsPlatform, openRegularNoFollow, readWindowsProcess, resolveExecutable,
|
|
8
|
+
runExecutable } from "@agents-can-communicate/adapter-sdk";
|
|
9
|
+
|
|
6
10
|
import { compareVersions, versionOrder } from "./app-server-client.mjs";
|
|
11
|
+
import { DAEMON_LAYOUTS } from "./daemon-layouts.mjs";
|
|
12
|
+
import { socketIsReady } from "./native-endpoint.mjs";
|
|
7
13
|
|
|
8
14
|
export const MINIMUM_MAINTENANCE_CLI = "0.154.0";
|
|
9
15
|
export const absolute = value => typeof value === "string" && path.isAbsolute(value)
|
|
10
16
|
&& !/[\0\r\n]/.test(value);
|
|
11
17
|
export const failMaintenance = reasonCode => { throw Object.assign(new Error(reasonCode), { reasonCode }); };
|
|
12
18
|
const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
|
|
19
|
+
// `ps -o lstart` on POSIX; a FILETIME string on Windows, where Codex records one
|
|
20
|
+
// and WMI reports one to the microsecond (measured on 0.159.3).
|
|
21
|
+
const FILETIME = /^\d{15,19}$/;
|
|
13
22
|
export const startTimeValid = value => typeof value === "string"
|
|
14
|
-
&& /^[A-Z][a-z]{2} [A-Z][a-z]{2} [ \d]\d \d\d:\d\d:\d\d \d{4}$/.test(value);
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// 0.157.1 and newer, with its PID in daemon.pid (0.159.0, 2026-09-29). A home
|
|
19
|
-
// that has the standalone package keeps using it, so it is looked for first.
|
|
20
|
-
const DAEMON_LAYOUTS = Object.freeze([
|
|
21
|
-
{ name: "standalone", packageDir: "packages/standalone", executables: ["current/bin/codex", "current/codex"],
|
|
22
|
-
pidFile: "app-server-daemon/app-server.pid" },
|
|
23
|
-
{ name: "self-installed", packageDir: "packages/app-server-daemon", executables: ["current/bin/codex"],
|
|
24
|
-
pidFile: "app-server-daemon/daemon.pid" },
|
|
25
|
-
]);
|
|
23
|
+
&& (/^[A-Z][a-z]{2} [A-Z][a-z]{2} [ \d]\d \d\d:\d\d:\d\d \d{4}$/.test(value) || FILETIME.test(value));
|
|
24
|
+
// WMI keeps a creation time in microseconds and a FILETIME counts tenths of one.
|
|
25
|
+
const sameStart = (observed, recorded) => (FILETIME.test(observed ?? "") && FILETIME.test(recorded ?? "")
|
|
26
|
+
? BigInt(observed) / 10n === BigInt(recorded) / 10n : observed === recorded);
|
|
26
27
|
export const managedExecutablePaths = codexHome => DAEMON_LAYOUTS.flatMap(layout =>
|
|
27
28
|
layout.executables.map(name => path.join(codexHome, layout.packageDir, name)));
|
|
28
29
|
|
|
@@ -41,6 +42,13 @@ export async function resolveLsof({ access: check = access } = {}) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function runMaintenanceCommand(command, args, options) {
|
|
45
|
+
// Windows finds codex as an npm .cmd, which starts only through cmd.exe.
|
|
46
|
+
if (process.platform === "win32") {
|
|
47
|
+
return runExecutable(command, args, { timeout: 20_000, ...options, maxBuffer: 131_072 })
|
|
48
|
+
.then(({ stdout, stderr }) => ({ status: 0, stdout, stderr }),
|
|
49
|
+
error => ({ status: typeof error?.code === "number" ? error.code : null,
|
|
50
|
+
stdout: error?.stdout ?? "", stderr: error?.stderr ?? "" }));
|
|
51
|
+
}
|
|
44
52
|
return new Promise(resolve => execFile(command, args,
|
|
45
53
|
{ timeout: 20_000, ...options, maxBuffer: 131_072, windowsHide: true },
|
|
46
54
|
(error, stdout, stderr) => resolve({ status: error ?
|
|
@@ -96,6 +104,11 @@ export async function metadataExists(file) {
|
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
async function resolveCli(pathEnv) {
|
|
107
|
+
if (process.platform === "win32") {
|
|
108
|
+
const found = await resolveExecutable("codex", { pathEnv });
|
|
109
|
+
if (found !== null) return found;
|
|
110
|
+
failMaintenance("maintenance_cli_unavailable");
|
|
111
|
+
}
|
|
99
112
|
for (const directory of String(pathEnv ?? "").split(path.delimiter)) {
|
|
100
113
|
if (!absolute(directory)) continue;
|
|
101
114
|
const candidate = path.join(directory, "codex");
|
|
@@ -130,12 +143,12 @@ export async function probeMaintenanceInstall(paths, run) {
|
|
|
130
143
|
return { cliPath, cliVersion: version, managedVersion };
|
|
131
144
|
}
|
|
132
145
|
|
|
133
|
-
export async function readMaintenancePid(file) {
|
|
146
|
+
export async function readMaintenancePid(file, { platform = process.platform } = {}) {
|
|
134
147
|
let handle;
|
|
135
148
|
try {
|
|
136
|
-
handle = await
|
|
149
|
+
handle = await openRegularNoFollow(file, undefined, { platform });
|
|
137
150
|
const info = await handle.stat();
|
|
138
|
-
if (!info.isFile() || !own(info) || info.size > 4_096 || (info
|
|
151
|
+
if (!info.isFile() || !own(info) || info.size > 4_096 || !closedTo(info, 0o022, { platform })) {
|
|
139
152
|
failMaintenance("daemon_identity_unavailable");
|
|
140
153
|
}
|
|
141
154
|
const value = JSON.parse(await handle.readFile("utf8"));
|
|
@@ -146,7 +159,23 @@ export async function readMaintenancePid(file) {
|
|
|
146
159
|
} finally { await handle?.close(); }
|
|
147
160
|
}
|
|
148
161
|
|
|
149
|
-
|
|
162
|
+
const processAlive = pid => {
|
|
163
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code === "EPERM"; }
|
|
164
|
+
};
|
|
165
|
+
// Windows reads one process through WMI. An empty answer cannot tell a gone
|
|
166
|
+
// process from an unreadable one, so whether the pid lives is asked first.
|
|
167
|
+
const WINDOWS_SYSTEM = Object.freeze({ alive: processAlive,
|
|
168
|
+
readProcess: pid => readWindowsProcess(pid, { timeoutMs: 5_000 }) });
|
|
169
|
+
|
|
170
|
+
export async function observeMaintenanceProcess(pid, paths, run, { system = WINDOWS_SYSTEM } = {}) {
|
|
171
|
+
if (isWindowsPlatform(paths.platform)) {
|
|
172
|
+
if (!system.alive(pid)) return { state: "dead" };
|
|
173
|
+
const found = await system.readProcess(pid).catch(() => null);
|
|
174
|
+
if (found?.pid !== pid || !startTimeValid(found.start) || typeof found.cmd !== "string") {
|
|
175
|
+
return { state: "unknown" };
|
|
176
|
+
}
|
|
177
|
+
return { state: "alive", processStartTime: found.start, command: found.cmd };
|
|
178
|
+
}
|
|
150
179
|
const response = await run("/bin/ps", ["-p", String(pid), "-o", "lstart=,command="], paths.options);
|
|
151
180
|
if (response.status === 1 && response.stdout === "" && response.stderr === "") return { state: "dead" };
|
|
152
181
|
if (response.status !== 0) return { state: "unknown" };
|
|
@@ -170,16 +199,28 @@ export function socketListedIn(stdout, socketPaths) {
|
|
|
170
199
|
// `--managed-daemon` appended. The executable must live in the package tree of
|
|
171
200
|
// the installed layout and be the managed binary itself, however it is named.
|
|
172
201
|
const DAEMON_COMMAND = /^(\/.+?) app-server --listen unix:\/\/(?: --managed-daemon)?$/;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
// Windows quotes the path and gives it the \\?\ prefix (measured on 0.159.3).
|
|
203
|
+
const WINDOWS_DAEMON_COMMAND = /^"(?:\\\\\?\\)?([^"]+)" app-server --listen unix:\/\/(?: --managed-daemon)?$/;
|
|
204
|
+
|
|
205
|
+
export async function verifyMaintenanceProcess(snapshot, paths, run, { lsof = resolveLsof,
|
|
206
|
+
system = WINDOWS_SYSTEM } = {}) {
|
|
207
|
+
const windows = isWindowsPlatform(paths.platform);
|
|
208
|
+
const process = await observeMaintenanceProcess(snapshot.pid, paths, run, { system });
|
|
209
|
+
const executable = (windows ? WINDOWS_DAEMON_COMMAND : DAEMON_COMMAND).exec(process.command ?? "")?.[1]
|
|
210
|
+
?? null;
|
|
177
211
|
const packages = (paths.packageDir ?? path.join(paths.codexHome, "packages", "standalone")) + path.sep;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
212
|
+
const inside = executable !== null && (windows
|
|
213
|
+
? executable.toLowerCase().startsWith(packages.toLowerCase()) : executable.startsWith(packages));
|
|
214
|
+
if (process.state !== "alive" || !sameStart(process.processStartTime, snapshot.processStartTime)
|
|
215
|
+
|| !inside || await realpath(executable).catch(() => null) !== await realpath(paths.managedPath)) {
|
|
181
216
|
failMaintenance("daemon_identity_unavailable");
|
|
182
217
|
}
|
|
218
|
+
// Windows has no lsof, and Node's lstat refuses the AF_UNIX socket file; Codex
|
|
219
|
+
// keeps the socket's directory to its user, and that directory lists it.
|
|
220
|
+
if (windows) {
|
|
221
|
+
if (!await socketIsReady(paths.socketPath, { platform: "win32" })) failMaintenance("daemon_socket_unproven");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
183
224
|
// 0.157.1 lists the socket by the path it listens on, not by the symlink it
|
|
184
225
|
// reports; either names the same socket.
|
|
185
226
|
const listening = [paths.socketPath, await realpath(paths.socketPath).catch(() => null)]
|
|
@@ -192,8 +233,8 @@ export async function verifyMaintenanceProcess(snapshot, paths, run, { lsof = re
|
|
|
192
233
|
}
|
|
193
234
|
}
|
|
194
235
|
|
|
195
|
-
export async function approvedProcessIsDead(expected, paths, run) {
|
|
196
|
-
const observed = await observeMaintenanceProcess(expected.pid, paths, run);
|
|
236
|
+
export async function approvedProcessIsDead(expected, paths, run, { system = WINDOWS_SYSTEM } = {}) {
|
|
237
|
+
const observed = await observeMaintenanceProcess(expected.pid, paths, run, { system });
|
|
197
238
|
return observed.state === "dead" || (observed.state === "alive"
|
|
198
|
-
&& observed.processStartTime
|
|
239
|
+
&& !sameStart(observed.processStartTime, expected.processStartTime));
|
|
199
240
|
}
|
|
@@ -36,10 +36,12 @@ async function usingPeer(socketPath, timeoutMs, open, run) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export async function probeNativeDelivery({ timeoutMs = 750, env = process.env,
|
|
39
|
-
open = openCodexAppServer } = {}) {
|
|
39
|
+
open = openCodexAppServer, platform = process.platform } = {}) {
|
|
40
40
|
const unsupported = (reasonCode, clientVersion = null) => ({ supported: false, clientVersion,
|
|
41
41
|
protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
|
|
42
|
-
|
|
42
|
+
// Windows reaches the daemon's AF_UNIX socket through `codex app-server proxy`
|
|
43
|
+
// (openCodexAppServer); everything past the transport is the same.
|
|
44
|
+
const socketPath = await readySocketPath(controlSocketPath(env), { platform });
|
|
43
45
|
if (socketPath === null) return unsupported("native_endpoint_unavailable");
|
|
44
46
|
try {
|
|
45
47
|
return await usingPeer(socketPath, timeoutMs, open, async peer => {
|
|
@@ -107,8 +109,9 @@ export async function verifyReceiver(peer, endpoint, { probe = probeCodexQueue,
|
|
|
107
109
|
// A chat that runs embedded has no receiver for any check below to find. Its
|
|
108
110
|
// host process is read only after a check failed, so a bound session never pays
|
|
109
111
|
// for it, and the reason then names the cause instead of the symptom.
|
|
110
|
-
export async function bindNativeSession({ argvOf = hostArgv,
|
|
111
|
-
|
|
112
|
+
export async function bindNativeSession({ argvOf = hostArgv, platform = process.platform,
|
|
113
|
+
...options } = {}) {
|
|
114
|
+
const result = await bindAttempt({ ...options, platform });
|
|
112
115
|
if (result.supported || !Number.isInteger(options.clientPid) || options.clientPid <= 0) return result;
|
|
113
116
|
const host = await embeddedHost(options.clientPid, argvOf);
|
|
114
117
|
if (host === null) return result;
|
|
@@ -117,14 +120,15 @@ export async function bindNativeSession({ argvOf = hostArgv, ...options } = {})
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
async function bindAttempt({ event, clientPid, clientVersion, runtimeDir,
|
|
120
|
-
env = process.env, timeoutMs = 750, now = Date.now, open = openCodexAppServer
|
|
123
|
+
env = process.env, timeoutMs = 750, now = Date.now, open = openCodexAppServer,
|
|
124
|
+
platform = process.platform } = {}) {
|
|
121
125
|
const rejected = reason => closed(clientVersion, reason);
|
|
122
126
|
if (!Number.isInteger(clientPid) || clientPid <= 0) return rejected("client_process_unknown");
|
|
123
127
|
if (versionOrder(clientVersion) === null) return rejected("version_unavailable");
|
|
124
128
|
if (typeof event?.sessionId !== "string" || event.sessionId === "") return rejected("handshake_failed");
|
|
125
129
|
const cwd = await canonicalCwd(event.cwd);
|
|
126
130
|
if (cwd === null) return rejected("workspace_identity_unavailable");
|
|
127
|
-
const socketPath = await readySocketPath(controlSocketPath(env));
|
|
131
|
+
const socketPath = await readySocketPath(controlSocketPath(env), { platform });
|
|
128
132
|
if (socketPath === null) return rejected("handshake_failed");
|
|
129
133
|
try {
|
|
130
134
|
return await usingPeer(socketPath, timeoutMs, open, async peer => {
|
|
@@ -138,7 +142,7 @@ async function bindAttempt({ event, clientPid, clientVersion, runtimeDir,
|
|
|
138
142
|
// answered so the binding and its later reads reflect the serving
|
|
139
143
|
// process, not the caller's claim.
|
|
140
144
|
const verified = { ...endpoint, clientVersion: servingVersion };
|
|
141
|
-
await writeNativeEndpoint({ runtimeDir, record: verified });
|
|
145
|
+
await writeNativeEndpoint({ runtimeDir, record: verified, platform });
|
|
142
146
|
return handshake(verified, now);
|
|
143
147
|
});
|
|
144
148
|
} catch (error) {
|
|
@@ -163,15 +167,15 @@ async function bindAttempt({ event, clientPid, clientVersion, runtimeDir,
|
|
|
163
167
|
// support: the endpoint records what the daemon serves, the binding recorded
|
|
164
168
|
// what `codex --version` printed, and a daemon that had updated under its CLI
|
|
165
169
|
// was refused here on that difference alone.
|
|
166
|
-
async function receiverFor(binding, runtimeDir) {
|
|
167
|
-
const endpoint = await readNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
|
|
168
|
-
return endpoint !== null && await socketIsReady(endpoint.socketPath) ? endpoint : null;
|
|
170
|
+
async function receiverFor(binding, runtimeDir, platform = process.platform) {
|
|
171
|
+
const endpoint = await readNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef, platform });
|
|
172
|
+
return endpoint !== null && await socketIsReady(endpoint.socketPath, { platform }) ? endpoint : null;
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 750,
|
|
172
|
-
now = Date.now, open = openCodexAppServer } = {}) {
|
|
176
|
+
now = Date.now, open = openCodexAppServer, platform = process.platform } = {}) {
|
|
173
177
|
const rejected = reason => closed(binding?.clientVersion, reason);
|
|
174
|
-
const endpoint = await receiverFor(binding, runtimeDir);
|
|
178
|
+
const endpoint = await receiverFor(binding, runtimeDir, platform);
|
|
175
179
|
if (endpoint === null) return rejected("handshake_failed");
|
|
176
180
|
try {
|
|
177
181
|
return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
|
|
@@ -185,10 +189,10 @@ export async function refreshNativeSession({ binding, runtimeDir, timeoutMs = 75
|
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 5_000,
|
|
188
|
-
open = openCodexAppServer } = {}) {
|
|
192
|
+
open = openCodexAppServer, platform = process.platform } = {}) {
|
|
189
193
|
const rejected = safeErrorCode => ({ accepted: false, transport: "codex-app-server",
|
|
190
194
|
clientVersion: binding?.clientVersion ?? null, safeErrorCode });
|
|
191
|
-
const endpoint = await receiverFor(binding, runtimeDir);
|
|
195
|
+
const endpoint = await receiverFor(binding, runtimeDir, platform);
|
|
192
196
|
if (endpoint === null) return rejected("recipient_unavailable");
|
|
193
197
|
try {
|
|
194
198
|
return await usingPeer(endpoint.socketPath, timeoutMs, open, async peer => {
|
|
@@ -207,8 +211,8 @@ export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 5
|
|
|
207
211
|
}
|
|
208
212
|
}
|
|
209
213
|
|
|
210
|
-
export const retireNativeSession = ({ binding, runtimeDir }) =>
|
|
211
|
-
removeNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
|
|
214
|
+
export const retireNativeSession = ({ binding, runtimeDir, platform = process.platform }) =>
|
|
215
|
+
removeNativeEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef, platform });
|
|
212
216
|
|
|
213
217
|
const OPEN_A_CHAT = "open a new Codex chat; if Codex does not start its app-server daemon itself, "
|
|
214
218
|
+ "run `codex app-server daemon start` first";
|
|
@@ -235,9 +239,20 @@ function embeddedNotice(launchOption) {
|
|
|
235
239
|
+ `it on its own app server. For live delivery, ${advice}.` };
|
|
236
240
|
}
|
|
237
241
|
|
|
242
|
+
// A chat the Codex app serves itself: nothing the user starts differently
|
|
243
|
+
// changes that, so the notice says what happens instead of what to do.
|
|
244
|
+
const DESKTOP_NOTICE = Object.freeze({
|
|
245
|
+
line: "ACC: live peer delivery is off in this Codex chat. The Codex app runs it on its own app "
|
|
246
|
+
+ "server, which no other process can reach, so peer messages arrive with the user's next "
|
|
247
|
+
+ "prompt. ACC has shown the user this.",
|
|
248
|
+
userMessage: "ACC: live peer delivery is off in this chat: the Codex app runs it on its own app "
|
|
249
|
+
+ "server. Peer messages arrive with your next prompt.",
|
|
250
|
+
});
|
|
251
|
+
|
|
238
252
|
// One delivered notice per chat. The marker is created exclusively, so a
|
|
239
253
|
// second turn finds it; a notice the runner did not deliver gives it back.
|
|
240
|
-
export async function nativeActivationHint({ event, nativeBinding, runtimeDir
|
|
254
|
+
export async function nativeActivationHint({ event, nativeBinding, runtimeDir, clientPid,
|
|
255
|
+
argvOf = hostArgv }) {
|
|
241
256
|
if (nativeBinding?.state !== "degraded" || nativeBinding.reasonCode !== "client_session_embedded"
|
|
242
257
|
|| typeof event?.sessionId !== "string" || event.sessionId === ""
|
|
243
258
|
|| typeof runtimeDir !== "string") return null;
|
|
@@ -249,7 +264,10 @@ export async function nativeActivationHint({ event, nativeBinding, runtimeDir })
|
|
|
249
264
|
} catch {
|
|
250
265
|
return null;
|
|
251
266
|
}
|
|
252
|
-
|
|
267
|
+
const host = Number.isInteger(clientPid) && clientPid > 0 && !isLaunchOption(nativeBinding.launchOption)
|
|
268
|
+
? await embeddedHost(clientPid, argvOf) : null;
|
|
269
|
+
const notice = host?.desktop === true ? DESKTOP_NOTICE : embeddedNotice(nativeBinding.launchOption);
|
|
270
|
+
return { ...notice, release: () => rm(marker, { force: true }) };
|
|
253
271
|
}
|
|
254
272
|
|
|
255
273
|
function renderText(message) {
|