@minhspark/codex-mcp-bridge 1.25.3 → 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 (38) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +45 -20
  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 +26 -14
  11. package/scripts/install-background-relay.mjs +428 -0
  12. package/scripts/install-native-relay.mjs +1 -1
  13. package/scripts/setup-background-relay.mjs +58 -0
  14. package/scripts/worker-relay.mjs +41 -0
  15. package/src/antigravity-bridge.mjs +18 -3
  16. package/src/antigravity-cli.mjs +13 -2
  17. package/src/antigravity-outbound-bridge.mjs +27 -6
  18. package/src/antigravity-outbound-targets.mjs +59 -4
  19. package/src/background-antigravity-worker.mjs +192 -0
  20. package/src/background-claude-worker.mjs +240 -0
  21. package/src/background-codex-worker.mjs +344 -0
  22. package/src/background-relay-client.mjs +107 -0
  23. package/src/background-relay-config.mjs +98 -0
  24. package/src/background-relay-daemon.mjs +60 -0
  25. package/src/background-relay-mcp.mjs +45 -0
  26. package/src/background-relay-protocol.mjs +54 -0
  27. package/src/background-relay-registration.mjs +198 -0
  28. package/src/background-relay-service.mjs +311 -0
  29. package/src/background-relay-storage.mjs +203 -0
  30. package/src/background-relay-storage.ps1 +64 -0
  31. package/src/background-relay.mjs +373 -0
  32. package/src/claude-bridge.mjs +6 -3
  33. package/src/index.mjs +7 -2
  34. package/src/mcp-supervisor.mjs +17 -11
  35. package/src/native-relay-companion.mjs +1 -1
  36. package/src/reload-control.mjs +2 -2
  37. package/src/setup.mjs +26 -9
  38. package/src/windows-pipe-acl.ps1 +1 -1
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { callBackgroundRelay } from "../src/background-relay-client.mjs";
4
+ import { withBridgeDiagnostic } from "../src/recipient-preflight.mjs";
5
+ import { relayFailure, RELAY_UUID } from "../src/background-relay-config.mjs";
6
+ import { exitForVersionRequest } from "../src/cli-version.mjs";
7
+
8
+ exitForVersionRequest(import.meta.url);
9
+ const args = process.argv.slice(2);
10
+ const action = args.shift() ?? "status";
11
+ const option = name => { const index = args.indexOf(name); return index < 0 ? undefined : args[index + 1]; };
12
+ try {
13
+ let result;
14
+ if (action === "status" || action === "list") result = await callBackgroundRelay(action);
15
+ else if (action === "read") result = await callBackgroundRelay("read", { requestId: option("--request-id"), waitSec: Number(option("--wait") ?? 0) }, { timeoutMs: (Number(option("--wait") ?? 0) + 10) * 1000 });
16
+ else if (action === "send") {
17
+ if (!args.includes("--stdin") || !RELAY_UUID.test(option("--request-id") ?? "")) throw relayFailure("WORKER_REQUEST_INVALID", "Use --stdin and a persistent --request-id UUID so uncertain deliveries can be inspected safely");
18
+ const chunks = [];
19
+ let bytes = 0;
20
+ for (;;) {
21
+ const buffer = Buffer.alloc(65536);
22
+ const count = fs.readSync(0, buffer, 0, buffer.length, null);
23
+ if (!count) break;
24
+ bytes += count;
25
+ if (bytes > 256 * 1024) throw relayFailure("WORKER_REQUEST_TOO_LARGE", "The authorized prompt exceeds the worker input limit");
26
+ chunks.push(buffer.subarray(0, count));
27
+ }
28
+ const prompt = Buffer.concat(chunks).toString("utf8");
29
+ result = await callBackgroundRelay("send", { agent: option("--agent"), cwd: option("--cwd"), requestId: option("--request-id"), prompt });
30
+ } else if (action === "--help" || action === "help") {
31
+ process.stdout.write("worker-relay status | list | send --agent <codex|claude|antigravity> --cwd <project> --request-id <uuid> --stdin | read --request-id <uuid> [--wait <0-60>]\n");
32
+ } else throw relayFailure("WORKER_COMMAND_INVALID", "Unknown worker relay command");
33
+ if (result) {
34
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
35
+ if (result.status === "failed" || result.status === "unknown") process.exitCode = 1;
36
+ }
37
+ } catch (error) {
38
+ const result = withBridgeDiagnostic({ isError: true, content: [] }, error, { component: "worker-relay-cli", operation: action, requestId: option("--request-id") });
39
+ process.stderr.write(result.content.map(item => item.text ?? "").join("\n") + "\n");
40
+ process.exitCode = 1;
41
+ }
@@ -15,6 +15,7 @@ import { exitForVersionRequest } from "./cli-version.mjs";
15
15
  import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
16
16
  import { requireBridgeAccounts, readBridgeAccounts, assertAccountIdentity } from "./bridge-account-context.mjs";
17
17
  import { bridgeDiagnostic, withBridgeDiagnostic, preflightFailure } from "./recipient-preflight.mjs";
18
+ import { createReloadControl, exitWhenClientCloses } from "./reload-control.mjs";
18
19
 
19
20
  exitForVersionRequest(import.meta.url);
20
21
 
@@ -28,6 +29,19 @@ const caller = process.env.ANTIGRAVITY_BRIDGE_CALLER;
28
29
  const senderRelay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() });
29
30
  const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
30
31
  const schema = { requestId: z.string().uuid(), cwd: z.string().min(1), prompt: z.string().min(1).max(65536) };
32
+ const reloadReason = () => startingSessions ? "Antigravity CLI sessions are still starting"
33
+ : deliveries.active.size ? "Antigravity deliveries are still active"
34
+ : sessions.size ? "Open Antigravity CLI sessions retain process pipes and cannot migrate" : null;
35
+ const reload = createReloadControl({
36
+ entry: "antigravity-bridge.mjs",
37
+ inspect: reloadReason,
38
+ exportState: () => ({ caller: caller ?? null, directory }),
39
+ restore: (state) => {
40
+ if (state.caller !== (caller ?? null) || typeof state.directory !== "string" || path.relative(state.directory, directory)) {
41
+ throw new Error("Antigravity caller or durable receipt directory changed during reload");
42
+ }
43
+ },
44
+ });
31
45
 
32
46
  async function identifySender(extra) {
33
47
  if (caller === "codex") {
@@ -89,7 +103,7 @@ function register(name, description, inputSchema, readOnly, handler) {
89
103
  server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
90
104
  const options = { component: "antigravity-bridge", operation: name, requestId: args.requestId, conversationId: args.conversationId, readOnly };
91
105
  try {
92
- const value = await handler(args, extra);
106
+ const value = await reload.run(() => handler(args, extra));
93
107
  const failure = antigravityDeliveryError(value);
94
108
  if (failure) return withBridgeDiagnostic({ ...result(value), isError: true }, failure, { ...options, readOnly: false, msgId: value.result?.msgId });
95
109
  if (value?.desktop?.available === false) return withBridgeDiagnostic({ ...result(value), isError: true }, { diagnostic: value.desktop.diagnostic }, options);
@@ -219,6 +233,7 @@ async function stop() {
219
233
  await stopProcessInspectors();
220
234
  await server.close();
221
235
  }
222
- for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
223
- process.stdin.once("end", () => { void stop(); });
224
236
  await server.connect(new StdioServerTransport());
237
+ const shutdown = exitWhenClientCloses({ busy: () => startingSessions || deliveries.active.size ? "Antigravity deliveries are still active" : null, stop });
238
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void shutdown.close(signal); });
239
+ reload.listen();
@@ -50,6 +50,8 @@ export class AntigravityCliSession {
50
50
  this.env = { ...(options.env ?? process.env) };
51
51
  this.binary = resolveAntigravityCliBinary({ binary: options.binary, env: this.env });
52
52
  this.spawn = options.spawn ?? spawnProcess;
53
+ this.onEvent = options.onEvent;
54
+ this.onStderr = options.onStderr;
53
55
  this.requestedConversationId = options.conversationId ?? null;
54
56
  if (this.requestedConversationId !== null && !UUID.test(this.requestedConversationId)) throw new Error("Invalid Antigravity conversation ID");
55
57
  this.startupTimeoutMs = positive(options.startupTimeoutMs, 30000, "startupTimeoutMs");
@@ -102,7 +104,11 @@ export class AntigravityCliSession {
102
104
  this.child.stdin.on("error", () => this.fail(failure("Antigravity CLI input pipe failed", "STDIN_ERROR", this.pending ? "unknown" : this.deliveryState)));
103
105
  this.child.stdout.on("error", () => this.fail(failure("Antigravity CLI output pipe failed", "STDOUT_ERROR", this.pending ? "unknown" : this.deliveryState)));
104
106
  this.child.stderr.on("error", () => this.fail(failure("Antigravity CLI diagnostic pipe failed", "STDERR_ERROR", this.pending ? "unknown" : this.deliveryState)));
105
- this.child.stderr.resume();
107
+ if (this.onStderr) this.child.stderr.on("data", (chunk) => {
108
+ try { this.onStderr(chunk); }
109
+ catch { this.fail(failure("Antigravity CLI diagnostic observer failed", "OBSERVER_ERROR", this.pending ? "unknown" : this.deliveryState)); }
110
+ });
111
+ else this.child.stderr.resume();
106
112
  this.child.stdout.on("data", (chunk) => this.consume(chunk));
107
113
  this.child.on("close", (code, signal) => {
108
114
  this.exited = true;
@@ -147,6 +153,7 @@ export class AntigravityCliSession {
147
153
  this.conversationId = event.conversation_id;
148
154
  this.permissionMode = event.init.permission_mode;
149
155
  this.status = "idle";
156
+ this.onEvent?.(structuredClone(event));
150
157
  clearTimeout(this.startTimer);
151
158
  this.startResolve(this.read());
152
159
  this.startResolve = null;
@@ -154,11 +161,15 @@ export class AntigravityCliSession {
154
161
  return;
155
162
  }
156
163
  if (!this.conversationId) throw new Error("Invalid Antigravity event");
157
- if (event.event !== "result") return;
164
+ if (event.event !== "result") {
165
+ this.onEvent?.(structuredClone(event));
166
+ return;
167
+ }
158
168
  const result = event.result;
159
169
  if (!this.awaitingResult || !result || result.conversation_id !== this.conversationId
160
170
  || typeof result.status !== "string" || !/^[A-Z][A-Z_]{0,127}$/.test(result.status)
161
171
  || typeof result.response !== "string" || result.error !== undefined && typeof result.error !== "string") throw new Error("Invalid Antigravity event");
172
+ this.onEvent?.(structuredClone(event));
162
173
  this.lastResult = { conversation_id: result.conversation_id, status: result.status, response: result.response, ...(result.error === undefined ? {} : { error: result.error }) };
163
174
  this.awaitingResult = false;
164
175
  this.deliveryState = "completed";
@@ -12,16 +12,36 @@ 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";
16
17
  import { bridgeDiagnostic, withBridgeDiagnostic, preflightFailure } from "./recipient-preflight.mjs";
18
+ import { createReloadControl, exitWhenClientCloses } from "./reload-control.mjs";
17
19
 
18
20
  const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
19
21
  const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
20
22
  const targetSchema = { target: z.enum(["codex", "claude"]) };
21
23
  const taskSchema = { ...targetSchema, conversationId: z.string().uuid(), taskId: z.string().min(1).max(256).optional() };
22
24
 
23
- export function createAntigravityOutboundBridge({ resolveSender = resolveAntigravitySenderContext, targets = createAntigravityOutboundTargets(), deliveries = new AntigravityDeliveries(path.join(antigravityBridgeDirectory(), "outbound-deliveries")), rootPolicy = createHardenedRootPolicy() } = {}) {
24
- 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. 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." });
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 });
25
45
 
26
46
  function scope(expected, actual) {
27
47
  try {
@@ -64,7 +84,7 @@ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigra
64
84
  server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
65
85
  const options = { component: "agent-bridge", operation: name, requestId: args.requestId, conversationId: args.conversationId, readOnly };
66
86
  try {
67
- const value = await handler(args, extra);
87
+ const value = await reload.run(() => handler(args, extra));
68
88
  const failure = antigravityDeliveryError(value);
69
89
  if (failure) return withBridgeDiagnostic({ ...result(value), isError: true }, failure, { ...options, readOnly: false, msgId: value.result?.msgId });
70
90
  if (value?.diagnostic && (value.nativeReceipt === null || value.sender?.status !== "verified")) return withBridgeDiagnostic({ ...result(value), isError: true }, { diagnostic: value.diagnostic }, options);
@@ -123,7 +143,8 @@ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigra
123
143
  return targets.read({ ...args, cwd: identity.cwd, sender: identity, ...(args.target === "codex" ? { threadId: args.conversationId } : { sessionId: args.conversationId }), beforeSend: recheck(identity, extra) });
124
144
  });
125
145
 
126
- 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(); } };
127
148
  }
128
149
 
129
150
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -136,7 +157,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
136
157
  await bridge.close();
137
158
  await stopProcessInspectors();
138
159
  };
139
- for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
140
- process.stdin.once("end", () => { void stop(); });
141
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); });
142
163
  }
@@ -9,6 +9,7 @@ import { assertRecipientClass } from "./recipient-preflight.mjs";
9
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
 
@@ -50,10 +51,12 @@ export function createAntigravityOutboundTargets(options = {}) {
50
51
  const peerFactory = options.peerFactory ?? ((settings) => new PeerEndpoint(settings));
51
52
  const receipts = new Map();
52
53
  let peer;
54
+ let peerCwd;
55
+ let paused = false;
53
56
  let closed = false;
54
57
 
55
58
  function operation(sender, beforeSend, required = false) {
56
- if (closed) throw blocked("Antigravity outbound targets have been closed");
59
+ if (closed || paused) throw blocked("Antigravity outbound targets are closed or quiesced");
57
60
  if (required && typeof beforeSend !== "function") throw blocked("A live Antigravity sender revalidation callback is required");
58
61
  const identity = senderIdentity(sender);
59
62
  const cwd = captureDirectory(identity.cwd);
@@ -61,7 +64,7 @@ export function createAntigravityOutboundTargets(options = {}) {
61
64
  const accounts = readAccounts();
62
65
  const accountContext = requireBridgeAccounts(accounts);
63
66
  const check = async () => {
64
- if (closed) throw blocked("Antigravity outbound targets have been closed");
67
+ if (closed || paused) throw blocked("Antigravity outbound targets are closed or quiesced");
65
68
  assertAccounts(accountContext);
66
69
  roots.recheck(root, "Antigravity sender project");
67
70
  const current = captureDirectory(identity.cwd);
@@ -210,7 +213,10 @@ export function createAntigravityOutboundTargets(options = {}) {
210
213
  if (target !== "claude") throw blocked("Target must be codex or claude");
211
214
  const recipient = await recoverClaudeRecipient(context, sessionId, taskId);
212
215
  assertRecipientClass(recipient, context.identity);
213
- 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
+ }
214
220
  await peer.start();
215
221
  let msgId;
216
222
  try {
@@ -275,5 +281,54 @@ export function createAntigravityOutboundTargets(options = {}) {
275
281
  await peer?.closed;
276
282
  }
277
283
 
278
- 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 };
279
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
+ }