@minhspark/codex-mcp-bridge 1.26.10 → 1.27.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-mcp-bridge",
3
- "version": "1.26.10",
3
+ "version": "1.27.0",
4
4
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
5
5
  "author": {
6
6
  "name": "Bui Dang Minh"
package/CHANGELOG.md CHANGED
@@ -2,7 +2,16 @@
2
2
 
3
3
  Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
4
4
 
5
- ## [Unreleased]
5
+ ## [1.27.0] - 2026-11-05
6
+
7
+ ### Added
8
+
9
+ - A versioned local Antigravity human-dashboard channel with exact conversation/workspace validation, fixed human sender identity, durable idempotent request receipts and explicit preflight/uncertain delivery outcomes. It requires the app-launched sidecar and never treats native acceptance as a completed reply.
10
+
11
+ ### Verification and limits
12
+
13
+ - Unit tests cover preflight, idempotent receipts and refusal paths on Windows, macOS and Linux with Node 22 and 24. Delivery through a live Antigravity sidecar was not exercised before this release.
14
+ - Upgrade with `npm install -g @minhspark/codex-mcp-bridge@latest --allow-scripts=@minhspark/codex-mcp-bridge --foreground-scripts`, then restart Antigravity so its sidecar loads the new channel.
6
15
 
7
16
  ## [1.26.10] - 2026-11-03
8
17
 
@@ -154,3 +154,7 @@ is still null. Continue with `wait_codex_reply`; do not create again to obtain t
154
154
  ID. The updated companion retains late creation acknowledgements. If it reports
155
155
  unsupported `get_creation_receipt`, reload both bridge workers after installing
156
156
  the update; that compatibility check occurs before any new task is dispatched.
157
+
158
+ ## Local human dashboards
159
+
160
+ Local human dashboards can use `src/antigravity-human.mjs`: `inspectHumanConversation({ conversationId, cwd })`, `sendHumanMessage({ requestId, conversationId, cwd, prompt })`, and `readHumanReceipt({ requestId, conversationId, cwd })`. The native app must have launched an updated sidecar advertising `humanDispatchVersion: 1`. Requests use an existing desktop conversation UUID, its canonical workspace and a stable request UUID. The sender is fixed to `human-web` / `minh-spark-company`; caller-supplied agent identities are rejected. Reusing a request ID never resends it, and changed request contents are refused. Poll the receipt after an uncertain outcome. Native acceptance does not prove a reply or completion. This channel does not answer permission prompts or create conversations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.26.10",
3
+ "version": "1.27.0",
4
4
  "description": "Connect Claude, Codex, and Antigravity through MCP, with native Desktop handoffs and bridge-owned Antigravity CLI sessions.",
5
5
  "keywords": [
6
6
  "mcp",
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.10",
4
+ "version": "1.27.0",
5
5
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
6
6
  "author": {
7
7
  "name": "Bui Dang Minh",
@@ -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.10";
28
+ const VERSION = "1.27.0";
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"];
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { callAntigravityDesktop } from "./antigravity-ipc.mjs";
4
+
5
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
+ export const HUMAN_SENDER = Object.freeze({ provider: "human-web", id: "minh-spark-company" });
7
+
8
+ function invalid(message, code = "ANTIGRAVITY_HUMAN_REQUEST_INVALID") {
9
+ return Object.assign(new Error(message), { code, phase: "validation", deliveryState: "not_sent" });
10
+ }
11
+
12
+ export function humanWorkspace(value) {
13
+ if (typeof value !== "string" || !path.isAbsolute(value)) throw invalid("An absolute existing workspace is required");
14
+ try {
15
+ const resolved = fs.realpathSync.native(value);
16
+ if (!fs.statSync(resolved).isDirectory()) throw new Error("not a directory");
17
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
18
+ } catch { throw invalid("The requested workspace is unavailable"); }
19
+ }
20
+
21
+ export function humanDeliveryInput(input, operation) {
22
+ if (!input || typeof input !== "object" || !UUID.test(input.conversationId) || input.sender !== undefined) throw invalid("An exact conversation UUID and a fixed human sender are required");
23
+ const out = { conversationId: input.conversationId.toLowerCase(), cwd: humanWorkspace(input.cwd) };
24
+ if (operation !== "inspect") {
25
+ if (typeof input.requestId !== "string" || !UUID.test(input.requestId)) throw invalid("requestId must be a UUID");
26
+ out.requestId = input.requestId.toLowerCase();
27
+ }
28
+ if (operation === "send") {
29
+ if (typeof input.prompt !== "string" || !input.prompt.trim() || input.prompt.length > 30000 || input.prompt.includes("\0")) throw invalid("Prompt must contain between 1 and 30000 characters without NUL");
30
+ out.prompt = input.prompt;
31
+ }
32
+ return out;
33
+ }
34
+
35
+ export function humanReceipt(record, target) {
36
+ if (record === null) return null;
37
+ if (!record || record.requestId !== target.requestId || record.conversationId !== target.conversationId || record.surface !== "desktop" || record.sender?.provider !== HUMAN_SENDER.provider || record.sender?.id !== HUMAN_SENDER.id || humanWorkspace(record.cwd) !== target.cwd) throw invalid("Receipt does not belong to this human delivery", "ANTIGRAVITY_HUMAN_RECEIPT_MISMATCH");
38
+ return record;
39
+ }
40
+
41
+ async function callHuman(operation, input, options = {}) {
42
+ const target = humanDeliveryInput(input, operation);
43
+ const call = options.call ?? ((request) => callAntigravityDesktop(request, { directory: options.directory, timeoutMs: options.timeoutMs ?? 10000 }));
44
+ let status;
45
+ try { status = await call({ operation: "status" }); }
46
+ catch (error) {
47
+ if (operation === "send") {
48
+ error.deliveryState = "not_sent";
49
+ error.phase = "preflight";
50
+ }
51
+ throw error;
52
+ }
53
+ if (status?.available !== true || status.humanDispatchVersion !== 1) throw invalid("The app-launched Antigravity sidecar must be updated to support human dashboard messages", "ANTIGRAVITY_HUMAN_UNSUPPORTED");
54
+ const result = await call({ operation: `human-${operation}`, ...target });
55
+ if (operation !== "inspect") {
56
+ try {
57
+ if (operation === "send" && result === null) throw invalid("No delivery receipt returned");
58
+ return humanReceipt(result, target);
59
+ } catch (error) {
60
+ if (operation === "send") { error.deliveryState = "unknown"; error.phase = "response"; }
61
+ throw error;
62
+ }
63
+ }
64
+ if (!result || result.conversationId !== target.conversationId || humanWorkspace(result.cwd) !== target.cwd || result.surface !== "desktop" || result.available !== true || result.status !== "CASCADE_RUN_STATUS_IDLE") throw invalid("Native conversation does not match the requested idle workspace", "ANTIGRAVITY_HUMAN_TARGET_MISMATCH");
65
+ return result;
66
+ }
67
+
68
+ export function inspectHumanConversation(input, options) { return callHuman("inspect", input, options); }
69
+ export function sendHumanMessage(input, options) { return callHuman("send", input, options); }
70
+ export function readHumanReceipt(input, options) { return callHuman("receipt", input, options); }
@@ -6,6 +6,7 @@ import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
6
6
  import { listAntigravityConversations, observeAntigravityDelivery, runAgentApi, sanitizeAntigravityMetadata } from "./antigravity-desktop.mjs";
7
7
  import { exitForVersionRequest } from "./cli-version.mjs";
8
8
  import { readProcessAncestry } from "./claude-sender-context.mjs";
9
+ import { HUMAN_SENDER, humanDeliveryInput, humanReceipt, humanWorkspace } from "./antigravity-human.mjs";
9
10
 
10
11
  export async function createAntigravityParentGuard({ parentPid = process.ppid, inspect = readProcessAncestry } = {}) {
11
12
  if (!Number.isSafeInteger(parentPid) || parentPid <= 1 || parentPid > 0xffffffff) throw new Error("Antigravity sidecar parent is unavailable; relaunch it from the native app");
@@ -34,7 +35,7 @@ export async function createAntigravityParentGuard({ parentPid = process.ppid, i
34
35
  };
35
36
  }
36
37
 
37
- export async function startAntigravitySidecar({ directory = antigravityBridgeDirectory(), env = process.env, createParentGuard = createAntigravityParentGuard, serve = serveAntigravityDesktop, parentCheckIntervalMs = 5000, onParentLost = () => {}, runNative = runAgentApi, observeDelivery = observeAntigravityDelivery } = {}) {
38
+ export async function startAntigravitySidecar({ directory = antigravityBridgeDirectory(), env = process.env, createParentGuard = createAntigravityParentGuard, serve = serveAntigravityDesktop, parentCheckIntervalMs = 5000, onParentLost = () => {}, list = listAntigravityConversations, runNative = runAgentApi, observeDelivery = observeAntigravityDelivery } = {}) {
38
39
  if (!env.ANTIGRAVITY_AGENTAPI_EXE || !env.ANTIGRAVITY_LS_ADDRESS) throw new Error("Antigravity must launch this sidecar. Do not run it as a replacement app server.");
39
40
  const assertParent = await createParentGuard();
40
41
  const deliveries = new AntigravityDeliveries(path.join(directory, "desktop-deliveries"));
@@ -59,7 +60,33 @@ export async function startAntigravitySidecar({ directory = antigravityBridgeDir
59
60
  sidecar = await serve(async (request) => {
60
61
  await checkParent();
61
62
  if (!request || typeof request !== "object") throw new Error("Invalid sidecar request");
62
- if (request.operation === "status") return { available: true, transport: "native-agentapi", permissionPolicy: "native-unchanged" };
63
+ if (request.operation === "status") return { available: true, transport: "native-agentapi", permissionPolicy: "native-unchanged", humanDispatchVersion: 1 };
64
+ if (["human-inspect", "human-send", "human-receipt"].includes(request.operation)) {
65
+ const operation = request.operation.slice("human-".length);
66
+ const target = humanDeliveryInput(request, operation);
67
+ if (operation === "receipt") return humanReceipt(deliveries.read(target.requestId), target);
68
+ const inspect = async () => {
69
+ await checkParent();
70
+ const records = (await list({ surface: "desktop", limit: 1000 })).filter((record) => record.conversationId === target.conversationId);
71
+ if (records.length !== 1 || records[0].surface !== "desktop" || humanWorkspace(records[0].cwd) !== target.cwd) throw Object.assign(new Error("Exact Antigravity conversation and workspace are unavailable"), { code: "ANTIGRAVITY_RECIPIENT_MISSING", deliveryState: "not_sent" });
72
+ const record = records[0];
73
+ if (record.status !== "CASCADE_RUN_STATUS_IDLE") throw Object.assign(new Error("Antigravity conversation is busy; no message was sent"), { code: "ANTIGRAVITY_RECIPIENT_BUSY", deliveryState: "not_sent" });
74
+ const metadata = sanitizeAntigravityMetadata(await runNative({ command: "metadata", conversationId: target.conversationId, env }), target.conversationId);
75
+ if (humanWorkspace(metadata.cwd) !== target.cwd || metadata.projectId !== record.projectId) throw Object.assign(new Error("Native Antigravity workspace or project changed"), { code: "ANTIGRAVITY_METADATA_MISMATCH", deliveryState: "not_sent" });
76
+ await checkParent();
77
+ return { ...record, ...metadata, available: true };
78
+ };
79
+ if (operation === "inspect") return inspect();
80
+ return deliveries.dispatch({ ...target, surface: "desktop", sender: HUMAN_SENDER }, async () => {
81
+ await checkParent();
82
+ if (sending.has(target.conversationId)) throw Object.assign(new Error("Another delivery is pending for this conversation"), { deliveryState: "not_sent" });
83
+ sending.add(target.conversationId);
84
+ try {
85
+ await inspect();
86
+ return await runNative({ command: "send", conversationId: target.conversationId, expectedCwd: target.cwd, prompt: target.prompt, env });
87
+ } finally { sending.delete(target.conversationId); }
88
+ });
89
+ }
63
90
  if (request.operation === "list") return listAntigravityConversations({ surface: "desktop" });
64
91
  if (request.operation === "read") {
65
92
  const records = await listAntigravityConversations({ surface: "desktop" });
@@ -31,7 +31,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
31
31
 
32
32
  exitForVersionRequest(import.meta.url);
33
33
 
34
- const VERSION = "1.26.10";
34
+ const VERSION = "1.27.0";
35
35
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
36
36
  const FORWARD_MIN_INTERVAL_MS = 5000;
37
37
  const FORWARD_MAX_PER_SESSION = 50;
package/src/index.mjs CHANGED
@@ -44,7 +44,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
44
44
 
45
45
  exitForVersionRequest(import.meta.url);
46
46
 
47
- const VERSION = "1.26.10";
47
+ const VERSION = "1.27.0";
48
48
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
49
49
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
50
50
 
@@ -43,7 +43,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
43
43
 
44
44
  exitForVersionRequest(import.meta.url);
45
45
 
46
- const VERSION = "1.26.10";
46
+ const VERSION = "1.27.0";
47
47
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
48
48
 
49
49
  function errorResponse(code, message, sent, details) {