@minhspark/codex-mcp-bridge 1.26.8 → 1.26.10
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +45 -0
- package/docs/daily-handoffs.md +18 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +3 -5
- package/scripts/install-codex-mcp.mjs +2 -5
- package/scripts/install-native-relay.mjs +4 -7
- package/scripts/setup.mjs +2 -1
- package/scripts/update.mjs +22 -13
- package/src/antigravity-bridge.mjs +50 -10
- package/src/antigravity-deliveries.mjs +27 -2
- package/src/antigravity-desktop.mjs +206 -2
- package/src/antigravity-outbound-targets.mjs +1 -1
- package/src/antigravity-sidecar.mjs +5 -4
- package/src/background-codex-worker.mjs +26 -4
- package/src/claude-bridge.mjs +1 -1
- package/src/codex-mcp-registration.mjs +21 -0
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/peer-protocol.mjs +116 -14
- package/src/platform.mjs +19 -18
- package/src/recipient-preflight.mjs +3 -2
- package/src/setup.mjs +18 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.26.10] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Recover Claude replies whose native ancestry passes through a verified Monitor notification. Preserve outstanding background work and reject unrelated or malformed notifications. Pending-delivery diagnostics now retain the original message ID and direct callers to its receipt without suggesting a duplicate send.
|
|
12
|
+
- Observe Antigravity Desktop replies through a bounded, read-only native transcript adapter. New deliveries retain a pre-send boundary and prompt hash; completion requires the exact original message, execution, public final response and idle conversation. Unsupported schemas, interleaved messages and ambiguous matches remain unconfirmed. Reply reads retain sender, account and project isolation.
|
|
13
|
+
- Migrate pending Claude transcript observations during future worker updates only after a unique native message proves acceptance. Held and unobserved deliveries keep their locks and defer reload, preserving their original reply channel.
|
|
14
|
+
- Resolve Codex CLI candidates from the worker's actual environment, skip incomplete version directories and retry read-only inspection once if an unpinned executable disappears during an app update. Explicit executable selections, authentication failures and real jobs are never retried or silently redirected.
|
|
15
|
+
- Report background relay readiness separately from unverified Desktop connections after updating. A ready background worker does not establish a live native Desktop relay.
|
|
16
|
+
- Reject malformed transcript session identifiers before composing filesystem paths, and validate executable candidates before inspecting their file type. Configured external executable locations remain supported.
|
|
17
|
+
|
|
18
|
+
### Verification and limits
|
|
19
|
+
|
|
20
|
+
- Regression coverage includes Monitor ancestry, pending receipt ownership, reload safety, exact native reply correlation, account changes and CLI replacement races. Historical native transcripts confirmed both reply-observation failures without resending their prompts.
|
|
21
|
+
- An already-running older worker can still defer its own upgrade while a receipt is pending. Preserve that receipt and any verified historical result before reconnecting its MCP client; a missing receipt after reconnecting is not evidence of non-delivery. Legacy Antigravity receipts without the original account binding cannot disclose recovered reply text.
|
|
22
|
+
- A Desktop relay exiting on another machine still requires that machine's MCP host log and shutdown reason. This release does not infer an idle-chat cause or replace Desktop delivery with a CLI worker.
|
|
23
|
+
|
|
24
|
+
### Upgrade
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
npx.cmd -y @minhspark/codex-mcp-bridge@latest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For noninteractive Windows updates, use `codex-bridge-update --user`. Reconnect affected existing MCP clients after active work finishes, then check their status tools. The global installation path remains available with `npm install -g @minhspark/codex-mcp-bridge@latest --allow-scripts=@minhspark/codex-mcp-bridge --foreground-scripts`.
|
|
31
|
+
|
|
32
|
+
## [1.26.9] - 2026-11-03
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Shorten the "Applying managed settings" step of `npx @minhspark/codex-mcp-bridge@latest`. Each Codex `mcp` command takes several seconds to start on a machine with many MCP servers and plugins, and one update ran about sixteen of them. The Codex installers now read their own entry with one `codex mcp get` instead of listing every server first, the verification reads all three registrations from a single `codex mcp list`, and the updater no longer runs a second, separate verification process after setup has already verified the saved settings.
|
|
37
|
+
- Show which component the updater is configuring while managed settings are applied, instead of a single line that looked frozen for a minute or more.
|
|
38
|
+
- Stop reporting `MCP registration is missing` for `claude-bridge`, `codex-native-relay` and `antigravity-bridge` when Codex CLI was merely too slow to answer. A slow inventory now says so, and the three registrations are reported as not checked. Codex `mcp` commands used by setup may take up to 120 seconds instead of 30, so a busy computer no longer fails an otherwise complete update.
|
|
39
|
+
|
|
40
|
+
### Verification
|
|
41
|
+
|
|
42
|
+
- Regression coverage checks that verification makes exactly one Codex inventory call, that only Codex's own "No MCP server named" error counts as an absent entry, that a timed-out inventory is reported as unchecked, and that the updater runs setup once and relays only its progress lines.
|
|
43
|
+
|
|
44
|
+
### Upgrade
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
npx.cmd -y @minhspark/codex-mcp-bridge@latest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This command runs the 1.26.9 updater itself, so the shorter sequence applies to this update.
|
|
51
|
+
|
|
7
52
|
## [1.26.8] - 2026-11-03
|
|
8
53
|
|
|
9
54
|
### Fixed
|
package/docs/daily-handoffs.md
CHANGED
|
@@ -82,6 +82,12 @@ new task, account, process, prompt and directory to match.
|
|
|
82
82
|
turn. The exact dispatch must occur after the saved watermark, and commentary
|
|
83
83
|
alone is never a final reply. Receipt observation resolves that exact dispatch
|
|
84
84
|
even after newer turns start; it does not substitute the latest turn's answer.
|
|
85
|
+
For Antigravity Desktop, retain `requestId` and use
|
|
86
|
+
`read_antigravity_delivery`. `accepted` confirms only native acceptance;
|
|
87
|
+
`reply_received` requires an exact native message and execution with a public
|
|
88
|
+
final reply. Unsupported transcript formats or ambiguous interleaving keep
|
|
89
|
+
the delivery unconfirmed. Receipts without their original account binding
|
|
90
|
+
cannot disclose recovered reply text. None of these states authorizes a resend.
|
|
85
91
|
5. An `interrupted` receipt ends observation only: task outcome is unknown and
|
|
86
92
|
background work may continue. Inspect the existing task before a scoped
|
|
87
93
|
follow-up; do not repeat card creation or file operations.
|
|
@@ -113,6 +119,18 @@ defer hot reload. Save the original receipt and any verified historical reply
|
|
|
113
119
|
before reconnecting. An unknown ID after reconnecting is not evidence of
|
|
114
120
|
non-delivery. Verify recovery with one harmless message, not by resending work.
|
|
115
121
|
|
|
122
|
+
Updated workers can transfer a pending Claude transcript observation only when
|
|
123
|
+
the exact native message has already been observed. Held or unobserved deliveries
|
|
124
|
+
retain their original callback channel and continue to defer reload. Installing
|
|
125
|
+
new code cannot change the migration behavior of an already-running old worker.
|
|
126
|
+
|
|
127
|
+
Updater success verifies managed settings and the background relay separately
|
|
128
|
+
from Desktop connectivity. Check `codex_bridge_status`, `claude_bridge_status`
|
|
129
|
+
and `antigravity_bridge_status` in the existing clients. If a native companion
|
|
130
|
+
starts and then exits, retain its MCP host stderr and shutdown reason. A missing
|
|
131
|
+
relay pipe alone does not identify the cause; do not infer that an empty chat or
|
|
132
|
+
a dummy prompt is required to keep it alive.
|
|
133
|
+
|
|
116
134
|
Card revocation and communication revocation are independent. Archiving a
|
|
117
135
|
conversation does not guarantee worktree/branch removal. Check running work,
|
|
118
136
|
uncommitted files and unique commits before cleanup; preserve configuration,
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "codex-mcp-bridge",
|
|
4
|
-
"version": "1.26.
|
|
4
|
+
"version": "1.26.10",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { applyStdioMcpRegistration, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
7
|
+
import { applyStdioMcpRegistration, CODEX_MCP_TIMEOUT_MS, readCodexMcpServer, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
8
8
|
import { bridgeEntrypointConflict, isBridgeSource, isMainModule, isNodeLauncher, stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
|
|
9
9
|
import { backupRegularConfigFile } from "../src/project-policy.mjs";
|
|
10
10
|
import { homeDir, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
|
|
@@ -256,10 +256,8 @@ export function runAntigravityInstaller(args = process.argv.slice(2), { prepareR
|
|
|
256
256
|
let codexExisting = null;
|
|
257
257
|
if (options.targets.includes("codex")) {
|
|
258
258
|
const codexBin = validateCodexExecutable(resolveCodexBin(process.env.CODEX_EXE));
|
|
259
|
-
run = (command) => execFileSync(codexBin, command, { encoding: "utf8", env: spawnEnv(), windowsHide: true, shell: false, timeout:
|
|
260
|
-
|
|
261
|
-
if (!Array.isArray(inventory)) throw new Error("Codex returned an invalid MCP inventory");
|
|
262
|
-
if (inventory.some((item) => item.name === NAME)) codexExisting = JSON.parse(run(["mcp", "get", NAME, "--json"]));
|
|
259
|
+
run = (command) => execFileSync(codexBin, command, { encoding: "utf8", env: spawnEnv(), windowsHide: true, shell: false, timeout: CODEX_MCP_TIMEOUT_MS, stdio: "pipe" });
|
|
260
|
+
codexExisting = readCodexMcpServer(run, NAME);
|
|
263
261
|
}
|
|
264
262
|
const plan = planAntigravityInstallation({ ...options, home, root, node, codexExisting });
|
|
265
263
|
if (!options.remove) {
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { PLATFORM_LABEL, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
|
|
8
8
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
9
9
|
import { desktopTasksConfigured } from "../src/native-relay.mjs";
|
|
10
|
-
import { applyStdioMcpRegistration, codexMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
10
|
+
import { applyStdioMcpRegistration, codexMcpRegistration, readCodexMcpServer } from "../src/codex-mcp-registration.mjs";
|
|
11
11
|
import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
|
|
12
12
|
import { codexAccessPaths, enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
|
|
13
13
|
import { stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
|
|
@@ -37,10 +37,7 @@ if (remove) {
|
|
|
37
37
|
process.exit(0);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
|
|
42
|
-
const existing = servers.some((server) => server.name === serverName)
|
|
43
|
-
? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
|
|
40
|
+
const existing = readCodexMcpServer(run, serverName);
|
|
44
41
|
const registration = codexMcpRegistration({ name: serverName, existing, node: nodeBin, entry, entryArgs, env: process.env, desktopOnly: desktopTasksConfigured() });
|
|
45
42
|
createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...registration.environment }) });
|
|
46
43
|
const applied = applyStdioMcpRegistration({ name: serverName, registration, existing, run, configPath: codexAccessPaths().config });
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
spawnEnv,
|
|
18
18
|
} from "../src/platform.mjs";
|
|
19
19
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
20
|
-
import { applyStdioMcpRegistration, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
20
|
+
import { applyStdioMcpRegistration, readCodexMcpServer, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
21
21
|
import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
|
|
22
22
|
import { codexAccessPaths, enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
|
|
23
23
|
import { stableInstallPath } from "../src/install-paths.mjs";
|
|
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
|
|
|
25
25
|
|
|
26
26
|
exitForVersionRequest(import.meta.url);
|
|
27
27
|
|
|
28
|
-
const VERSION = "1.26.
|
|
28
|
+
const VERSION = "1.26.10";
|
|
29
29
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
30
30
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
31
31
|
const entryArgs = ["native-relay-companion.mjs"];
|
|
@@ -69,10 +69,7 @@ if (!IS_MACOS && !IS_WINDOWS) {
|
|
|
69
69
|
console.log("claude-bridge will keep using the app-server path here.");
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const
|
|
73
|
-
if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
|
|
74
|
-
const existingServer = servers.some((server) => server.name === serverName)
|
|
75
|
-
? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
|
|
72
|
+
const existingServer = readCodexMcpServer(run, serverName);
|
|
76
73
|
/**
|
|
77
74
|
* Registering under whichever Node happens to run this installer is what made
|
|
78
75
|
* the companion unreachable on macOS: Codex Desktop rejects a peer whose
|
|
@@ -105,7 +102,7 @@ if (runtime.source === "process.execPath" && hasCodexDesktopApp()) {
|
|
|
105
102
|
console.log("runtime shipped inside Codex Desktop if that happens.");
|
|
106
103
|
}
|
|
107
104
|
console.log(`registered MCP server "${serverName}" with Codex (${applied.mode === "retarget" ? "existing entry retargeted in place; its other settings were kept" : "codex mcp add"}):`);
|
|
108
|
-
console.log(run(["mcp", "get", serverName]));
|
|
105
|
+
if (!process.argv.includes("--setup")) console.log(run(["mcp", "get", serverName]));
|
|
109
106
|
if (applied.notice) console.log(`NOTE: ${applied.notice}`);
|
|
110
107
|
else if (ensureEnvVars.length) console.log(`env_vars includes ${ensureEnvVars.join(", ")}; the temporary pipe path itself is never saved.`);
|
|
111
108
|
|
package/scripts/setup.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { printSetup, runSetup, setupFailureResult } from "../src/setup.mjs";
|
|
|
4
4
|
|
|
5
5
|
exitForVersionRequest(import.meta.url);
|
|
6
6
|
try {
|
|
7
|
-
const
|
|
7
|
+
const onProgress = process.argv.includes("--progress") ? (step) => process.stderr.write(`setup-progress: ${step}\n`) : undefined;
|
|
8
|
+
const result = runSetup({ args: process.argv.slice(2), onProgress });
|
|
8
9
|
printSetup(result, { json: process.argv.includes("--json") });
|
|
9
10
|
if (result.configured === false && !result.dryRun) process.exitCode = 1;
|
|
10
11
|
} catch (error) {
|
package/scripts/update.mjs
CHANGED
|
@@ -65,9 +65,18 @@ export function resolveUpdateNpm(env = process.env) {
|
|
|
65
65
|
throw failure("UPDATE_NPM_NOT_FOUND", "A supported npm CLI could not be located. Run this updater through npm exec.", "preflight");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
const SETUP_PROGRESS = /^setup-progress: ([^\u0000-\u001f\u007f]{1,120})$/;
|
|
69
|
+
|
|
70
|
+
function execute(command, args, { onStderrLine, ...options }) {
|
|
69
71
|
return new Promise(resolve => {
|
|
70
|
-
execFile(command, args, options, (error, stdout, stderr) => resolve({ status: error ? 1 : 0, error, stdout, stderr }));
|
|
72
|
+
const child = execFile(command, args, options, (error, stdout, stderr) => resolve({ status: error ? 1 : 0, error, stdout, stderr }));
|
|
73
|
+
if (!onStderrLine || !child.stderr) return;
|
|
74
|
+
let pending = "";
|
|
75
|
+
child.stderr.on("data", chunk => {
|
|
76
|
+
const lines = (pending + chunk).split(/\r?\n/);
|
|
77
|
+
pending = lines.pop();
|
|
78
|
+
for (const line of lines) onStderrLine(line);
|
|
79
|
+
});
|
|
71
80
|
});
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -141,11 +150,11 @@ export async function runUpdate({ args = [], env = process.env, platform = proce
|
|
|
141
150
|
const prefixArgs = userPrefix ? ["--prefix", userPrefix] : [];
|
|
142
151
|
let cwd = neutralDirectory([root], env);
|
|
143
152
|
let childEnv = Object.fromEntries(Object.entries(env).filter(([name]) => !/^AGENT_RELAY_(?!DIRECTORY$)/i.test(name)));
|
|
144
|
-
const command = async (arguments_, phase, timeout = 600000, environment = childEnv, setupCommand = false) => {
|
|
153
|
+
const command = async (arguments_, phase, timeout = 600000, environment = childEnv, setupCommand = false, onStderrLine) => {
|
|
145
154
|
let result;
|
|
146
155
|
try {
|
|
147
156
|
result = await run(process.execPath, arguments_, { env: environment, cwd, encoding: "utf8", windowsHide: true,
|
|
148
|
-
shell: false, timeout, maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
157
|
+
shell: false, timeout, maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], ...(onStderrLine ? { onStderrLine } : {}) });
|
|
149
158
|
} catch (error) { throw childFailure({ error, stdout: error.stdout, stderr: error.stderr }, phase, setupCommand); }
|
|
150
159
|
if (result?.error || (result?.status ?? result?.exitCode ?? 0) !== 0) throw childFailure(result, phase, setupCommand);
|
|
151
160
|
return String(result?.stdout ?? "");
|
|
@@ -203,15 +212,15 @@ export async function runUpdate({ args = [], env = process.env, platform = proce
|
|
|
203
212
|
await command([npm, "install", "--global", "--prefix", prefix, `${PACKAGE}@${version}`, "--ignore-scripts"],
|
|
204
213
|
"installation", 600000, { ...childEnv, CODEX_BRIDGE_SKIP_SETUP: "1" });
|
|
205
214
|
if (packageVersion(globalRoot) !== version) throw failure("UPDATE_VERSION_MISMATCH", "The global package version does not match the updater version. Rerun the same update command.", "verification");
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
onProgress("Applying and verifying managed settings from the installed bridge. Each step can take up to a minute while Codex CLI reads its configuration.");
|
|
216
|
+
const setupProgress = line => { const step = SETUP_PROGRESS.exec(line)?.[1]; if (step) onProgress(` ${step}`); };
|
|
217
|
+
let setup;
|
|
218
|
+
try { setup = JSON.parse(await command([path.join(globalRoot, "scripts", "setup.mjs"), "--json", "--progress"], "verification", 600000, childEnv, true, setupProgress)); }
|
|
219
|
+
catch (error) { if (error?.code) throw error; throw failure("UPDATE_SETUP_INCOMPLETE", `Global setup did not return a verifiable result. ${SETUP_INSPECTION} ${RETRY}`, "verification"); }
|
|
220
|
+
if (setup?.configured !== true) throw failure("UPDATE_SETUP_INCOMPLETE", `The global bridge settings are incomplete. ${setupFailureSummary(setup) ?? SETUP_INSPECTION} ${RETRY}`, "verification");
|
|
221
|
+
onProgress("Verifying the owned relay runtime.");
|
|
213
222
|
const service = await awaitService(globalRoot, false, "verification");
|
|
214
|
-
return { updated: true, configured: true, version, previousVersion, migrated, service, opensApps: false, opensChats: false };
|
|
223
|
+
return { updated: true, configured: true, version, previousVersion, migrated, service, desktopConnectionsVerified: false, opensApps: false, opensChats: false };
|
|
215
224
|
}
|
|
216
225
|
|
|
217
226
|
export async function runUpdateCli(args = process.argv.slice(2)) {
|
|
@@ -219,7 +228,7 @@ export async function runUpdateCli(args = process.argv.slice(2)) {
|
|
|
219
228
|
process.chdir(neutralDirectory([ROOT], process.env));
|
|
220
229
|
const result = await runUpdate({ args, onProgress: message => process.stderr.write(`${message}\n`) });
|
|
221
230
|
process.stdout.write(result.help ? `${result.help}\n` : args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n`
|
|
222
|
-
: `Bridge ${result.version} updated; managed settings verified. Background relay ${result.service.ready ? "ready" : "remains intentionally disabled"}.\n`);
|
|
231
|
+
: `Bridge ${result.version} updated; managed settings verified. Background relay ${result.service.ready ? "ready" : "remains intentionally disabled"}.\nDesktop connections have not been verified. Check codex_bridge_status, claude_bridge_status and antigravity_bridge_status in the existing apps.\n`);
|
|
223
232
|
} catch (error) {
|
|
224
233
|
const options = { component: "bridge-update", operation: "update", deliveryState: "not_applicable" };
|
|
225
234
|
if (args.includes("--json")) process.stdout.write(JSON.stringify({ updated: false, pending: error.pending === true, diagnostic: bridgeDiagnostic(error, options) }, null, 2) + "\n");
|
|
@@ -7,9 +7,11 @@ import { z } from "zod";
|
|
|
7
7
|
import { resolveCodexSenderContext } from "./codex-sender-context.mjs";
|
|
8
8
|
import { readClaudeSenderContext, requireClaudeSenderContext, stopProcessInspectors } from "./claude-sender-context.mjs";
|
|
9
9
|
import { readClaudeAccountContext } from "./desktop-account-context.mjs";
|
|
10
|
+
import { readCodexAccountContext } from "./codex-account-context.mjs";
|
|
10
11
|
import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
11
12
|
import { antigravityBridgeDirectory, callAntigravityDesktop } from "./antigravity-ipc.mjs";
|
|
12
13
|
import { AntigravityDeliveries, antigravityDeliveryError } from "./antigravity-deliveries.mjs";
|
|
14
|
+
import { observeAntigravityDelivery } from "./antigravity-desktop.mjs";
|
|
13
15
|
import { AntigravityCliSession } from "./antigravity-cli.mjs";
|
|
14
16
|
import { exitForVersionRequest } from "./cli-version.mjs";
|
|
15
17
|
import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
|
|
@@ -45,6 +47,7 @@ const reload = createReloadControl({
|
|
|
45
47
|
|
|
46
48
|
async function identifySender(extra) {
|
|
47
49
|
if (caller === "codex") {
|
|
50
|
+
const account = readCodexAccountContext();
|
|
48
51
|
const context = await resolveCodexSenderContext(extra?._meta, { inspectDesktopTask: async (threadId) => {
|
|
49
52
|
const accounts = requireBridgeAccounts(readBridgeAccounts());
|
|
50
53
|
const { result } = await senderRelay.requestDesktop("read_thread", { threadId, hostId: "local", turnLimit: 1, includeOutputs: false, maxOutputCharsPerItem: 1 }, { accountContext: accounts, beforeSend: () => assertAccountIdentity(accounts) });
|
|
@@ -52,11 +55,16 @@ async function identifySender(extra) {
|
|
|
52
55
|
return { thread: result?.thread, latestTurnId: result?.turns?.[0]?.id ?? null };
|
|
53
56
|
} });
|
|
54
57
|
if (context.status !== "verified") throw new Error(context.reason ?? "Unverified Codex sender");
|
|
55
|
-
|
|
58
|
+
const current = readCodexAccountContext();
|
|
59
|
+
if (account.status !== "verified" || current.status !== "verified" || !/^[a-f0-9]{64}$/.test(account.fingerprint ?? "")) throw new Error("The calling Codex account is not verified");
|
|
60
|
+
if (current.fingerprint !== account.fingerprint) throw new Error("The calling Codex account changed during sender verification");
|
|
61
|
+
return { provider: "codex", id: context.threadId, cwd: context.cwd, accountFingerprint: account.fingerprint };
|
|
56
62
|
}
|
|
57
63
|
if (caller === "claude") {
|
|
58
64
|
const context = requireClaudeSenderContext(await readClaudeSenderContext({ account: readClaudeAccountContext() }));
|
|
59
|
-
|
|
65
|
+
const current = readClaudeAccountContext();
|
|
66
|
+
if (current.status !== "verified" || !/^[a-f0-9]{64}$/.test(context.accountFingerprint ?? "") || current.fingerprint !== context.accountFingerprint) throw new Error("The calling Claude account changed during sender verification");
|
|
67
|
+
return { provider: "claude", id: context.sessionId, cwd: context.cwd, accountFingerprint: context.accountFingerprint };
|
|
60
68
|
}
|
|
61
69
|
throw new Error("Install this bridge for Codex or Claude; unknown callers cannot read conversations or send work");
|
|
62
70
|
}
|
|
@@ -89,10 +97,25 @@ function sameCwd(expected, actual) {
|
|
|
89
97
|
|
|
90
98
|
function owns(record, identity) {
|
|
91
99
|
if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw preflightFailure("ANTIGRAVITY_RECEIPT_OWNER_MISMATCH", "Delivery does not belong to this sending conversation");
|
|
100
|
+
if (Object.hasOwn(record.sender, "accountFingerprint") && record.sender.accountFingerprint !== identity.accountFingerprint) throw preflightFailure("ANTIGRAVITY_RECEIPT_ACCOUNT_MISMATCH", "Delivery does not belong to the currently signed-in sending account");
|
|
92
101
|
sameCwd(identity.cwd, record.cwd);
|
|
93
102
|
return record;
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
function sameSender(left, right) {
|
|
106
|
+
return left?.provider === right?.provider && left?.id === right?.id && left?.accountFingerprint === right?.accountFingerprint;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function accountUnboundReceipt(record) {
|
|
110
|
+
const recovered = record.status === "reply_received" || record.result?.status === "reply_received" || record.replyObservation?.status === "reply_received";
|
|
111
|
+
const status = recovered ? "unknown" : record.status;
|
|
112
|
+
const { response, reply, ...originalResult } = record.result ?? {};
|
|
113
|
+
return { ...record, status,
|
|
114
|
+
result: recovered ? { conversationId: record.conversationId, surface: "desktop", status, completed: false } : originalResult,
|
|
115
|
+
replyObservation: { status: "unavailable", code: "ANTIGRAVITY_RECEIPT_ACCOUNT_UNBOUND", source: "native-transcript", retrySafe: false,
|
|
116
|
+
reason: "The original delivery did not record its sending account. Recovered reply text is withheld; retain the original receipt and do not resend." } };
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
function handoff(prompt, identity) {
|
|
97
120
|
return `[Agent handoff from ${identity.provider} conversation ${identity.id}. Native Antigravity permissions remain in effect.]\n\n${prompt}`;
|
|
98
121
|
}
|
|
@@ -146,7 +169,7 @@ register("list_antigravity_conversations", "List native Desktop conversations an
|
|
|
146
169
|
const identity = await sender(extra);
|
|
147
170
|
const records = surface === "desktop" ? await callAntigravityDesktop({ operation: "list" }) : [...sessions.values()].map((entry) => ({ ...entry.session.read(), sender: entry.sender }));
|
|
148
171
|
return records.filter((record) => {
|
|
149
|
-
try { sameCwd(identity.cwd, record.cwd); return surface !== "cli" || record.sender
|
|
172
|
+
try { sameCwd(identity.cwd, record.cwd); return surface !== "cli" || sameSender(record.sender, identity); }
|
|
150
173
|
catch { return false; }
|
|
151
174
|
});
|
|
152
175
|
});
|
|
@@ -156,14 +179,26 @@ register("send_to_antigravity_conversation", "Send one authorized prompt to an e
|
|
|
156
179
|
const cwd = sameCwd(identity.cwd, args.cwd);
|
|
157
180
|
const prompt = handoff(args.prompt, identity);
|
|
158
181
|
if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw preflightFailure("ANTIGRAVITY_PROMPT_INVALID", "Desktop handoff must fit within 30000 characters and contain no NUL");
|
|
159
|
-
if (args.surface === "desktop")
|
|
182
|
+
if (args.surface === "desktop") {
|
|
183
|
+
const receipt = await callAntigravityDesktop({ operation: "send", ...args, cwd, prompt, sender: identity });
|
|
184
|
+
try {
|
|
185
|
+
const current = await sender(extra);
|
|
186
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed while inspecting the native delivery receipt");
|
|
187
|
+
owns(receipt, current);
|
|
188
|
+
return Object.hasOwn(receipt.sender, "accountFingerprint") ? receipt : accountUnboundReceipt(receipt);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
error.phase = "receipt";
|
|
191
|
+
error.deliveryState = "unknown";
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
160
195
|
const entry = sessions.get(args.conversationId);
|
|
161
|
-
if (!entry || entry.sender
|
|
196
|
+
if (!entry || !sameSender(entry.sender, identity)) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender and bridge process");
|
|
162
197
|
sameCwd(cwd, entry.session.read().cwd);
|
|
163
198
|
return deliveries.dispatch({ ...args, cwd, sender: identity, prompt }, async () => {
|
|
164
199
|
const current = await sender(extra);
|
|
165
200
|
sameCwd(cwd, current.cwd);
|
|
166
|
-
if (current
|
|
201
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
167
202
|
return entry.session.send(prompt);
|
|
168
203
|
}, { component: "antigravity-bridge", operation: "send_to_antigravity_conversation" });
|
|
169
204
|
});
|
|
@@ -188,7 +223,7 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
188
223
|
reserved = false;
|
|
189
224
|
const current = await sender(extra);
|
|
190
225
|
sameCwd(cwd, current.cwd);
|
|
191
|
-
if (current
|
|
226
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
192
227
|
sending = true;
|
|
193
228
|
return await session.send(handoff(args.prompt, identity));
|
|
194
229
|
} catch (error) {
|
|
@@ -208,16 +243,21 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
208
243
|
}, { component: "antigravity-bridge", operation: "start_antigravity_cli_session" });
|
|
209
244
|
});
|
|
210
245
|
|
|
211
|
-
register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means
|
|
246
|
+
register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means native acceptance; reply_received requires a final reply correlated to the original message and execution.", { surface: z.enum(["desktop", "cli"]), requestId: z.string().uuid() }, true, async (args, extra) => {
|
|
212
247
|
const identity = await sender(extra);
|
|
213
248
|
const record = args.surface === "desktop" ? await callAntigravityDesktop({ operation: "receipt", requestId: args.requestId }) : deliveries.read(args.requestId);
|
|
214
|
-
|
|
249
|
+
owns(record, identity);
|
|
250
|
+
if (args.surface !== "desktop") return record;
|
|
251
|
+
const observed = Object.hasOwn(record.sender, "accountFingerprint") ? await observeAntigravityDelivery(record) : accountUnboundReceipt(record);
|
|
252
|
+
const current = await sender(extra);
|
|
253
|
+
if (!sameSender(current, identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed while inspecting the original delivery");
|
|
254
|
+
return owns(observed, current);
|
|
215
255
|
});
|
|
216
256
|
|
|
217
257
|
register("close_antigravity_cli_session", "Close an idle bridge-owned CLI session belonging to the caller.", { conversationId: z.string().uuid() }, false, async ({ conversationId }, extra) => {
|
|
218
258
|
const identity = await sender(extra);
|
|
219
259
|
const entry = sessions.get(conversationId);
|
|
220
|
-
if (!entry || entry.sender
|
|
260
|
+
if (!entry || !sameSender(entry.sender, identity)) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender");
|
|
221
261
|
sameCwd(identity.cwd, entry.session.read().cwd);
|
|
222
262
|
if (["running", "starting"].includes(entry.session.read().status)) throw preflightFailure("ANTIGRAVITY_SESSION_BUSY", "Wait for the active CLI delivery before closing");
|
|
223
263
|
const closed = await entry.session.close();
|
|
@@ -64,6 +64,29 @@ export class AntigravityDeliveries {
|
|
|
64
64
|
this.write(record);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
captureObservation(requestId, observation) {
|
|
68
|
+
if (!this.active.has(requestId)) throw new Error("Cannot prepare inactive delivery observation");
|
|
69
|
+
if (observation?.version !== 1 || !["ready", "unavailable"].includes(observation.status)
|
|
70
|
+
|| !/^[a-f0-9]{64}$/.test(observation.promptSha256 ?? "") || typeof observation.projectId !== "string"
|
|
71
|
+
|| observation.status === "ready" && (!UUID.test(observation.trajectoryId ?? "") || !Number.isSafeInteger(observation.beforeStepIndex) || observation.beforeStepIndex < -1)) throw new Error("Invalid native observation boundary");
|
|
72
|
+
const record = this.read(requestId);
|
|
73
|
+
record.nativeObservation = { version: 1, status: observation.status, promptSha256: observation.promptSha256, projectId: observation.projectId,
|
|
74
|
+
...(observation.status === "ready" ? { trajectoryId: observation.trajectoryId, beforeStepIndex: observation.beforeStepIndex } : { code: "ANTIGRAVITY_TRANSCRIPT_UNAVAILABLE" }) };
|
|
75
|
+
this.write(record);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async refresh(requestId, observe) {
|
|
79
|
+
const record = this.read(requestId);
|
|
80
|
+
if (!record || this.active.has(requestId)) return record;
|
|
81
|
+
const refreshed = await observe(record);
|
|
82
|
+
if (!refreshed || refreshed.requestId !== record.requestId || refreshed.digest !== record.digest || refreshed.cwd !== record.cwd
|
|
83
|
+
|| refreshed.conversationId !== record.conversationId || refreshed.surface !== record.surface || JSON.stringify(refreshed.sender) !== JSON.stringify(record.sender)) throw new Error("Observation changed the delivery identity");
|
|
84
|
+
const current = this.read(requestId);
|
|
85
|
+
if (this.active.has(requestId) || JSON.stringify(current) !== JSON.stringify(record)) return current;
|
|
86
|
+
if (refreshed.status === "reply_received") this.write(refreshed);
|
|
87
|
+
return refreshed;
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
write(record) {
|
|
68
91
|
const temporary = `${this.file(record.requestId)}.${process.pid}.tmp`;
|
|
69
92
|
fs.writeFileSync(temporary, JSON.stringify(record), { flag: "wx", mode: 0o600 });
|
|
@@ -91,7 +114,7 @@ export class AntigravityDeliveries {
|
|
|
91
114
|
}
|
|
92
115
|
this.active.add(requestId);
|
|
93
116
|
void Promise.resolve().then(operation).then((result) => {
|
|
94
|
-
record.status = ["accepted", "sent_unconfirmed", "held", "refused"].includes(result?.status) ? result.status : result?.status && result.status !== "SUCCESS" ? "failed" : "completed";
|
|
117
|
+
record.status = ["accepted", "sent_unconfirmed", "held", "refused", "reply_received"].includes(result?.status) ? result.status : result?.status && result.status !== "SUCCESS" ? "failed" : "completed";
|
|
95
118
|
record.result = result;
|
|
96
119
|
record.conversationId = result?.conversationId ?? result?.conversation_id ?? record.conversationId;
|
|
97
120
|
if (record.status === "failed") {
|
|
@@ -107,7 +130,9 @@ export class AntigravityDeliveries {
|
|
|
107
130
|
}).finally(() => {
|
|
108
131
|
record.finishedAt = new Date().toISOString();
|
|
109
132
|
try {
|
|
110
|
-
|
|
133
|
+
const previous = this.read(requestId);
|
|
134
|
+
record.conversationId ??= previous?.conversationId;
|
|
135
|
+
record.nativeObservation ??= previous?.nativeObservation;
|
|
111
136
|
this.write(record);
|
|
112
137
|
} catch (error) { process.stderr.write(`Could not persist Antigravity delivery outcome: ${error.code ?? "WRITE_FAILED"}\n`); }
|
|
113
138
|
this.active.delete(requestId);
|