@minhspark/codex-mcp-bridge 1.23.0 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ const execute = promisify(execFile);
9
+ const SURFACES = { desktop: "antigravity", cli: "antigravity-cli" };
10
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
11
+
12
+ function failure(code, message) {
13
+ return Object.assign(new Error(message), { code, deliveryState: "not_sent" });
14
+ }
15
+
16
+ function nativeDiagnostic(error, env, args) {
17
+ const exitCode = Number.isInteger(error.code) ? String(error.code) : /^[A-Z_]{2,40}$/.test(error.code ?? "") ? error.code : "unknown";
18
+ let detail = "";
19
+ try {
20
+ const parsed = JSON.parse(error.stdout);
21
+ if (typeof parsed.error === "string") detail = parsed.error;
22
+ } catch {}
23
+ for (const value of [...Object.entries(env).filter(([key]) => /ANTIGRAVITY|TOKEN|SECRET|PASSWORD|AUTH|KEY/i.test(key)).map(([, value]) => value), ...args.slice(1)]) {
24
+ if (typeof value === "string" && value) detail = detail.split(value).join("[redacted]");
25
+ }
26
+ detail = detail.replace(/https?:\/\/[^\s"']+/gi, "[endpoint]").replace(/Bearer\s+\S+/gi, "Bearer [redacted]").replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 400);
27
+ return `exit ${exitCode}${detail ? `: ${detail}` : ""}`;
28
+ }
29
+
30
+ function canonicalCwd(value) {
31
+ if (typeof value !== "string" || !path.isAbsolute(value)) throw failure("ANTIGRAVITY_CWD_INVALID", "An absolute existing workspace directory is required");
32
+ const resolved = fs.realpathSync.native(value);
33
+ if (!fs.statSync(resolved).isDirectory()) throw failure("ANTIGRAVITY_CWD_INVALID", "Workspace is not a directory");
34
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
35
+ }
36
+
37
+ function workspaceCwd(uris) {
38
+ if (typeof uris === "string") uris = JSON.parse(uris);
39
+ if (!Array.isArray(uris) || uris.length !== 1 || typeof uris[0] !== "string") throw failure("ANTIGRAVITY_WORKSPACE_AMBIGUOUS", "Conversation must have exactly one workspace");
40
+ const uri = new URL(uris[0]);
41
+ if (uri.protocol !== "file:" || uri.search || uri.hash) throw failure("ANTIGRAVITY_CWD_INVALID", "Conversation workspace must be a local file URI");
42
+ return canonicalCwd(fileURLToPath(uri));
43
+ }
44
+
45
+ export async function listAntigravityConversations({ home = os.homedir(), surface = "all", limit = 100 } = {}) {
46
+ if (surface !== "all" && !Object.hasOwn(SURFACES, surface)) throw failure("ANTIGRAVITY_SURFACE_INVALID", "Surface must be desktop, cli, or all");
47
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error("Conversation limit must be between 1 and 1000");
48
+ const { DatabaseSync } = await import("node:sqlite");
49
+ const rows = [];
50
+ for (const [kind, directory] of Object.entries(SURFACES)) {
51
+ if (surface !== "all" && surface !== kind) continue;
52
+ const filename = path.join(home, ".gemini", directory, "conversation_summaries.db");
53
+ if (!fs.existsSync(filename)) continue;
54
+ const db = new DatabaseSync(filename, { readOnly: true });
55
+ try {
56
+ const records = db.prepare("SELECT conversation_id, title, workspace_uris, project_id, status, not_fully_idle, killed, app_data_dir, source FROM conversation_summaries WHERE app_data_dir = ? ORDER BY last_modified_time DESC LIMIT ?").all(directory, limit);
57
+ for (const row of records) {
58
+ if (!UUID.test(row.conversation_id) || row.killed) continue;
59
+ if (kind === "desktop" && ["CORTEX_TRAJECTORY_SOURCE_CLI", "CORTEX_TRAJECTORY_SOURCE_VSCODE", "CORTEX_TRAJECTORY_SOURCE_VISUAL_STUDIO", "CORTEX_TRAJECTORY_SOURCE_SDK", "CORTEX_TRAJECTORY_SOURCE_PYTHON_SDK"].includes(row.source)) continue;
60
+ let cwd;
61
+ try { cwd = workspaceCwd(row.workspace_uris); } catch { continue; }
62
+ rows.push({ conversationId: row.conversation_id, cwd, projectId: row.project_id, title: row.title, status: row.not_fully_idle ? "busy" : row.status || "unknown", surface: kind });
63
+ }
64
+ } finally { db.close(); }
65
+ }
66
+ return rows;
67
+ }
68
+
69
+ function nativeMetadata(result) {
70
+ return result.metadata ?? result.response?.conversationMetadata?.metadata ?? result.response?.conversation_metadata?.metadata ?? result.payload?.conversationMetadata?.metadata ?? result.payload?.conversation_metadata?.metadata ?? result.conversationMetadata?.metadata ?? result.conversation_metadata?.metadata;
71
+ }
72
+
73
+ function metadataShape(result) {
74
+ const rows = [];
75
+ const visit = (value, prefix, depth) => {
76
+ if (!value || typeof value !== "object" || Array.isArray(value) || depth > 3) return;
77
+ for (const key of Object.keys(value).slice(0, 20)) {
78
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/.test(key)) continue;
79
+ const field = prefix ? `${prefix}.${key}` : key;
80
+ rows.push(field);
81
+ visit(value[key], field, depth + 1);
82
+ }
83
+ };
84
+ visit(result, "", 0);
85
+ return rows.join(", ").slice(0, 800);
86
+ }
87
+
88
+ export function sanitizeAntigravityMetadata(result, conversationId) {
89
+ const metadata = nativeMetadata(result);
90
+ if (!metadata || typeof metadata !== "object") throw failure("ANTIGRAVITY_METADATA_UNSUPPORTED", `Native Antigravity metadata is missing or unsupported (fields: ${metadataShape(result)})`);
91
+ const returnedId = result.conversationId ?? result.conversation_id ?? result.response?.conversationId ?? result.response?.conversation_id ?? metadata.conversationId ?? metadata.conversation_id;
92
+ if (returnedId !== undefined && returnedId !== conversationId) throw failure("ANTIGRAVITY_METADATA_MISMATCH", "Native metadata identifies a different conversation");
93
+ return {
94
+ conversationId,
95
+ cwd: workspaceCwd(metadata.workspaceUris ?? metadata.workspace_uris),
96
+ projectId: metadata.projectId ?? metadata.project_id ?? "",
97
+ surface: "desktop",
98
+ };
99
+ }
100
+
101
+ export async function runAgentApi(options) {
102
+ try { return await executeAgentApi(options); } catch (error) {
103
+ error.deliveryState ??= "not_sent";
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ async function executeAgentApi({ command, conversationId, prompt, expectedCwd, home = os.homedir(), env = process.env, executable = env.ANTIGRAVITY_AGENTAPI_EXE, run = execute, list = listAntigravityConversations }) {
109
+ if (!UUID.test(conversationId)) throw failure("ANTIGRAVITY_ID_INVALID", "An exact conversation UUID is required");
110
+ if (!["metadata", "send"].includes(command)) throw new Error("Unsupported Antigravity agentapi command");
111
+ if (!env.ANTIGRAVITY_LS_ADDRESS || !env.ANTIGRAVITY_CSRF_TOKEN) throw failure("ANTIGRAVITY_SIDECAR_REQUIRED", "Antigravity must launch the bridge sidecar before native requests are available");
112
+ if (executable && (!path.isAbsolute(executable) || /\.(bat|cmd|ps1)$/i.test(executable))) throw failure("ANTIGRAVITY_EXECUTABLE_INVALID", "Antigravity agentapi executable must be an absolute native executable path");
113
+ if (!executable && process.platform === "win32") throw failure("ANTIGRAVITY_EXECUTABLE_MISSING", "The Antigravity sidecar must provide ANTIGRAVITY_AGENTAPI_EXE on Windows");
114
+ const invoke = async (args) => {
115
+ let output;
116
+ try {
117
+ output = await run(executable || "agentapi", executable ? ["agentapi", ...args] : args, { env, encoding: "utf8", timeout: 30000, maxBuffer: 1048576, windowsHide: true, shell: false });
118
+ } catch (error) {
119
+ const nativeError = failure(error.killed || error.code === "ETIMEDOUT" ? "ANTIGRAVITY_OUTCOME_UNKNOWN" : "ANTIGRAVITY_NATIVE_FAILED", `Native Antigravity request failed (${nativeDiagnostic(error, env, args)}); inspect the original conversation before resending`);
120
+ nativeError.deliveryState = args[0] === "send-message" ? "unknown" : "not_sent";
121
+ throw nativeError;
122
+ }
123
+ let result;
124
+ try { result = JSON.parse(output.stdout); } catch { throw Object.assign(failure("ANTIGRAVITY_RESPONSE_INVALID", "Native Antigravity returned invalid JSON; do not automatically resend"), { deliveryState: args[0] === "send-message" ? "unknown" : "not_sent" }); }
125
+ if (!result || typeof result !== "object" || Array.isArray(result) || result.error || result.success === false) throw Object.assign(failure("ANTIGRAVITY_NATIVE_REJECTED", "Native Antigravity rejected the request"), { deliveryState: args[0] === "send-message" ? "unknown" : "not_sent" });
126
+ return result;
127
+ };
128
+ if (command === "metadata") return invoke(["get-conversation-metadata", conversationId]);
129
+ if (typeof prompt !== "string" || !prompt.trim() || prompt.length > 32768 || prompt.includes("\0")) throw new Error("Prompt must contain between 1 and 32768 characters without NUL");
130
+ const cwd = canonicalCwd(expectedCwd);
131
+ const select = async () => {
132
+ const records = (await list({ home, surface: "desktop", limit: 1000 })).filter((row) => row.conversationId === conversationId);
133
+ if (records.length !== 1) throw failure("ANTIGRAVITY_RECIPIENT_MISSING", "Exact desktop conversation is unavailable or has an ambiguous workspace");
134
+ const row = records[0];
135
+ if (row.surface !== "desktop" || canonicalCwd(row.cwd) !== cwd) throw failure("ANTIGRAVITY_WORKSPACE_MISMATCH", "Conversation workspace does not match the authorized workspace");
136
+ if (row.status !== "CASCADE_RUN_STATUS_IDLE") throw failure("ANTIGRAVITY_RECIPIENT_BUSY", "Conversation is busy or its idle state cannot be verified");
137
+ return row;
138
+ };
139
+ const before = await select();
140
+ const metadata = sanitizeAntigravityMetadata(await invoke(["get-conversation-metadata", conversationId]), conversationId);
141
+ if (metadata.cwd !== cwd || metadata.projectId !== before.projectId) throw failure("ANTIGRAVITY_METADATA_MISMATCH", "Native metadata does not match the selected conversation workspace and project");
142
+ const current = await select();
143
+ if (current.projectId !== before.projectId) throw failure("ANTIGRAVITY_METADATA_MISMATCH", "Conversation project changed during delivery preflight");
144
+ await invoke(["send-message", conversationId, prompt]);
145
+ return { conversationId, status: "accepted", completed: false, surface: "desktop" };
146
+ }
@@ -0,0 +1,141 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import net from "node:net";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
7
+ import { relayFileIdentity, removeRelayFile } from "./native-relay-files.mjs";
8
+
9
+ const LIMIT = 2 * 1024 * 1024;
10
+
11
+ async function socketRefused(socketPath) {
12
+ return new Promise((resolve) => {
13
+ const socket = net.createConnection(socketPath);
14
+ let settled = false;
15
+ const done = (refused) => {
16
+ if (settled) return;
17
+ settled = true;
18
+ clearTimeout(timer);
19
+ socket.destroy();
20
+ resolve(refused);
21
+ };
22
+ const timer = setTimeout(() => done(false), 1000);
23
+ socket.on("connect", () => done(false));
24
+ socket.on("error", (error) => done(error.code === "ECONNREFUSED"));
25
+ });
26
+ }
27
+
28
+ export function antigravityBridgeDirectory(home = os.homedir()) {
29
+ return path.join(home, ".codex", "antigravity-bridge");
30
+ }
31
+
32
+ export function antigravitySocket(directory = antigravityBridgeDirectory()) {
33
+ const hash = crypto.createHash("sha256").update(path.resolve(directory).toLowerCase()).digest("hex").slice(0, 24);
34
+ return process.platform === "win32" ? `\\\\.\\pipe\\LOCAL\\codex-bridge-antigravity-${hash}` : path.join(directory, "desktop.sock");
35
+ }
36
+
37
+ export function ensureBridgeDirectory(directory) {
38
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
39
+ const stat = fs.lstatSync(directory);
40
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Bridge directory must be a real directory");
41
+ if (typeof process.getuid === "function" && (stat.uid !== process.getuid() || (stat.mode & 0o077))) throw new Error("Bridge directory must be owned by this user with mode 0700");
42
+ return directory;
43
+ }
44
+
45
+ export function callAntigravityDesktop(request, { directory = antigravityBridgeDirectory(), timeoutMs = 30000 } = {}) {
46
+ return new Promise((resolve, reject) => {
47
+ const socket = net.createConnection(antigravitySocket(directory));
48
+ let buffer = Buffer.alloc(0);
49
+ let finished = false;
50
+ let dispatched = false;
51
+ const done = (error, result) => {
52
+ if (finished) return;
53
+ finished = true;
54
+ clearTimeout(timer);
55
+ socket.destroy();
56
+ if (error) {
57
+ error.deliveryState ??= dispatched ? "unknown" : "not_sent";
58
+ reject(error);
59
+ } else resolve(result);
60
+ };
61
+ const timer = setTimeout(() => done(Object.assign(new Error("Antigravity sidecar did not reply; inspect the original requestId before retrying"), { deliveryState: "unknown" })), timeoutMs);
62
+ socket.on("error", (error) => done(error));
63
+ socket.on("end", () => done(new Error("Antigravity sidecar disconnected before replying")));
64
+ socket.on("connect", () => {
65
+ const frame = JSON.stringify(request) + "\n";
66
+ if (Buffer.byteLength(frame) > LIMIT) return done(new Error("Request exceeds size limit"));
67
+ dispatched = true;
68
+ socket.write(frame);
69
+ });
70
+ socket.on("data", (chunk) => {
71
+ buffer = Buffer.concat([buffer, chunk]);
72
+ if (buffer.length > LIMIT) return done(new Error("Sidecar response exceeds size limit"));
73
+ const end = buffer.indexOf(10);
74
+ if (end < 0) return;
75
+ try {
76
+ const response = JSON.parse(buffer.subarray(0, end).toString("utf8"));
77
+ if (response.error) return done(Object.assign(new Error(response.error), { deliveryState: response.deliveryState }));
78
+ done(null, response.result);
79
+ } catch (error) { done(error); }
80
+ });
81
+ });
82
+ }
83
+
84
+ export async function serveAntigravityDesktop(handler, { directory = antigravityBridgeDirectory() } = {}) {
85
+ ensureBridgeDirectory(directory);
86
+ const socketPath = antigravitySocket(directory);
87
+ let ready = false;
88
+ const connections = new Set();
89
+ const server = net.createServer((socket) => {
90
+ if (!ready || connections.size >= 32) return socket.destroy();
91
+ connections.add(socket);
92
+ socket.on("close", () => connections.delete(socket));
93
+ socket.on("error", () => {});
94
+ socket.setTimeout(35000, () => socket.destroy());
95
+ let buffer = Buffer.alloc(0);
96
+ let received = false;
97
+ socket.on("data", async (chunk) => {
98
+ if (received) return;
99
+ buffer = Buffer.concat([buffer, chunk]);
100
+ if (buffer.length > LIMIT) return socket.destroy();
101
+ const end = buffer.indexOf(10);
102
+ if (end < 0) return;
103
+ received = true;
104
+ try {
105
+ const result = await handler(JSON.parse(buffer.subarray(0, end).toString("utf8")));
106
+ const frame = JSON.stringify({ result }) + "\n";
107
+ if (Buffer.byteLength(frame) > LIMIT) throw new Error("Sidecar response exceeds size limit");
108
+ socket.end(frame);
109
+ } catch (error) { socket.end(JSON.stringify({ error: error.message, deliveryState: error.deliveryState }) + "\n"); }
110
+ });
111
+ });
112
+ const privatePath = process.platform === "win32" ? socketPath : path.join(directory, `.${crypto.randomBytes(6).toString("hex")}.sock`);
113
+ let identity;
114
+ await new Promise((resolve, reject) => {
115
+ server.once("error", reject);
116
+ server.listen(privatePath, () => { server.off("error", reject); resolve(); });
117
+ });
118
+ try {
119
+ if (process.platform === "win32") await protectCurrentUserPipe(socketPath);
120
+ else {
121
+ fs.chmodSync(privatePath, 0o600);
122
+ try { fs.linkSync(privatePath, socketPath); }
123
+ catch (error) {
124
+ if (error.code !== "EEXIST") throw error;
125
+ const existing = relayFileIdentity(socketPath, "socket");
126
+ if (!await socketRefused(socketPath)) throw new Error("Another Antigravity sidecar owns the socket");
127
+ removeRelayFile(socketPath, { expected: existing });
128
+ fs.linkSync(privatePath, socketPath);
129
+ }
130
+ fs.unlinkSync(privatePath);
131
+ identity = relayFileIdentity(socketPath, "socket");
132
+ }
133
+ ready = true;
134
+ } catch (error) { server.close(); throw error; }
135
+ return { server, close: () => new Promise((resolve) => {
136
+ ready = false;
137
+ for (const socket of connections) socket.destroy();
138
+ if (identity) removeRelayFile(socketPath, { expected: identity });
139
+ server.close(resolve);
140
+ }) };
141
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { resolveAntigravitySenderContext } from "./antigravity-sender-context.mjs";
9
+ import { createAntigravityOutboundTargets } from "./antigravity-outbound-targets.mjs";
10
+ import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
11
+ import { antigravityBridgeDirectory } from "./antigravity-ipc.mjs";
12
+ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
13
+ import { stopProcessInspectors } from "./claude-sender-context.mjs";
14
+ import { exitForVersionRequest } from "./cli-version.mjs";
15
+ import { assertAccountIdentity } from "./bridge-account-context.mjs";
16
+
17
+ const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
18
+ const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
19
+ const targetSchema = { target: z.enum(["codex", "claude"]) };
20
+ const taskSchema = { ...targetSchema, conversationId: z.string().uuid(), taskId: z.string().min(1).max(256).optional() };
21
+
22
+ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigravitySenderContext, targets = createAntigravityOutboundTargets(), deliveries = new AntigravityDeliveries(path.join(antigravityBridgeDirectory(), "outbound-deliveries")), rootPolicy = createHardenedRootPolicy() } = {}) {
23
+ const server = new McpServer({ name: "agent-bridge", version }, { instructions: "Send user-authorized work FROM the current Antigravity Desktop conversation to an exact existing Codex Desktop task or Claude Desktop Code session in the same project. Use list_agent_tasks first, verify title and identity, then send_to_agent_task with a fresh requestId. Keep requestId after uncertainty and read_agent_delivery; never automatically resend. Native permissions and sender identity are verified on every call. Do not launch a CLI substitute, impersonate Codex/Claude, or change permissions to overcome a rejection. Native acceptance is not task completion; read_agent_task for Codex or read_agent_delivery for a Claude reply. Independent new work requires a separate native conversation; this bridge only continues existing tasks. Agent-authored handoffs must be in English and name Antigravity as the author." });
24
+
25
+ function scope(expected, actual) {
26
+ if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("An absolute project directory is required");
27
+ const canonical = fs.realpathSync.native(expected);
28
+ if (path.relative(canonical, fs.realpathSync.native(actual))) throw new Error("Destination must match the verified Antigravity sender's project");
29
+ rootPolicy.assert(canonical, "Antigravity sender project");
30
+ return canonical;
31
+ }
32
+
33
+ async function sender(extra) {
34
+ const identity = await resolveSender(extra?._meta);
35
+ if (identity.status !== "verified" || identity.provider !== "antigravity" || !identity.id || !["bypass", "prompting"].includes(identity.mode)) throw new Error(identity.reason ?? "Antigravity sender is not verified");
36
+ scope(identity.cwd, identity.cwd);
37
+ return identity;
38
+ }
39
+
40
+ function recheck(identity, extra) {
41
+ return async () => {
42
+ const current = await sender(extra);
43
+ if (JSON.stringify(current) !== JSON.stringify(identity)) throw Object.assign(new Error("Antigravity sender identity or permissions changed before delivery"), { deliveryState: "not_sent" });
44
+ };
45
+ }
46
+
47
+ function register(name, description, inputSchema, readOnly, handler) {
48
+ server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
49
+ try { return result(await handler(args, extra)); }
50
+ catch (error) { return { ...result({ error: error.message, deliveryState: error.deliveryState ?? "not_sent" }), isError: true }; }
51
+ });
52
+ }
53
+
54
+ register("agent_bridge_status", "Check this Antigravity caller's native identity and outbound bridge capabilities without sending work.", {}, true, async (_, extra) => {
55
+ const identity = await resolveSender(extra?._meta);
56
+ const metadataFields = Object.fromEntries(Object.entries(extra?._meta ?? {}).filter(([key]) => key.startsWith("antigravity.google/")).map(([key, value]) => [key, Array.isArray(value) ? "array" : typeof value]));
57
+ return { version, sender: identity, metadataFields, targets: ["codex-desktop", "claude-desktop"], permissions: "native-unchanged", createsConversations: false };
58
+ });
59
+
60
+ register("list_agent_tasks", "List exact existing native tasks in this Antigravity conversation's project before selecting a destination.", targetSchema, true, async (args, extra) => {
61
+ const identity = await sender(extra);
62
+ return targets.list({ ...args, sender: identity, beforeSend: recheck(identity, extra) });
63
+ });
64
+
65
+ register("send_to_agent_task", "Continue an exact existing Codex or Claude Desktop task with authorized work. Preserve requestId across retries; inspect the durable receipt.", { ...taskSchema, requestId: z.string().uuid(), cwd: z.string().min(1), prompt: z.string().min(1).max(30000) }, false, async (args, extra) => {
66
+ const identity = await sender(extra);
67
+ const cwd = scope(identity.cwd, args.cwd);
68
+ if (args.prompt.includes("\0")) throw new Error("Prompt must not contain NUL");
69
+ const prompt = `[From Antigravity Desktop · ${path.basename(cwd)} · Authorized task handoff]\n[Source conversation: ${identity.id}]\n\n${args.prompt}`;
70
+ return deliveries.dispatch({ ...args, cwd, surface: args.target, sender: { provider: identity.provider, id: identity.id }, prompt }, async () => {
71
+ await recheck(identity, extra)();
72
+ const sent = await targets.send({ ...args, cwd, prompt, sender: identity, ...(args.target === "codex" ? { threadId: args.conversationId } : { sessionId: args.conversationId }), beforeSend: recheck(identity, extra) });
73
+ return { ...sent, completed: false };
74
+ });
75
+ });
76
+
77
+ register("read_agent_delivery", "Read an owned delivery without resending. Accepted or pending does not prove a completed response.", { requestId: z.string().uuid() }, true, async ({ requestId }, extra) => {
78
+ const identity = await sender(extra);
79
+ const record = deliveries.read(requestId);
80
+ if (!record || record.sender?.provider !== "antigravity" || record.sender.id !== identity.id) throw new Error("Delivery does not belong to this Antigravity conversation");
81
+ scope(identity.cwd, record.cwd);
82
+ if (record.result?.accountContext) assertAccountIdentity(record.result.accountContext);
83
+ if (record.surface === "claude" && record.result?.msgId) {
84
+ try {
85
+ return { ...record, nativeReceipt: await targets.read({ target: "claude", msgId: record.result.msgId, sender: identity, beforeSend: recheck(identity, extra) }) };
86
+ } catch (error) {
87
+ if (error.code !== "ANTIGRAVITY_NATIVE_RECEIPT_UNAVAILABLE") throw error;
88
+ return { ...record, nativeReceipt: null, recovery: "The original native receipt is unavailable. Inspect the exact Claude Desktop task before further action; do not resend.", receiptError: error.message };
89
+ }
90
+ }
91
+ return record;
92
+ });
93
+
94
+ register("read_agent_task", "Read the exact destination task's current native state and available response in the same project.", taskSchema, true, async (args, extra) => {
95
+ const identity = await sender(extra);
96
+ return targets.read({ ...args, cwd: identity.cwd, sender: identity, ...(args.target === "codex" ? { threadId: args.conversationId } : { sessionId: args.conversationId }), beforeSend: recheck(identity, extra) });
97
+ });
98
+
99
+ return { server, close: async () => { await targets.close(); await server.close(); } };
100
+ }
101
+
102
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
103
+ exitForVersionRequest(import.meta.url);
104
+ const bridge = createAntigravityOutboundBridge();
105
+ let stopping = false;
106
+ const stop = async () => {
107
+ if (stopping) return;
108
+ stopping = true;
109
+ await bridge.close();
110
+ await stopProcessInspectors();
111
+ };
112
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
113
+ process.stdin.once("end", () => { void stop(); });
114
+ await bridge.server.connect(new StdioServerTransport());
115
+ }
@@ -0,0 +1,244 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
4
+ import { readBridgeAccounts, requireBridgeAccounts, assertAccountIdentity } from "./bridge-account-context.mjs";
5
+ import { PeerEndpoint, listClaudeSessions, assertClaudeSessionProcess } from "./peer-protocol.mjs";
6
+ import { readClaudeDesktopContext } from "./claude-desktop-context.mjs";
7
+ import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
8
+ import { assertRecipientClass } from "./recipient-preflight.mjs";
9
+ import { sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
10
+ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
11
+ import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
12
+
13
+ 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;
14
+
15
+ function blocked(message) {
16
+ return Object.assign(new Error(message), { deliveryState: "not_sent" });
17
+ }
18
+
19
+ function captureDirectory(value) {
20
+ if (typeof value !== "string" || !path.isAbsolute(value)) throw blocked("An absolute existing project directory is required");
21
+ const canonical = fs.realpathSync.native(value);
22
+ const stat = fs.statSync(canonical, { bigint: true });
23
+ if (!stat.isDirectory()) throw blocked("The project path must be a directory");
24
+ return { path: canonical, identity: `${stat.dev}:${stat.ino}:${stat.birthtimeNs}` };
25
+ }
26
+
27
+ function sameDirectory(left, right) {
28
+ return path.relative(left, right) === "";
29
+ }
30
+
31
+ function senderIdentity(sender) {
32
+ if (sender?.provider !== "antigravity" || !UUID.test(sender.id ?? "") || sender.conversationId !== sender.id
33
+ || !["bypass", "prompting"].includes(sender.mode) || typeof sender.permissionFingerprint !== "string" || !sender.permissionFingerprint) {
34
+ throw blocked("Verified Antigravity conversation identity and permission evidence are required");
35
+ }
36
+ return Object.freeze({ provider: sender.provider, id: sender.id, conversationId: sender.conversationId, cwd: sender.cwd, mode: sender.mode, permissionFingerprint: sender.permissionFingerprint });
37
+ }
38
+
39
+ export function createAntigravityOutboundTargets(options = {}) {
40
+ const env = options.env ?? process.env;
41
+ const relay = options.relay ?? new NativeDesktopRelay({ env, socketPath: desktopTaskSocketPath(env), accountSocketPath: accountRelaySocketPath(env) });
42
+ const readAccounts = options.readAccounts ?? readBridgeAccounts;
43
+ const assertAccounts = options.assertAccounts ?? ((identity) => assertAccountIdentity(identity, readAccounts()));
44
+ const listSessions = options.listSessions ?? listClaudeSessions;
45
+ const readContext = options.readContext ?? ((session, account) => readClaudeDesktopContext(session, { account }));
46
+ const readInbound = options.readInbound ?? readClaudeInboundPolicy;
47
+ const assertProcess = options.assertProcess ?? assertClaudeSessionProcess;
48
+ const roots = options.roots ?? createHardenedRootPolicy(env);
49
+ const peerFactory = options.peerFactory ?? ((settings) => new PeerEndpoint(settings));
50
+ const receipts = new Map();
51
+ let peer;
52
+ let closed = false;
53
+
54
+ function operation(sender, beforeSend, required = false) {
55
+ if (closed) throw blocked("Antigravity outbound targets have been closed");
56
+ if (required && typeof beforeSend !== "function") throw blocked("A live Antigravity sender revalidation callback is required");
57
+ const identity = senderIdentity(sender);
58
+ const cwd = captureDirectory(identity.cwd);
59
+ const root = roots.capture(cwd.path, "Antigravity sender project");
60
+ const accounts = readAccounts();
61
+ const accountContext = requireBridgeAccounts(accounts);
62
+ const check = async () => {
63
+ if (closed) throw blocked("Antigravity outbound targets have been closed");
64
+ assertAccounts(accountContext);
65
+ roots.recheck(root, "Antigravity sender project");
66
+ const current = captureDirectory(identity.cwd);
67
+ if (!sameDirectory(current.path, cwd.path) || current.identity !== cwd.identity) throw blocked("Antigravity sender project changed before delivery");
68
+ const refreshed = await beforeSend?.();
69
+ if (refreshed?.provider && JSON.stringify(senderIdentity(refreshed)) !== JSON.stringify(identity)) throw blocked("Antigravity sender identity or permissions changed before delivery");
70
+ assertAccounts(accountContext);
71
+ };
72
+ return { identity, cwd: cwd.path, accounts, accountContext, check };
73
+ }
74
+
75
+ async function request(context, name, args, beforeSend = context.check) {
76
+ const response = await relay.requestDesktop(name, args, { accountContext: context.accountContext, beforeSend });
77
+ try { await context.check(); }
78
+ catch (error) { if (name === "send_message_to_thread") error.deliveryState = "unknown"; throw error; }
79
+ return response.result;
80
+ }
81
+
82
+ async function projects(context) {
83
+ const result = await request(context, "list_projects", {});
84
+ if (!Array.isArray(result?.projects)) throw blocked("Native Codex saved projects are unavailable");
85
+ const matches = result.projects.filter((project) => {
86
+ try { return project.projectKind === "local" && project.hostId === "local" && typeof project.projectId === "string" && project.projectId && sameDirectory(captureDirectory(project.path).path, context.cwd); }
87
+ catch { return false; }
88
+ });
89
+ if (matches.length !== 1 || result.projects.filter((project) => project.projectId === matches[0].projectId).length !== 1) throw blocked("Sender project must match exactly one saved local Codex project");
90
+ return matches[0];
91
+ }
92
+
93
+ function threadRecord(thread, threadId, project, context, requireProjectId = true) {
94
+ if (thread?.id !== threadId || thread.kind !== "codex" || thread.hostId !== "local"
95
+ || (requireProjectId || thread.projectId !== undefined) && thread.projectId !== project.projectId
96
+ || !sameDirectory(captureDirectory(thread.cwd).path, context.cwd)) throw blocked("Exact Codex task does not belong to the current account's matching saved project");
97
+ return { threadId, projectId: project.projectId, cwd: context.cwd, title: thread.title ?? null, status: thread.status ?? null, hostId: "local", target: "codex" };
98
+ }
99
+
100
+ async function codexRecipient(context, threadId) {
101
+ if (!UUID.test(threadId ?? "")) throw blocked("An exact existing Codex task UUID is required");
102
+ const project = await projects(context);
103
+ const inventory = await request(context, "list_threads", { limit: 50 });
104
+ if (!Array.isArray(inventory?.threads) || !Array.isArray(inventory?.pinnedThreads ?? [])) throw blocked("Native Codex task inventory is unavailable");
105
+ const matches = [...(inventory.pinnedThreads ?? []), ...inventory.threads].filter((thread) => thread?.id === threadId);
106
+ if (!matches.length) throw blocked("Exact Codex task must be an existing recent or pinned task");
107
+ for (const thread of matches) threadRecord(thread, threadId, project, context);
108
+ const result = await request(context, "read_thread", { threadId, hostId: "local", turnLimit: 1, includeOutputs: false, maxOutputCharsPerItem: 1 });
109
+ return threadRecord(result?.thread, threadId, project, context, false);
110
+ }
111
+
112
+ function claudeRecipient(context, sessionId, taskId) {
113
+ if (typeof sessionId !== "string" || !sessionId || typeof taskId !== "string" || !taskId) throw blocked("Exact Claude sessionId and native taskId are required");
114
+ const candidates = listSessions().filter((session) => session.sessionId === sessionId);
115
+ if (candidates.length !== 1) throw blocked("Exact Claude session is missing or ambiguous");
116
+ const session = candidates[0];
117
+ if (!session.alive || session.entrypoint !== "claude-desktop" || !session.socket || !sameDirectory(captureDirectory(session.cwd).path, context.cwd)) throw blocked("Claude destination must be a live Desktop Code session in the sender project");
118
+ assertProcess(session);
119
+ const desktop = readContext(session, context.accounts.claude);
120
+ if (desktop?.status !== "matched" || desktop.taskId !== taskId || desktop.accountFingerprint !== context.accountContext.claude
121
+ || !sameDirectory(captureDirectory(desktop.cwd).path, context.cwd)) throw blocked("Claude native task identity or current account does not match");
122
+ return { ...session, desktop, inbound: readInbound(session.cwd) };
123
+ }
124
+
125
+ async function list({ target, sender, beforeSend } = {}) {
126
+ const context = operation(sender, beforeSend);
127
+ await context.check();
128
+ if (target === "codex") {
129
+ const project = await projects(context);
130
+ const result = await request(context, "list_threads", { limit: 50 });
131
+ if (!Array.isArray(result?.threads) || !Array.isArray(result?.pinnedThreads ?? [])) throw blocked("Native Codex task inventory is unavailable");
132
+ const records = [];
133
+ const seen = new Set();
134
+ for (const thread of [...(result.pinnedThreads ?? []), ...result.threads]) {
135
+ if (seen.has(thread?.id)) continue;
136
+ try {
137
+ const row = threadRecord(thread, thread.id, project, context);
138
+ if (!UUID.test(row.threadId)) continue;
139
+ records.push(row);
140
+ seen.add(thread.id);
141
+ } catch {}
142
+ }
143
+ return records;
144
+ }
145
+ if (target !== "claude") throw blocked("Target must be codex or claude");
146
+ const rows = [];
147
+ for (const session of listSessions()) {
148
+ try {
149
+ const desktop = readContext(session, context.accounts.claude);
150
+ const verified = claudeRecipient(context, session.sessionId, desktop?.taskId);
151
+ rows.push({ target: "claude", sessionId: verified.sessionId, taskId: verified.desktop.taskId, cwd: context.cwd, title: verified.desktop.title ?? null, permissionClass: verified.desktop.permissionClass ?? null, inboundPolicy: verified.inbound?.value ?? null });
152
+ } catch {}
153
+ }
154
+ await context.check();
155
+ return rows;
156
+ }
157
+
158
+ async function send({ target, threadId, sessionId, taskId, cwd, prompt, sender, beforeSend } = {}) {
159
+ const context = operation(sender, beforeSend, true);
160
+ if (!sameDirectory(captureDirectory(cwd).path, context.cwd)) throw blocked("Destination directory must match the verified Antigravity sender project");
161
+ if (typeof prompt !== "string" || !prompt.trim() || Buffer.byteLength(prompt) > 65536) throw blocked("Prompt must contain between 1 and 65536 UTF-8 bytes");
162
+ await context.check();
163
+ const text = `[Agent handoff from Antigravity conversation ${context.identity.id}; native recipient permissions remain in effect.]\n\n${prompt}`;
164
+ if (target === "codex") {
165
+ const recipient = await codexRecipient(context, threadId);
166
+ try {
167
+ const result = await request(context, "send_message_to_thread", { threadId, prompt: text }, async () => {
168
+ await context.check();
169
+ const current = await codexRecipient(context, threadId);
170
+ if (current.projectId !== recipient.projectId || !sameDirectory(current.cwd, recipient.cwd)) throw blocked("Codex destination changed before delivery");
171
+ await context.check();
172
+ });
173
+ return { target, threadId, cwd: context.cwd, status: "accepted", completed: false, accountContext: context.accountContext, result };
174
+ } catch (error) {
175
+ if (!error.deliveryState) error.deliveryState = error.sent === false || error.reachedCompanion === false ? "not_sent" : "unknown";
176
+ if (error.deliveryState === "unknown") error.accountContext = context.accountContext;
177
+ throw error;
178
+ }
179
+ }
180
+ if (target !== "claude") throw blocked("Target must be codex or claude");
181
+ const recipient = claudeRecipient(context, sessionId, taskId);
182
+ assertRecipientClass(recipient, context.identity);
183
+ if (!peer) peer = peerFactory({ name: `antigravity-${process.pid}`, cwd: context.cwd, signalExit: false, env, protectSocket: protectCurrentUserPipe });
184
+ await peer.start();
185
+ let msgId;
186
+ try {
187
+ const outcome = await peer.sendAndWait(recipient.socket, text, {
188
+ timeoutMs: 0, permissionMode: context.identity.mode, transcriptSession: recipient, accountContext: context.accountContext,
189
+ recipient: { permissionMode: recipient.desktop.permissionMode ?? null, permissionClass: recipient.desktop.permissionClass ?? null, inboundPolicy: recipient.inbound?.value ?? null },
190
+ beforeSend: async () => {
191
+ await context.check();
192
+ const current = claudeRecipient(context, sessionId, taskId);
193
+ if (!sameClaudeDesktopRecipient(recipient, current)) throw blocked("Claude session process or socket changed before delivery");
194
+ assertRecipientClass(current, context.identity);
195
+ await context.check();
196
+ },
197
+ });
198
+ msgId = outcome.msgId;
199
+ receipts.set(msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
200
+ await context.check();
201
+ return { target, sessionId, taskId, cwd: context.cwd, msgId, status: outcome.delivery?.status ?? "sent_unconfirmed", completed: false, accountContext: context.accountContext };
202
+ } catch (error) {
203
+ if (error.msgId) receipts.set(error.msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
204
+ if (msgId) error.msgId = msgId;
205
+ if (msgId || error.deliveryUncertain) error.deliveryState = "unknown";
206
+ else if (!error.deliveryState) error.deliveryState = "not_sent";
207
+ if (error.deliveryState === "unknown") error.accountContext = context.accountContext;
208
+ throw error;
209
+ }
210
+ }
211
+
212
+ async function read({ target, threadId, sessionId, taskId, cwd, msgId, sender, beforeSend } = {}) {
213
+ const context = operation(sender, beforeSend);
214
+ if (cwd !== undefined && !sameDirectory(captureDirectory(cwd).path, context.cwd)) throw blocked("Destination directory must match the verified Antigravity sender project");
215
+ await context.check();
216
+ if (target === "codex") {
217
+ const recipient = await codexRecipient(context, threadId);
218
+ const result = await request(context, "read_thread", { threadId, hostId: "local", turnLimit: 3, includeOutputs: false, maxOutputCharsPerItem: 4000 });
219
+ threadRecord(result?.thread, threadId, { projectId: recipient.projectId }, context, false);
220
+ return { target, threadId, cwd: context.cwd, result };
221
+ }
222
+ if (target !== undefined && target !== "claude") throw blocked("Target must be codex or claude");
223
+ if (msgId === undefined) {
224
+ const recipient = claudeRecipient(context, sessionId, taskId);
225
+ await context.check();
226
+ return { target: "claude", sessionId, taskId, cwd: context.cwd, title: recipient.desktop.title ?? null, permissionClass: recipient.desktop.permissionClass ?? null, inboundPolicy: recipient.inbound?.value ?? null };
227
+ }
228
+ const receipt = receipts.get(msgId);
229
+ if (!receipt) throw Object.assign(blocked("The native receipt is unavailable in this bridge process"), { code: "ANTIGRAVITY_NATIVE_RECEIPT_UNAVAILABLE" });
230
+ if (receipt.sender.id !== context.identity.id || !sameDirectory(receipt.cwd, context.cwd)) throw blocked("Receipt does not belong to this Antigravity conversation");
231
+ assertAccounts(receipt.accountContext);
232
+ const result = peer.readDelivery(msgId);
233
+ await context.check();
234
+ return result;
235
+ }
236
+
237
+ async function close() {
238
+ closed = true;
239
+ peer?.stop();
240
+ await peer?.closed;
241
+ }
242
+
243
+ return { list, send, read, close };
244
+ }