@minhspark/codex-mcp-bridge 1.25.2 → 1.26.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.
Files changed (50) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +81 -32
  4. package/docs/new-macos-setup.md +47 -21
  5. package/docs/new-windows-setup.md +34 -9
  6. package/docs/project-onboarding.md +21 -8
  7. package/package.json +2 -1
  8. package/plugin.json +1 -1
  9. package/scripts/background-relay-hidden.vbs +11 -0
  10. package/scripts/install-antigravity-bridge.mjs +65 -23
  11. package/scripts/install-background-relay.mjs +428 -0
  12. package/scripts/install-claude-code.mjs +2 -2
  13. package/scripts/install-claude-desktop.mjs +12 -7
  14. package/scripts/install-native-relay.mjs +2 -2
  15. package/scripts/setup-background-relay.mjs +58 -0
  16. package/scripts/setup.mjs +3 -3
  17. package/scripts/worker-relay.mjs +41 -0
  18. package/src/antigravity-bridge.mjs +96 -27
  19. package/src/antigravity-cli.mjs +13 -2
  20. package/src/antigravity-deliveries.mjs +32 -8
  21. package/src/antigravity-ipc.mjs +18 -9
  22. package/src/antigravity-outbound-bridge.mjs +71 -23
  23. package/src/antigravity-outbound-targets.mjs +100 -10
  24. package/src/background-antigravity-worker.mjs +192 -0
  25. package/src/background-claude-worker.mjs +240 -0
  26. package/src/background-codex-worker.mjs +344 -0
  27. package/src/background-relay-client.mjs +107 -0
  28. package/src/background-relay-config.mjs +98 -0
  29. package/src/background-relay-daemon.mjs +60 -0
  30. package/src/background-relay-mcp.mjs +45 -0
  31. package/src/background-relay-protocol.mjs +54 -0
  32. package/src/background-relay-registration.mjs +198 -0
  33. package/src/background-relay-service.mjs +311 -0
  34. package/src/background-relay-storage.mjs +203 -0
  35. package/src/background-relay-storage.ps1 +64 -0
  36. package/src/background-relay.mjs +373 -0
  37. package/src/bridge-readiness.mjs +2 -2
  38. package/src/claude-bridge.mjs +43 -25
  39. package/src/claude-code-registration.mjs +2 -1
  40. package/src/claude-session-router.mjs +8 -15
  41. package/src/index.mjs +39 -22
  42. package/src/mcp-supervisor.mjs +26 -16
  43. package/src/native-relay-companion.mjs +4 -1
  44. package/src/native-relay.mjs +5 -1
  45. package/src/peer-protocol.mjs +9 -6
  46. package/src/recipient-preflight.mjs +81 -0
  47. package/src/reload-control.mjs +2 -2
  48. package/src/setup.mjs +62 -25
  49. package/src/thread-delivery.mjs +10 -5
  50. package/src/windows-pipe-acl.ps1 +1 -1
@@ -7,32 +7,68 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
  import { z } from "zod";
8
8
  import { resolveAntigravitySenderContext } from "./antigravity-sender-context.mjs";
9
9
  import { createAntigravityOutboundTargets } from "./antigravity-outbound-targets.mjs";
10
- import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
10
+ import { AntigravityDeliveries, antigravityDeliveryError } from "./antigravity-deliveries.mjs";
11
11
  import { antigravityBridgeDirectory } from "./antigravity-ipc.mjs";
12
12
  import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
13
13
  import { stopProcessInspectors } from "./claude-sender-context.mjs";
14
14
  import { exitForVersionRequest } from "./cli-version.mjs";
15
+ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./background-relay-mcp.mjs";
15
16
  import { assertAccountIdentity } from "./bridge-account-context.mjs";
17
+ import { bridgeDiagnostic, withBridgeDiagnostic, preflightFailure } from "./recipient-preflight.mjs";
18
+ import { createReloadControl, exitWhenClientCloses } from "./reload-control.mjs";
16
19
 
17
20
  const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
18
21
  const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
19
22
  const targetSchema = { target: z.enum(["codex", "claude"]) };
20
23
  const taskSchema = { ...targetSchema, conversationId: z.string().uuid(), taskId: z.string().min(1).max(256).optional() };
21
24
 
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." });
25
+ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigravitySenderContext, targets = createAntigravityOutboundTargets(), deliveries = new AntigravityDeliveries(path.join(antigravityBridgeDirectory(), "outbound-deliveries")), rootPolicy = createHardenedRootPolicy(), env = process.env, channel = process } = {}) {
26
+ const reload = createReloadControl({
27
+ entry: "antigravity-outbound-bridge.mjs", env, channel,
28
+ inspect: () => deliveries.active.size ? "Antigravity outbound deliveries are still active" : targets.reloadReason?.() ?? null,
29
+ quiesce: () => targets.quiesce(),
30
+ exportState: () => ({ directory: deliveries.directory, targets: targets.exportReloadState() }),
31
+ restore: (state) => {
32
+ if (typeof state.directory !== "string" || path.relative(state.directory, deliveries.directory)) throw new Error("Antigravity durable receipt directory changed during reload");
33
+ targets.restoreReloadState(state.targets);
34
+ },
35
+ activate: () => targets.activate(),
36
+ });
37
+ 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 for a native task, 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 native work requires a separate native conversation; native handoff tools only continue existing tasks. Never use Computer Use, open an old chat, or create an empty, new, or error chat to load or recover a relay. Report the structured diagnostic when a native relay or session is unavailable. Agent-authored handoffs must be in English and name Antigravity as the author. " + BACKGROUND_WORKER_GUIDANCE });
38
+
39
+ registerBackgroundTools((name, config, handler) => server.registerTool(name, config, async (args, extra) => {
40
+ try { return await reload.run(() => handler(args, extra)); }
41
+ catch (error) {
42
+ return withBridgeDiagnostic({ isError: true, content: [] }, error, { component: "agent-bridge", operation: name, requestId: args.requestId, readOnly: config.annotations?.readOnlyHint === true });
43
+ }
44
+ }), { env });
24
45
 
25
46
  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;
47
+ try {
48
+ if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("An absolute project directory is required");
49
+ const canonical = fs.realpathSync.native(expected);
50
+ if (path.relative(canonical, fs.realpathSync.native(actual))) throw new Error("Destination must match the verified Antigravity sender's project");
51
+ rootPolicy.assert(canonical, "Antigravity sender project");
52
+ return canonical;
53
+ } catch (error) {
54
+ error.code ??= "ANTIGRAVITY_PROJECT_SCOPE_INVALID";
55
+ error.phase ??= "scope";
56
+ error.deliveryState = "not_sent";
57
+ throw error;
58
+ }
31
59
  }
32
60
 
33
61
  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");
62
+ let identity;
63
+ try {
64
+ identity = await resolveSender(extra?._meta);
65
+ if (identity.status !== "verified" || identity.provider !== "antigravity" || !identity.id || !["bypass", "prompting"].includes(identity.mode)) throw new Error(identity.reason ?? "Antigravity sender is not verified");
66
+ } catch (error) {
67
+ error.code ??= "ANTIGRAVITY_SENDER_UNVERIFIED";
68
+ error.phase ??= "sender";
69
+ error.deliveryState = "not_sent";
70
+ throw error;
71
+ }
36
72
  scope(identity.cwd, identity.cwd);
37
73
  return identity;
38
74
  }
@@ -40,44 +76,54 @@ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigra
40
76
  function recheck(identity, extra) {
41
77
  return async () => {
42
78
  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" });
79
+ if (JSON.stringify(current) !== JSON.stringify(identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Antigravity sender identity or permissions changed before delivery");
44
80
  };
45
81
  }
46
82
 
47
83
  function register(name, description, inputSchema, readOnly, handler) {
48
84
  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 }; }
85
+ const options = { component: "agent-bridge", operation: name, requestId: args.requestId, conversationId: args.conversationId, readOnly };
86
+ try {
87
+ const value = await reload.run(() => handler(args, extra));
88
+ const failure = antigravityDeliveryError(value);
89
+ if (failure) return withBridgeDiagnostic({ ...result(value), isError: true }, failure, { ...options, readOnly: false, msgId: value.result?.msgId });
90
+ if (value?.diagnostic && (value.nativeReceipt === null || value.sender?.status !== "verified")) return withBridgeDiagnostic({ ...result(value), isError: true }, { diagnostic: value.diagnostic }, options);
91
+ return result(value);
92
+ } catch (error) {
93
+ const diagnostic = bridgeDiagnostic(error, options);
94
+ return withBridgeDiagnostic({ ...result({ error: diagnostic.reason, code: diagnostic.code, deliveryState: error.deliveryState ?? diagnostic.deliveryState }), isError: true }, { diagnostic }, options);
95
+ }
51
96
  });
52
97
  }
53
98
 
54
99
  register("agent_bridge_status", "Check this Antigravity caller's native identity and outbound bridge capabilities without sending work.", {}, true, async (_, extra) => {
55
100
  const identity = await resolveSender(extra?._meta);
56
101
  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 };
102
+ const diagnostic = identity.status === "verified" ? null : bridgeDiagnostic(Object.assign(new Error(identity.reason ?? "Antigravity sender is not verified"), { code: "ANTIGRAVITY_SENDER_UNVERIFIED", phase: "sender" }), { component: "agent-bridge", operation: "agent_bridge_status", readOnly: true });
103
+ return { version, sender: identity, metadataFields, targets: ["codex-desktop", "claude-desktop"], permissions: "native-unchanged", createsConversations: false, ...(diagnostic ? { diagnostic } : {}) };
58
104
  });
59
105
 
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) => {
106
+ register("list_agent_tasks", "List exact existing native tasks in this Antigravity conversation's project before selecting a destination. Dormant Claude tasks remain unavailable until their native session is already connected.", targetSchema, true, async (args, extra) => {
61
107
  const identity = await sender(extra);
62
108
  return targets.list({ ...args, sender: identity, beforeSend: recheck(identity, extra) });
63
109
  });
64
110
 
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) => {
111
+ register("send_to_agent_task", "Continue an exact existing Codex or connected Claude Desktop task with authorized work. Unavailable native sessions produce a diagnostic without opening chats; native permissions remain unchanged. 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
112
  const identity = await sender(extra);
67
113
  const cwd = scope(identity.cwd, args.cwd);
68
- if (args.prompt.includes("\0")) throw new Error("Prompt must not contain NUL");
114
+ if (args.prompt.includes("\0")) throw preflightFailure("ANTIGRAVITY_PROMPT_INVALID", "Prompt must not contain NUL");
69
115
  const prompt = `[From Antigravity Desktop · ${path.basename(cwd)} · Authorized task handoff]\n[Source conversation: ${identity.id}]\n\n${args.prompt}`;
70
116
  return deliveries.dispatch({ ...args, cwd, surface: args.target, sender: { provider: identity.provider, id: identity.id }, prompt }, async () => {
71
117
  await recheck(identity, extra)();
72
118
  const sent = await targets.send({ ...args, cwd, prompt, sender: identity, ...(args.target === "codex" ? { threadId: args.conversationId } : { sessionId: args.conversationId }), beforeSend: recheck(identity, extra) });
73
119
  return { ...sent, completed: false };
74
- });
120
+ }, { component: "agent-bridge", operation: "send_to_agent_task" });
75
121
  });
76
122
 
77
123
  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
124
  const identity = await sender(extra);
79
125
  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");
126
+ if (!record || record.sender?.provider !== "antigravity" || record.sender.id !== identity.id) throw preflightFailure("ANTIGRAVITY_RECEIPT_OWNER_MISMATCH", "Delivery does not belong to this Antigravity conversation");
81
127
  scope(identity.cwd, record.cwd);
82
128
  if (record.result?.accountContext) assertAccountIdentity(record.result.accountContext);
83
129
  if (record.surface === "claude" && record.result?.msgId) {
@@ -85,7 +131,8 @@ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigra
85
131
  return { ...record, nativeReceipt: await targets.read({ target: "claude", msgId: record.result.msgId, sender: identity, beforeSend: recheck(identity, extra) }) };
86
132
  } catch (error) {
87
133
  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 };
134
+ const diagnostic = bridgeDiagnostic(error, { component: "agent-bridge", operation: "read_agent_delivery", phase: "receipt", readOnly: true, requestId, msgId: record.result.msgId, conversationId: record.conversationId });
135
+ return { ...record, nativeReceipt: null, recovery: "The original native receipt is unavailable. Report the diagnostic before further action; do not resend or open a chat to load the relay.", receiptError: diagnostic.reason, diagnostic: record.diagnostic ?? diagnostic, receiptDiagnostic: diagnostic };
89
136
  }
90
137
  }
91
138
  return record;
@@ -96,7 +143,8 @@ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigra
96
143
  return targets.read({ ...args, cwd: identity.cwd, sender: identity, ...(args.target === "codex" ? { threadId: args.conversationId } : { sessionId: args.conversationId }), beforeSend: recheck(identity, extra) });
97
144
  });
98
145
 
99
- return { server, close: async () => { await targets.close(); await server.close(); } };
146
+ reload.listen();
147
+ return { server, reload, busy: () => deliveries.active.size ? "Antigravity outbound deliveries are still active" : null, close: async () => { await targets.close(); await server.close(); } };
100
148
  }
101
149
 
102
150
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -109,7 +157,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
109
157
  await bridge.close();
110
158
  await stopProcessInspectors();
111
159
  };
112
- for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
113
- process.stdin.once("end", () => { void stop(); });
114
160
  await bridge.server.connect(new StdioServerTransport());
161
+ const shutdown = exitWhenClientCloses({ busy: bridge.busy, stop });
162
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void shutdown.close(signal); });
115
163
  }
@@ -3,12 +3,13 @@ import path from "node:path";
3
3
  import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
4
4
  import { readBridgeAccounts, requireBridgeAccounts, assertAccountIdentity } from "./bridge-account-context.mjs";
5
5
  import { PeerEndpoint, listClaudeSessions, assertClaudeSessionProcess } from "./peer-protocol.mjs";
6
- import { readClaudeDesktopContext } from "./claude-desktop-context.mjs";
6
+ import { readClaudeDesktopContext, readClaudeDesktopTasks } from "./claude-desktop-context.mjs";
7
7
  import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
8
8
  import { assertRecipientClass } from "./recipient-preflight.mjs";
9
- import { sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
9
+ import { recoverClaudeDesktopSession, sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
10
10
  import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
11
11
  import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
12
+ import { cloneReloadState } from "./reload-control.mjs";
12
13
 
13
14
  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
 
@@ -42,6 +43,7 @@ export function createAntigravityOutboundTargets(options = {}) {
42
43
  const readAccounts = options.readAccounts ?? readBridgeAccounts;
43
44
  const assertAccounts = options.assertAccounts ?? ((identity) => assertAccountIdentity(identity, readAccounts()));
44
45
  const listSessions = options.listSessions ?? listClaudeSessions;
46
+ const listTasks = options.listTasks ?? ((account) => readClaudeDesktopTasks({ account }));
45
47
  const readContext = options.readContext ?? ((session, account) => readClaudeDesktopContext(session, { account }));
46
48
  const readInbound = options.readInbound ?? readClaudeInboundPolicy;
47
49
  const assertProcess = options.assertProcess ?? assertClaudeSessionProcess;
@@ -49,10 +51,12 @@ export function createAntigravityOutboundTargets(options = {}) {
49
51
  const peerFactory = options.peerFactory ?? ((settings) => new PeerEndpoint(settings));
50
52
  const receipts = new Map();
51
53
  let peer;
54
+ let peerCwd;
55
+ let paused = false;
52
56
  let closed = false;
53
57
 
54
58
  function operation(sender, beforeSend, required = false) {
55
- if (closed) throw blocked("Antigravity outbound targets have been closed");
59
+ if (closed || paused) throw blocked("Antigravity outbound targets are closed or quiesced");
56
60
  if (required && typeof beforeSend !== "function") throw blocked("A live Antigravity sender revalidation callback is required");
57
61
  const identity = senderIdentity(sender);
58
62
  const cwd = captureDirectory(identity.cwd);
@@ -60,7 +64,7 @@ export function createAntigravityOutboundTargets(options = {}) {
60
64
  const accounts = readAccounts();
61
65
  const accountContext = requireBridgeAccounts(accounts);
62
66
  const check = async () => {
63
- if (closed) throw blocked("Antigravity outbound targets have been closed");
67
+ if (closed || paused) throw blocked("Antigravity outbound targets are closed or quiesced");
64
68
  assertAccounts(accountContext);
65
69
  roots.recheck(root, "Antigravity sender project");
66
70
  const current = captureDirectory(identity.cwd);
@@ -122,6 +126,29 @@ export function createAntigravityOutboundTargets(options = {}) {
122
126
  return { ...session, desktop, inbound: readInbound(session.cwd) };
123
127
  }
124
128
 
129
+ function savedClaudeTask(context, tasks, sessionId, taskId) {
130
+ if (!Array.isArray(tasks) || tasks.length > 8192) throw blocked("Saved Claude Desktop tasks cannot be verified");
131
+ const matches = tasks.filter((task) => task.taskId === taskId);
132
+ const task = matches[0];
133
+ if (matches.length !== 1 || typeof taskId !== "string" || !taskId.startsWith("local_") || !UUID.test(taskId.slice(6)) || task.fileTaskId !== taskId || task.isArchived !== false
134
+ || !UUID.test(sessionId ?? "") || task.cliSessionId !== sessionId || !sameDirectory(captureDirectory(task.cwd).path, context.cwd)
135
+ || typeof task.title !== "string" || !task.title.trim() || task.title.length > 1024 || /[\u0000-\u001f\u007f]/u.test(task.title)) throw blocked("Exact saved Claude Desktop task and session identity cannot be verified");
136
+ return { target: "claude", sessionId, taskId, cwd: context.cwd, title: task.title, status: "dormant", recoverable: false, code: "CLAUDE_SESSION_NOT_READY" };
137
+ }
138
+
139
+ async function recoverClaudeRecipient(context, sessionId, taskId) {
140
+ if (listSessions().some((session) => session.sessionId === sessionId && session.alive)) return claudeRecipient(context, sessionId, taskId);
141
+ savedClaudeTask(context, await listTasks(context.accounts.claude), sessionId, taskId);
142
+ await context.check();
143
+ const resolved = await recoverClaudeDesktopSession({
144
+ target: "auto", expectedCwd: context.cwd, expectedTaskId: taskId, sessions: listSessions(), account: context.accounts.claude, readContext,
145
+ }, {
146
+ listTasks: () => listTasks(context.accounts.claude), listSessions, beforeInspect: context.check,
147
+ });
148
+ await context.check();
149
+ return claudeRecipient(context, resolved.session.sessionId, taskId);
150
+ }
151
+
125
152
  async function list({ target, sender, beforeSend } = {}) {
126
153
  const context = operation(sender, beforeSend);
127
154
  await context.check();
@@ -148,9 +175,15 @@ export function createAntigravityOutboundTargets(options = {}) {
148
175
  try {
149
176
  const desktop = readContext(session, context.accounts.claude);
150
177
  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 });
178
+ 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, status: "ready" });
152
179
  } catch {}
153
180
  }
181
+ const tasks = await listTasks(context.accounts.claude);
182
+ if (!Array.isArray(tasks) || tasks.length > 8192) throw blocked("Saved Claude Desktop tasks cannot be verified");
183
+ for (const task of tasks) {
184
+ if (rows.some((row) => row.taskId === task.taskId) || listSessions().some((session) => session.sessionId === task.cliSessionId && session.alive)) continue;
185
+ try { rows.push(savedClaudeTask(context, tasks, task.cliSessionId, task.taskId)); } catch {}
186
+ }
154
187
  await context.check();
155
188
  return rows;
156
189
  }
@@ -178,9 +211,12 @@ export function createAntigravityOutboundTargets(options = {}) {
178
211
  }
179
212
  }
180
213
  if (target !== "claude") throw blocked("Target must be codex or claude");
181
- const recipient = claudeRecipient(context, sessionId, taskId);
214
+ const recipient = await recoverClaudeRecipient(context, sessionId, taskId);
182
215
  assertRecipientClass(recipient, context.identity);
183
- if (!peer) peer = peerFactory({ name: `antigravity-${process.pid}`, cwd: context.cwd, signalExit: false, env, protectSocket: protectCurrentUserPipe });
216
+ if (!peer) {
217
+ peerCwd = context.cwd;
218
+ peer = peerFactory({ name: `antigravity-${process.pid}`, cwd: peerCwd, signalExit: false, env, protectSocket: protectCurrentUserPipe });
219
+ }
184
220
  await peer.start();
185
221
  let msgId;
186
222
  try {
@@ -189,7 +225,7 @@ export function createAntigravityOutboundTargets(options = {}) {
189
225
  recipient: { permissionMode: recipient.desktop.permissionMode ?? null, permissionClass: recipient.desktop.permissionClass ?? null, inboundPolicy: recipient.inbound?.value ?? null },
190
226
  beforeSend: async () => {
191
227
  await context.check();
192
- const current = claudeRecipient(context, sessionId, taskId);
228
+ const current = claudeRecipient(context, recipient.sessionId, taskId);
193
229
  if (!sameClaudeDesktopRecipient(recipient, current)) throw blocked("Claude session process or socket changed before delivery");
194
230
  assertRecipientClass(current, context.identity);
195
231
  await context.check();
@@ -198,7 +234,7 @@ export function createAntigravityOutboundTargets(options = {}) {
198
234
  msgId = outcome.msgId;
199
235
  receipts.set(msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
200
236
  await context.check();
201
- return { target, sessionId, taskId, cwd: context.cwd, msgId, status: outcome.delivery?.status ?? "sent_unconfirmed", completed: false, accountContext: context.accountContext };
237
+ return { target, sessionId: recipient.sessionId, taskId, cwd: context.cwd, msgId, status: outcome.delivery?.status ?? "sent_unconfirmed", completed: false, accountContext: context.accountContext };
202
238
  } catch (error) {
203
239
  if (error.msgId) receipts.set(error.msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
204
240
  if (msgId) error.msgId = msgId;
@@ -221,6 +257,11 @@ export function createAntigravityOutboundTargets(options = {}) {
221
257
  }
222
258
  if (target !== undefined && target !== "claude") throw blocked("Target must be codex or claude");
223
259
  if (msgId === undefined) {
260
+ if (!listSessions().some((session) => session.sessionId === sessionId && session.alive)) {
261
+ const task = savedClaudeTask(context, await listTasks(context.accounts.claude), sessionId, taskId);
262
+ await context.check();
263
+ return task;
264
+ }
224
265
  const recipient = claudeRecipient(context, sessionId, taskId);
225
266
  await context.check();
226
267
  return { target: "claude", sessionId, taskId, cwd: context.cwd, title: recipient.desktop.title ?? null, permissionClass: recipient.desktop.permissionClass ?? null, inboundPolicy: recipient.inbound?.value ?? null };
@@ -240,5 +281,54 @@ export function createAntigravityOutboundTargets(options = {}) {
240
281
  await peer?.closed;
241
282
  }
242
283
 
243
- return { list, send, read, close };
284
+ const reloadReason = () => peer?.reloadReason() ?? null;
285
+
286
+ async function quiesce() {
287
+ const reason = reloadReason();
288
+ if (reason) throw new Error(reason);
289
+ paused = true;
290
+ await peer?.quiesce();
291
+ }
292
+
293
+ function exportReloadState() {
294
+ if (!paused || reloadReason()) throw new Error("Quiesce Antigravity targets before exporting reload state");
295
+ return cloneReloadState({ receipts: [...receipts], peer: peer ? { cwd: peerCwd, state: peer.exportReloadState() } : null });
296
+ }
297
+
298
+ function restoreReloadState(input) {
299
+ if (peer || receipts.size || closed) throw new Error("Antigravity reload state requires empty targets");
300
+ const state = cloneReloadState(input);
301
+ if (!state || !Array.isArray(state.receipts) || state.receipts.length > 100000
302
+ || state.peer !== null && (!state.peer || typeof state.peer.cwd !== "string" || !path.isAbsolute(state.peer.cwd))) {
303
+ throw new Error("Invalid Antigravity target reload state");
304
+ }
305
+ const restored = new Map();
306
+ for (const pair of state.receipts) {
307
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || !pair[0] || restored.has(pair[0])) throw new Error("Invalid Antigravity receipt reload record");
308
+ const record = pair[1];
309
+ const identity = senderIdentity(record?.sender);
310
+ if (typeof record.cwd !== "string" || !path.isAbsolute(record.cwd) || typeof identity.cwd !== "string" || !sameDirectory(record.cwd, identity.cwd)
311
+ || !record.accountContext || typeof record.accountContext.codex !== "string" || !record.accountContext.codex
312
+ || typeof record.accountContext.claude !== "string" || !record.accountContext.claude) throw new Error("Invalid Antigravity receipt ownership reload record");
313
+ restored.set(pair[0], { ...record, sender: identity });
314
+ }
315
+ if (!state.peer && restored.size) throw new Error("Antigravity receipts require their native peer state");
316
+ if (state.peer) {
317
+ const nativeMessages = state.peer.state?.sentMessages;
318
+ if (!Array.isArray(nativeMessages) || nativeMessages.length !== restored.size || nativeMessages.some(([id]) => !restored.has(id))) throw new Error("Antigravity receipt ownership does not match native peer state");
319
+ const candidate = peerFactory({ name: `antigravity-${process.pid}`, cwd: state.peer.cwd, signalExit: false, env, protectSocket: protectCurrentUserPipe });
320
+ candidate.restoreReloadState(state.peer.state);
321
+ peer = candidate;
322
+ peerCwd = state.peer.cwd;
323
+ }
324
+ for (const [id, record] of restored) receipts.set(id, record);
325
+ }
326
+
327
+ async function activate() {
328
+ if (closed) throw new Error("Antigravity targets are closed");
329
+ await peer?.resume();
330
+ paused = false;
331
+ }
332
+
333
+ return { list, send, read, close, reloadReason, quiesce, exportReloadState, restoreReloadState, activate };
244
334
  }
@@ -0,0 +1,192 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { AntigravityCliSession, resolveAntigravityCliBinary } from "./antigravity-cli.mjs";
4
+
5
+ const runFile = promisify(execFile);
6
+ 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;
7
+ const RELAY_TOOLS = ["worker_status", "list_workers", "send_to_worker", "read_worker_delivery"];
8
+ const RELAY_ENV = ["AGENT_RELAY_ENDPOINT", "AGENT_RELAY_TOKEN", "AGENT_RELAY_WORKER_ID", "AGENT_RELAY_REQUEST_ID"];
9
+ const MAX_STDERR_BYTES = 65536;
10
+
11
+ function failure(code, reason, phase, deliveryState = "not_sent") {
12
+ return Object.assign(new Error(reason), { code, phase, deliveryState, sent: deliveryState === "sent" ? true : deliveryState === "not_sent" ? false : null });
13
+ }
14
+
15
+ export async function inspectAntigravityWorker({ env = process.env, executable, platform = process.platform, run = runFile } = {}) {
16
+ const state = { agent: "antigravity", available: false, authenticated: null, ready: false };
17
+ let binary;
18
+ try { binary = resolveAntigravityCliBinary({ binary: executable, env, platform }); }
19
+ catch { return { ...state, code: "ANTIGRAVITY_CLI_UNAVAILABLE", reason: "The dedicated Antigravity CLI executable is unavailable." }; }
20
+ try {
21
+ const result = await run(binary, ["--version"], { env: { ...env }, windowsHide: true, shell: false, timeout: 5000, maxBuffer: 4096, encoding: "utf8" });
22
+ const status = result?.exitCode ?? result?.status ?? result?.code ?? 0;
23
+ const version = String(result?.stdout ?? "").trim();
24
+ if (status !== 0 || !/^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/i.test(version)) throw new Error("Invalid version result");
25
+ return { ...state, available: true, executable: binary, version, code: "ANTIGRAVITY_AUTH_UNVERIFIED", reason: "Antigravity CLI has no supported read-only authentication probe. An authorized job must establish runtime authentication." };
26
+ } catch {
27
+ return { ...state, executable: binary, code: "ANTIGRAVITY_INSPECTION_FAILED", reason: "Antigravity CLI version inspection failed or returned an unsupported response." };
28
+ }
29
+ }
30
+
31
+ function classifyDiagnostic(value) {
32
+ if (typeof value !== "string") return null;
33
+ const text = value.slice(0, 8192).replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ");
34
+ if (/\b(?:unauthenticated|auth(?:entication)?[ _-]+(?:required|failed)|not[ _-]+authenticated|not[ _-]+logged[ _-]+in|(?:login|sign[ -]?in)[ _-]+required|(?:credentials?|tokens?)[ _-]+(?:are[ _-]+|is[ _-]+|has[ _-]+|have[ _-]+)?(?:expired|invalid|missing))\b/i.test(text)) return "authentication";
35
+ if (/\b(?:soft[ _-]+denied|permission[ _-]+(?:denied|required|rejected)|approval[ _-]+required|requires?[ _-]+(?:user[ _-]+)?(?:approval|permission))\b/i.test(text)
36
+ || /\b(?:tool|mcp)\b[^\r\n]{0,160}\b(?:not allowed|blocked|denied)\b/i.test(text)) return "permission";
37
+ return null;
38
+ }
39
+
40
+ function discoverRelayTools(tools) {
41
+ if (tools === undefined) return { relayToolsVerified: false, relayToolDiscovery: "unadvertised" };
42
+ if (!Array.isArray(tools) || tools.length > 4096 || tools.some((name) => typeof name !== "string" || !name || name.length > 256)) {
43
+ throw failure("ANTIGRAVITY_WORKER_PROTOCOL_ERROR", "Antigravity CLI advertised an invalid tool list.", "startup");
44
+ }
45
+ const names = new Set(tools.map((name) => name.replace(/[-_:/\.]+/g, "_")));
46
+ if (RELAY_TOOLS.every((name) => names.has(name) || names.has(`worker_relay_${name}`) || names.has(`mcp_worker_relay_${name}`))) return { relayToolsVerified: true, relayToolDiscovery: "explicit-tools" };
47
+ if (tools.includes("call_mcp_tool")) return { relayToolsVerified: false, relayToolDiscovery: "mcp-dispatcher" };
48
+ throw failure("ANTIGRAVITY_RELAY_TOOLS_MISSING", "Antigravity CLI advertised neither its native MCP dispatcher nor all required worker-relay tools. Inspect the CLI MCP configuration.", "relay");
49
+ }
50
+
51
+ function workerEnvironment(env, relayMcp, requestId) {
52
+ const grant = relayMcp?.env;
53
+ if (typeof relayMcp?.command !== "string" || !relayMcp.command || !Array.isArray(relayMcp.args)
54
+ || relayMcp.args.some((value) => typeof value !== "string") || !grant || typeof grant !== "object" || Array.isArray(grant)
55
+ || RELAY_ENV.some((key) => typeof grant[key] !== "string" || !grant[key] || grant[key].length > 8192 || grant[key].includes("\0"))
56
+ || grant.AGENT_RELAY_REQUEST_ID !== requestId) {
57
+ throw failure("ANTIGRAVITY_RELAY_GRANT_INVALID", "The Antigravity worker requires a complete relay grant bound to this request.", "preflight");
58
+ }
59
+ const cleaned = Object.fromEntries(Object.entries(env).filter(([name]) => !/^(?:CODEX_|CLAUDE|ANTHROPIC_|MCP_|AGENT_RELAY_)/i.test(name)));
60
+ for (const key of RELAY_ENV) cleaned[key] = grant[key];
61
+ return cleaned;
62
+ }
63
+
64
+ export async function runAntigravityWorker({ executable, cwd, prompt, requestId, sessionId, env = process.env, relayMcp, onEvent = () => {}, signal, timeoutMs = 600000, spawn, conversationId, resume } = {}) {
65
+ if (conversationId !== undefined || resume !== undefined) throw failure("ANTIGRAVITY_WORKER_RESUME_UNSUPPORTED", "Background Antigravity jobs require a fresh CLI conversation and cannot resume an external conversation.", "preflight");
66
+ if (typeof prompt !== "string" || !prompt.trim() || !UUID.test(requestId ?? "")
67
+ || sessionId !== undefined && !UUID.test(sessionId) || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1
68
+ || typeof onEvent !== "function") {
69
+ throw failure("ANTIGRAVITY_WORKER_INPUT_INVALID", "The Antigravity worker requires a non-empty prompt, valid request identity, and bounded timeout.", "preflight");
70
+ }
71
+ if (signal?.aborted) throw failure("ANTIGRAVITY_WORKER_ABORTED", "The Antigravity worker was canceled before dispatch.", "cancellation");
72
+ const workerEnv = workerEnvironment(env, relayMcp, requestId);
73
+ const deadline = Date.now() + timeoutMs;
74
+ let session;
75
+ let initialized;
76
+ let accepted = false;
77
+ let terminal = false;
78
+ let diagnosticKind = null;
79
+ let stderrBytes = 0;
80
+ let stderrTail = "";
81
+ let stderrOverflow = false;
82
+ let observerFailed = false;
83
+ let finalError;
84
+ let deadlineTimer;
85
+ let rejectInterruption;
86
+ const interrupted = new Promise((resolve, reject) => { rejectInterruption = reject; });
87
+ interrupted.catch(() => {});
88
+ const observers = [];
89
+ const deliveryState = () => accepted ? "sent" : session && ["unknown", "completed"].includes(session.read().deliveryState) ? "unknown" : "not_sent";
90
+ const notify = (event) => {
91
+ try { observers.push(Promise.resolve(onEvent(event)).catch(() => { observerFailed = true; })); }
92
+ catch { observerFailed = true; }
93
+ };
94
+ const accept = () => {
95
+ if (accepted) return;
96
+ accepted = true;
97
+ notify({ type: "accepted", sessionId: initialized.conversation_id });
98
+ };
99
+ const observeDiagnostic = (value) => {
100
+ const kind = classifyDiagnostic(value);
101
+ if (kind && (!diagnosticKind || kind === "authentication")) diagnosticKind = kind;
102
+ };
103
+ const diagnosticFailure = () => diagnosticKind === "authentication"
104
+ ? failure("ANTIGRAVITY_AUTH_REQUIRED", "Antigravity CLI could not authenticate the background worker. Restore its configured credentials without opening a Desktop chat.", "authentication", deliveryState())
105
+ : diagnosticKind === "permission"
106
+ ? failure("ANTIGRAVITY_PERMISSION_DENIED", "Antigravity CLI denied a required tool operation. Inspect its scoped permission rules; no permissions were changed.", "permission", deliveryState())
107
+ : stderrOverflow ? failure("ANTIGRAVITY_WORKER_DIAGNOSTIC_LIMIT", "Antigravity CLI exceeded its bounded diagnostic limit; successful execution could not be verified.", "observation", deliveryState()) : null;
108
+ const abort = () => {
109
+ if (!terminal) {
110
+ const error = failure("ANTIGRAVITY_WORKER_ABORTED", "The Antigravity worker was canceled. Inspect its existing receipt before further action.", "cancellation", deliveryState());
111
+ session?.fail(error);
112
+ rejectInterruption(error);
113
+ }
114
+ };
115
+ const observe = (promise) => Promise.race([promise, interrupted]);
116
+ try {
117
+ session = new AntigravityCliSession({
118
+ binary: executable, cwd, env: workerEnv, spawn,
119
+ startupTimeoutMs: Math.min(30000, timeoutMs), turnTimeoutMs: timeoutMs, closeTimeoutMs: Math.min(1000, timeoutMs),
120
+ onStderr(chunk) {
121
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
122
+ if (bytes.length > MAX_STDERR_BYTES - stderrBytes) stderrOverflow = true;
123
+ if (stderrBytes >= MAX_STDERR_BYTES) return;
124
+ const text = bytes.subarray(0, MAX_STDERR_BYTES - stderrBytes).toString("utf8");
125
+ stderrBytes += Math.min(bytes.length, MAX_STDERR_BYTES - stderrBytes);
126
+ for (let offset = 0; offset < text.length; offset += 4096) {
127
+ stderrTail = `${stderrTail}${text.slice(offset, offset + 4096)}`.slice(-8192);
128
+ observeDiagnostic(stderrTail);
129
+ }
130
+ },
131
+ onEvent(event) {
132
+ if (event.event === "init") {
133
+ initialized = { conversation_id: event.conversation_id, tools: event.init.tools };
134
+ return;
135
+ }
136
+ if (event.event === "result") {
137
+ accept();
138
+ terminal = true;
139
+ observeDiagnostic(event.result.error);
140
+ return;
141
+ }
142
+ if (event.event !== "step_update") return;
143
+ const step = event.step_update;
144
+ if (!step || step.conversation_id !== initialized?.conversation_id) throw new Error("Invalid Antigravity event");
145
+ if (!session.awaitingResult) throw new Error("Invalid Antigravity event");
146
+ if (step.step_type === "user_input" && step.state === "DONE") accept();
147
+ const error = step.tool_info?.error;
148
+ if (error) observeDiagnostic(typeof error === "string" ? error : `${typeof error.type === "string" ? error.type : ""} ${typeof error.message === "string" ? error.message : ""}`);
149
+ },
150
+ });
151
+ deadlineTimer = setTimeout(() => {
152
+ const error = failure("ANTIGRAVITY_WORKER_TIMEOUT", "The Antigravity worker exceeded its observation deadline. Inspect its existing receipt before further action.", "observation", deliveryState());
153
+ session.fail(error);
154
+ rejectInterruption(error);
155
+ }, Math.max(1, deadline - Date.now()));
156
+ signal?.addEventListener("abort", abort, { once: true });
157
+ if (signal?.aborted) throw failure("ANTIGRAVITY_WORKER_ABORTED", "The Antigravity worker was canceled before dispatch.", "cancellation");
158
+ const started = await observe(session.start());
159
+ if (diagnosticFailure()) throw diagnosticFailure();
160
+ const relayTools = discoverRelayTools(initialized?.tools);
161
+ notify({ type: "session", sessionId: started.conversationId });
162
+ await observe(Promise.all(observers));
163
+ if (observerFailed) throw failure("ANTIGRAVITY_WORKER_OBSERVER_FAILED", "The Antigravity worker could not record its session event.", "observation");
164
+ if (signal?.aborted) throw failure("ANTIGRAVITY_WORKER_ABORTED", "The Antigravity worker was canceled before dispatch.", "cancellation");
165
+ const remaining = deadline - Date.now();
166
+ if (remaining < 1) throw failure("ANTIGRAVITY_WORKER_TIMEOUT", "The Antigravity worker exceeded its observation deadline.", "observation");
167
+ session.turnTimeoutMs = remaining;
168
+ const result = await observe(session.send(prompt));
169
+ await observe(session.close());
170
+ await observe(Promise.all(observers));
171
+ if (observerFailed) throw failure("ANTIGRAVITY_WORKER_OBSERVER_FAILED", "The Antigravity worker could not record its acceptance event.", "observation", deliveryState());
172
+ if (diagnosticFailure()) throw diagnosticFailure();
173
+ if (result.status !== "SUCCESS" || result.error) throw failure("ANTIGRAVITY_WORKER_TURN_FAILED", "Antigravity CLI returned an unsuccessful background turn. Inspect the existing worker receipt.", "execution", deliveryState());
174
+ return { output: result.response, sessionId: result.conversation_id, ...(sessionId ? { workerSessionId: sessionId } : {}), deliveryState: "sent", sent: true, authenticated: true, ...relayTools };
175
+ } catch (error) {
176
+ const state = deliveryState();
177
+ if (/^ANTIGRAVITY_[A-Z_]+$/.test(error?.code ?? "")) finalError = failure(error.code, error.message, error.phase, state);
178
+ else if (diagnosticFailure()) finalError = diagnosticFailure();
179
+ else if (["STARTUP_TIMEOUT", "TURN_TIMEOUT"].includes(error?.code)) finalError = failure("ANTIGRAVITY_WORKER_TIMEOUT", "The Antigravity worker exceeded its observation deadline. Inspect its existing receipt before further action.", "observation", state);
180
+ else if (error?.code === "INVALID_PROTOCOL") finalError = failure("ANTIGRAVITY_WORKER_PROTOCOL_ERROR", "Antigravity CLI returned invalid or mismatched streaming data.", "protocol", state);
181
+ else finalError = failure("ANTIGRAVITY_WORKER_FAILED", "The Antigravity CLI worker could not complete its operation. Inspect its existing receipt before further action.", state === "not_sent" ? "startup" : "execution", state);
182
+ finalError.requestId = requestId;
183
+ if (initialized?.conversation_id) finalError.sessionId = initialized.conversation_id;
184
+ if (sessionId) finalError.workerSessionId = sessionId;
185
+ throw finalError;
186
+ } finally {
187
+ clearTimeout(deadlineTimer);
188
+ signal?.removeEventListener("abort", abort);
189
+ const closed = await session?.close();
190
+ if (!finalError && closed?.status === "closing") throw failure("ANTIGRAVITY_WORKER_STOP_FAILED", "The Antigravity worker did not confirm process shutdown.", "shutdown", deliveryState());
191
+ }
192
+ }