@minhspark/codex-mcp-bridge 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +32 -1
- package/package.json +8 -3
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +233 -0
- package/scripts/install-native-relay.mjs +1 -1
- package/src/antigravity-bridge.mjs +170 -0
- package/src/antigravity-cli.mjs +248 -0
- package/src/antigravity-deliveries.mjs +93 -0
- package/src/antigravity-desktop.mjs +146 -0
- package/src/antigravity-ipc.mjs +141 -0
- package/src/antigravity-outbound-bridge.mjs +115 -0
- package/src/antigravity-outbound-targets.mjs +244 -0
- package/src/antigravity-sender-context.mjs +162 -0
- package/src/antigravity-sidecar.mjs +100 -0
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { readProcessAncestry, runProcessInspector } from "./claude-sender-context.mjs";
|
|
8
|
+
import { antigravityBridgeDirectory, callAntigravityDesktop } from "./antigravity-ipc.mjs";
|
|
9
|
+
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
const PREFIX = "antigravity.google/";
|
|
12
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
class ContextError extends Error {}
|
|
14
|
+
const fail = (reason) => { throw new ContextError(reason); };
|
|
15
|
+
const hash = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
16
|
+
|
|
17
|
+
export async function readAntigravityConfigurationJson(file, { filesystem = fs } = {}) {
|
|
18
|
+
const valid = (stat) => stat.isFile() && stat.nlink === 1n && stat.size >= 0n && stat.size <= 1048576n;
|
|
19
|
+
const same = (left, right) => ["dev", "ino", "size", "mtimeNs", "ctimeNs", "nlink"].every((key) => left[key] === right[key]);
|
|
20
|
+
const handle = await filesystem.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
21
|
+
try {
|
|
22
|
+
const opened = await handle.stat({ bigint: true });
|
|
23
|
+
const linked = await filesystem.lstat(file, { bigint: true });
|
|
24
|
+
if (!valid(opened) || !valid(linked)) fail("Native configuration must be a bounded regular file with one link");
|
|
25
|
+
if (!same(opened, linked)) fail("Native configuration changed while opening");
|
|
26
|
+
const buffer = Buffer.alloc(Number(opened.size) + 1);
|
|
27
|
+
let length = 0;
|
|
28
|
+
while (length < buffer.length) {
|
|
29
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
30
|
+
if (bytesRead === 0) break;
|
|
31
|
+
length += bytesRead;
|
|
32
|
+
}
|
|
33
|
+
const after = await handle.stat({ bigint: true });
|
|
34
|
+
const current = await filesystem.lstat(file, { bigint: true });
|
|
35
|
+
if (length !== Number(opened.size) || !valid(after) || !valid(current) || !same(opened, after) || !same(after, current)) fail("Native configuration changed while reading");
|
|
36
|
+
return JSON.parse(buffer.subarray(0, length).toString("utf8"));
|
|
37
|
+
} finally { await handle.close(); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function readExecutables(rows, { platform }) {
|
|
41
|
+
if (platform === "win32") {
|
|
42
|
+
const ids = rows.map((row) => row.pid);
|
|
43
|
+
const script = `$ErrorActionPreference='Stop'; @(${ids.join(",")}) | ForEach-Object { $processId=$_; $p=Get-Process -Id $processId -ErrorAction SilentlyContinue; [PSCustomObject]@{pid=$processId;executable=$p.Path} } | ConvertTo-Json -Compress`;
|
|
44
|
+
const shell = path.win32.join(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
45
|
+
const result = await runProcessInspector(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { timeout: 10000, maxBuffer: 16384, windowsHide: true });
|
|
46
|
+
const parsed = JSON.parse(result.stdout);
|
|
47
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
48
|
+
}
|
|
49
|
+
if (platform === "darwin") return Promise.all(rows.map(async ({ pid }) => {
|
|
50
|
+
const result = await runProcessInspector("/bin/ps", ["-p", String(pid), "-o", "comm="], { timeout: 5000, maxBuffer: 4096 });
|
|
51
|
+
return { pid, executable: result.stdout.trim() };
|
|
52
|
+
}));
|
|
53
|
+
fail("Antigravity Desktop host verification is unsupported on this platform");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validAncestry(rows, parentPid) {
|
|
57
|
+
return Array.isArray(rows) && rows.length > 0 && rows.length <= 8 && rows[0].pid === parentPid
|
|
58
|
+
&& new Set(rows.map((row) => row.pid)).size === rows.length
|
|
59
|
+
&& rows.every((row, index) => Number.isSafeInteger(row.pid) && row.pid > 0 && row.pid <= 0xffffffff
|
|
60
|
+
&& Number.isSafeInteger(row.parentPid) && row.parentPid >= 0 && row.parentPid <= 0xffffffff
|
|
61
|
+
&& typeof row.processStart === "string" && row.processStart.length > 0
|
|
62
|
+
&& (!index || rows[index - 1].parentPid === row.pid));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function grants(value) {
|
|
66
|
+
if (value === undefined) return { allow: [], ask: [], deny: [] };
|
|
67
|
+
if (!object(value) || Object.keys(value).some((key) => !["allow", "ask", "deny"].includes(key))) fail("Native permission grants have an unsupported schema");
|
|
68
|
+
const result = {};
|
|
69
|
+
for (const key of ["allow", "ask", "deny"]) {
|
|
70
|
+
const items = value[key] ?? [];
|
|
71
|
+
if (!Array.isArray(items) || items.some((item) => typeof item !== "string" || !item.trim())) fail("Native permission grants have an unsupported schema");
|
|
72
|
+
result[key] = items;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function permissions(global, project) {
|
|
78
|
+
const settings = global?.userSettings;
|
|
79
|
+
if (!object(settings) || !object(project.settings ?? {})) fail("Native permission settings are unavailable");
|
|
80
|
+
const local = project.settings ?? {};
|
|
81
|
+
const wrapper = project.permissionGrants ?? {};
|
|
82
|
+
if (!object(wrapper) || Object.keys(wrapper).some((key) => !["permissionGrants", "v2Migrated"].includes(key))) fail("Native project permission grants have an unsupported schema");
|
|
83
|
+
if (settings.permissionGrantsV2Migrated !== true || (wrapper.v2Migrated !== undefined && wrapper.v2Migrated !== true)) fail("Native permission migration state is unsupported");
|
|
84
|
+
const globalGrants = grants(settings.globalPermissionGrants);
|
|
85
|
+
const projectGrants = grants(wrapper.permissionGrants);
|
|
86
|
+
const preset = local.permissionPreset ?? settings.permissionPreset;
|
|
87
|
+
const execution = local.autoExecutionPolicy ?? settings.autoExecutionPolicy;
|
|
88
|
+
const sandbox = local.sandboxMode ?? settings.enableTerminalSandbox;
|
|
89
|
+
const fileAccess = local.fileAccessPolicy ?? settings.nonWorkspaceFileAccessPolicy;
|
|
90
|
+
const artifact = local.artifactReviewMode ?? settings.artifactReviewMode;
|
|
91
|
+
const browser = local.browserJsExecutionPolicy ?? settings.browserJsExecutionPolicy;
|
|
92
|
+
const knownPresets = ["AGENT_PERMISSION_PRESET_TURBO", "AGENT_PERMISSION_PRESET_REQUEST_REVIEW", "AGENT_PERMISSION_PRESET_DEFAULT", "AGENT_PERMISSION_PRESET_AUTO"];
|
|
93
|
+
if (preset !== undefined && !knownPresets.includes(preset)) fail("Native permission preset is unsupported");
|
|
94
|
+
if (!['CASCADE_COMMANDS_AUTO_EXECUTION_EAGER', 'CASCADE_COMMANDS_AUTO_EXECUTION_OFF', 'CASCADE_COMMANDS_AUTO_EXECUTION_AUTO', 'CASCADE_COMMANDS_AUTO_EXECUTION_PROCEED_IN_SANDBOX'].includes(execution)
|
|
95
|
+
|| typeof sandbox !== "boolean" || !["AGENT_SETTING_POLICY_ALLOW", "AGENT_SETTING_POLICY_ASK", "AGENT_SETTING_POLICY_DENY"].includes(fileAccess)
|
|
96
|
+
|| !["ARTIFACT_REVIEW_MODE_ALWAYS", "ARTIFACT_REVIEW_MODE_TURBO", "ARTIFACT_REVIEW_MODE_AUTO"].includes(artifact)
|
|
97
|
+
|| !["BROWSER_JS_EXECUTION_POLICY_DISABLED", "BROWSER_JS_EXECUTION_POLICY_ALWAYS_ASK", "BROWSER_JS_EXECUTION_POLICY_MODEL_DECIDES", "BROWSER_JS_EXECUTION_POLICY_TURBO"].includes(browser)) fail("Native permission settings are incomplete or unsupported");
|
|
98
|
+
const restricted = globalGrants.ask.length + globalGrants.deny.length + projectGrants.ask.length + projectGrants.deny.length > 0;
|
|
99
|
+
const unrestricted = (!preset || preset === "AGENT_PERMISSION_PRESET_TURBO") && execution === "CASCADE_COMMANDS_AUTO_EXECUTION_EAGER" && sandbox === false && fileAccess === "AGENT_SETTING_POLICY_ALLOW" && artifact === "ARTIFACT_REVIEW_MODE_TURBO" && browser === "BROWSER_JS_EXECUTION_POLICY_TURBO" && !restricted;
|
|
100
|
+
const mode = unrestricted ? "bypass" : "prompting";
|
|
101
|
+
return { mode, fingerprint: hash({ preset, execution, sandbox, fileAccess, artifact, browser, globalGrants, projectGrants, globalMigration: settings.permissionGrantsV2Migrated, projectMigration: wrapper.v2Migrated }) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function resolveAntigravitySenderContext(meta, dependencies = {}) {
|
|
105
|
+
let reason = "Native Antigravity caller metadata is missing or invalid";
|
|
106
|
+
try {
|
|
107
|
+
if (!object(meta)) fail(reason);
|
|
108
|
+
const conversationId = meta[PREFIX + "conversation_id"];
|
|
109
|
+
const workspaces = meta[PREFIX + "workspace_paths"];
|
|
110
|
+
if (typeof conversationId !== "string" || !UUID.test(conversationId) || (workspaces !== undefined && (!Array.isArray(workspaces) || workspaces.length !== 1))) fail(reason);
|
|
111
|
+
const parent = meta[PREFIX + "parent_conversation_id"];
|
|
112
|
+
const root = meta[PREFIX + "root_conversation_id"];
|
|
113
|
+
if ((parent !== undefined && parent !== null && parent !== "") || (root !== undefined && root !== null && root !== "" && root !== conversationId)) fail("Nested Antigravity callers are not supported");
|
|
114
|
+
const { home = os.homedir(), platform = process.platform, parentPid = process.ppid, readAncestry = readProcessAncestry, readExecutables: executables = readExecutables, readJson: json = readAntigravityConfigurationJson, realpath = fs.realpath } = dependencies;
|
|
115
|
+
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
116
|
+
const canonical = async (value) => {
|
|
117
|
+
if (typeof value !== "string" || value.includes("\0") || !paths.isAbsolute(value)) fail("Native workspace path is invalid");
|
|
118
|
+
const resolved = paths.normalize(await realpath(value));
|
|
119
|
+
return platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
120
|
+
};
|
|
121
|
+
reason = "Native Antigravity Desktop process ancestry could not be verified";
|
|
122
|
+
const before = await readAncestry({ parentPid, platform });
|
|
123
|
+
if (!validAncestry(before, parentPid)) fail(reason);
|
|
124
|
+
const images = await executables(before, { platform });
|
|
125
|
+
if (!Array.isArray(images) || images.length !== before.length || new Set(images.map((image) => image.pid)).size !== before.length) fail(reason);
|
|
126
|
+
let appIndex = -1;
|
|
127
|
+
let expected;
|
|
128
|
+
for (let index = 0; index < before.length; index++) {
|
|
129
|
+
const executable = await canonical(images.find((image) => image.pid === before[index].pid)?.executable);
|
|
130
|
+
if (expected && executable === expected) { appIndex = index; break; }
|
|
131
|
+
const suffix = platform === "win32" ? "\\resources\\bin\\language_server.exe" : "/Contents/Resources/bin/language_server";
|
|
132
|
+
if (executable.endsWith(suffix)) {
|
|
133
|
+
const rootPath = executable.slice(0, -suffix.length);
|
|
134
|
+
expected = platform === "win32" ? paths.join(rootPath, "antigravity.exe") : paths.join(rootPath, "Contents", "MacOS", "Antigravity");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (appIndex < 0) fail(reason);
|
|
138
|
+
reason = "Native Antigravity conversation could not be verified";
|
|
139
|
+
const readNative = dependencies.readNative ?? ((id) => callAntigravityDesktop({ operation: "read", conversationId: id }, { directory: antigravityBridgeDirectory(home) }));
|
|
140
|
+
const native = await readNative(conversationId);
|
|
141
|
+
if (native?.conversationId !== conversationId || native.surface !== "desktop" || !UUID.test(native.projectId)) fail(reason);
|
|
142
|
+
const cwd = await canonical(native.cwd);
|
|
143
|
+
if (workspaces !== undefined && await canonical(workspaces[0]) !== cwd) fail(reason);
|
|
144
|
+
const projectId = native.projectId;
|
|
145
|
+
reason = "Native Antigravity project permissions could not be verified";
|
|
146
|
+
const globalPath = paths.join(home, ".gemini", "config", "config.json");
|
|
147
|
+
const projectPath = paths.join(home, ".gemini", "config", "projects", projectId + ".json");
|
|
148
|
+
const global = await json(globalPath);
|
|
149
|
+
const project = await json(projectPath);
|
|
150
|
+
const resources = project?.projectResources?.resources;
|
|
151
|
+
if (project?.id !== projectId || !Array.isArray(resources) || resources.length !== 1) fail(reason);
|
|
152
|
+
const uri = resources[0]?.gitFolder?.folderUri;
|
|
153
|
+
if (typeof uri !== "string" || await canonical(fileURLToPath(uri, { windows: platform === "win32" })) !== cwd) fail(reason);
|
|
154
|
+
const policy = permissions(global, project);
|
|
155
|
+
const after = await readAncestry({ parentPid, platform });
|
|
156
|
+
if (!validAncestry(after, parentPid) || hash(before.slice(0, appIndex + 1)) !== hash(after.slice(0, appIndex + 1))) fail("Native Antigravity host changed during verification");
|
|
157
|
+
if (hash(global) !== hash(await json(globalPath)) || hash(project) !== hash(await json(projectPath))) fail("Native Antigravity permissions changed during verification");
|
|
158
|
+
return { status: "verified", provider: "antigravity", id: conversationId, conversationId, cwd, projectId, surface: "desktop", mode: policy.mode, permissionFingerprint: policy.fingerprint };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { status: "unverified", provider: "antigravity", reason: error instanceof ContextError ? error.message : reason };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { antigravityBridgeDirectory, serveAntigravityDesktop } from "./antigravity-ipc.mjs";
|
|
5
|
+
import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
|
|
6
|
+
import { listAntigravityConversations, runAgentApi, sanitizeAntigravityMetadata } from "./antigravity-desktop.mjs";
|
|
7
|
+
import { exitForVersionRequest } from "./cli-version.mjs";
|
|
8
|
+
import { readProcessAncestry } from "./claude-sender-context.mjs";
|
|
9
|
+
|
|
10
|
+
export async function createAntigravityParentGuard({ parentPid = process.ppid, inspect = readProcessAncestry } = {}) {
|
|
11
|
+
if (!Number.isSafeInteger(parentPid) || parentPid <= 1 || parentPid > 0xffffffff) throw new Error("Antigravity sidecar parent is unavailable; relaunch it from the native app");
|
|
12
|
+
const read = async () => {
|
|
13
|
+
const rows = await inspect({ parentPid, maxDepth: 1 });
|
|
14
|
+
const row = rows?.[0];
|
|
15
|
+
return rows?.length === 1 && row?.pid === parentPid && typeof row.processStart === "string" && row.processStart ? { pid: row.pid, processStart: row.processStart } : null;
|
|
16
|
+
};
|
|
17
|
+
let initial;
|
|
18
|
+
try { initial = await read(); } catch {}
|
|
19
|
+
if (!initial) throw new Error("Antigravity sidecar parent is unavailable; relaunch it from the native app");
|
|
20
|
+
let unavailable = false;
|
|
21
|
+
let pending;
|
|
22
|
+
return async () => {
|
|
23
|
+
if (!unavailable) {
|
|
24
|
+
pending ??= (async () => {
|
|
25
|
+
try {
|
|
26
|
+
const current = await read();
|
|
27
|
+
unavailable = !current || current.pid !== initial.pid || current.processStart !== initial.processStart;
|
|
28
|
+
} catch { unavailable = true; }
|
|
29
|
+
})();
|
|
30
|
+
await pending;
|
|
31
|
+
pending = undefined;
|
|
32
|
+
}
|
|
33
|
+
if (unavailable) throw Object.assign(new Error("Antigravity sidecar parent exited or changed; relaunch it from the native app"), { deliveryState: "not_sent" });
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function startAntigravitySidecar({ directory = antigravityBridgeDirectory(), env = process.env, createParentGuard = createAntigravityParentGuard, serve = serveAntigravityDesktop, parentCheckIntervalMs = 5000, onParentLost = () => {} } = {}) {
|
|
38
|
+
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
|
+
const assertParent = await createParentGuard();
|
|
40
|
+
const deliveries = new AntigravityDeliveries(path.join(directory, "desktop-deliveries"));
|
|
41
|
+
const sending = new Set();
|
|
42
|
+
let sidecar;
|
|
43
|
+
let timer;
|
|
44
|
+
let closing;
|
|
45
|
+
let closed = false;
|
|
46
|
+
let parentLost = false;
|
|
47
|
+
const close = () => {
|
|
48
|
+
closed = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
return closing ??= Promise.resolve().then(() => sidecar?.close());
|
|
51
|
+
};
|
|
52
|
+
const checkParent = async () => {
|
|
53
|
+
try { await assertParent(); }
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!parentLost) { parentLost = true; void close().then(onParentLost); }
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
sidecar = await serve(async (request) => {
|
|
60
|
+
await checkParent();
|
|
61
|
+
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 === "list") return listAntigravityConversations({ surface: "desktop" });
|
|
64
|
+
if (request.operation === "read") {
|
|
65
|
+
const records = await listAntigravityConversations({ surface: "desktop" });
|
|
66
|
+
const record = records.find((item) => item.conversationId === request.conversationId);
|
|
67
|
+
if (!record) throw new Error("Exact Antigravity Desktop conversation not found");
|
|
68
|
+
const native = sanitizeAntigravityMetadata(await runAgentApi({ command: "metadata", conversationId: request.conversationId, env }), request.conversationId);
|
|
69
|
+
return { ...record, ...native };
|
|
70
|
+
}
|
|
71
|
+
if (request.operation === "receipt") return deliveries.read(request.requestId);
|
|
72
|
+
if (request.operation !== "send" || typeof request.prompt !== "string" || !request.prompt.trim() || request.prompt.length > 30000 || request.prompt.includes("\0")
|
|
73
|
+
|| typeof request.cwd !== "string" || !path.isAbsolute(request.cwd) || !request.sender || !["codex", "claude"].includes(request.sender.provider)
|
|
74
|
+
|| typeof request.sender.id !== "string" || !request.sender.id) throw new Error("Invalid Antigravity delivery request");
|
|
75
|
+
return deliveries.dispatch({ requestId: request.requestId, surface: "desktop", conversationId: request.conversationId, cwd: request.cwd, sender: request.sender, prompt: request.prompt }, async () => {
|
|
76
|
+
await checkParent();
|
|
77
|
+
if (sending.has(request.conversationId)) throw Object.assign(new Error("Another delivery is pending for this conversation"), { deliveryState: "not_sent" });
|
|
78
|
+
sending.add(request.conversationId);
|
|
79
|
+
try { return await runAgentApi({ command: "send", conversationId: request.conversationId, expectedCwd: request.cwd, prompt: request.prompt, env }); }
|
|
80
|
+
finally { sending.delete(request.conversationId); }
|
|
81
|
+
});
|
|
82
|
+
}, { directory });
|
|
83
|
+
try { await checkParent(); } catch (error) { await close(); throw error; }
|
|
84
|
+
const schedule = () => {
|
|
85
|
+
if (closed) return;
|
|
86
|
+
timer = setTimeout(async () => {
|
|
87
|
+
try { await checkParent(); } catch { return; }
|
|
88
|
+
schedule();
|
|
89
|
+
}, parentCheckIntervalMs);
|
|
90
|
+
timer.unref();
|
|
91
|
+
};
|
|
92
|
+
schedule();
|
|
93
|
+
return { ...sidecar, close };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
97
|
+
exitForVersionRequest(import.meta.url);
|
|
98
|
+
const sidecar = await startAntigravitySidecar({ onParentLost: () => process.exit(0) });
|
|
99
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void sidecar.close().then(() => process.exit(0)); });
|
|
100
|
+
}
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { discoverClaudeProject } from "./claude-project-discovery.mjs";
|
|
|
30
30
|
|
|
31
31
|
exitForVersionRequest(import.meta.url);
|
|
32
32
|
|
|
33
|
-
const VERSION = "1.
|
|
33
|
+
const VERSION = "1.24.0";
|
|
34
34
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
35
35
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
36
36
|
const FORWARD_MAX_PER_SESSION = 50;
|
package/src/index.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { createRelayWake, relayKeeperEnabled, relayWakeStateFile, startRelayKeep
|
|
|
41
41
|
|
|
42
42
|
exitForVersionRequest(import.meta.url);
|
|
43
43
|
|
|
44
|
-
const VERSION = "1.
|
|
44
|
+
const VERSION = "1.24.0";
|
|
45
45
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
46
46
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
47
47
|
|