@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.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +58 -0
- package/README.md +81 -32
- package/docs/new-macos-setup.md +47 -21
- package/docs/new-windows-setup.md +34 -9
- package/docs/project-onboarding.md +21 -8
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/scripts/background-relay-hidden.vbs +11 -0
- package/scripts/install-antigravity-bridge.mjs +65 -23
- package/scripts/install-background-relay.mjs +428 -0
- package/scripts/install-claude-code.mjs +2 -2
- package/scripts/install-claude-desktop.mjs +12 -7
- package/scripts/install-native-relay.mjs +2 -2
- package/scripts/setup-background-relay.mjs +58 -0
- package/scripts/setup.mjs +3 -3
- package/scripts/worker-relay.mjs +41 -0
- package/src/antigravity-bridge.mjs +96 -27
- package/src/antigravity-cli.mjs +13 -2
- package/src/antigravity-deliveries.mjs +32 -8
- package/src/antigravity-ipc.mjs +18 -9
- package/src/antigravity-outbound-bridge.mjs +71 -23
- package/src/antigravity-outbound-targets.mjs +100 -10
- package/src/background-antigravity-worker.mjs +192 -0
- package/src/background-claude-worker.mjs +240 -0
- package/src/background-codex-worker.mjs +344 -0
- package/src/background-relay-client.mjs +107 -0
- package/src/background-relay-config.mjs +98 -0
- package/src/background-relay-daemon.mjs +60 -0
- package/src/background-relay-mcp.mjs +45 -0
- package/src/background-relay-protocol.mjs +54 -0
- package/src/background-relay-registration.mjs +198 -0
- package/src/background-relay-service.mjs +311 -0
- package/src/background-relay-storage.mjs +203 -0
- package/src/background-relay-storage.ps1 +64 -0
- package/src/background-relay.mjs +373 -0
- package/src/bridge-readiness.mjs +2 -2
- package/src/claude-bridge.mjs +43 -25
- package/src/claude-code-registration.mjs +2 -1
- package/src/claude-session-router.mjs +8 -15
- package/src/index.mjs +39 -22
- package/src/mcp-supervisor.mjs +26 -16
- package/src/native-relay-companion.mjs +4 -1
- package/src/native-relay.mjs +5 -1
- package/src/peer-protocol.mjs +9 -6
- package/src/recipient-preflight.mjs +81 -0
- package/src/reload-control.mjs +2 -2
- package/src/setup.mjs +62 -25
- package/src/thread-delivery.mjs +10 -5
- 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
|
+
}
|
|
@@ -9,11 +9,13 @@ import { readClaudeSenderContext, requireClaudeSenderContext, stopProcessInspect
|
|
|
9
9
|
import { readClaudeAccountContext } from "./desktop-account-context.mjs";
|
|
10
10
|
import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
11
11
|
import { antigravityBridgeDirectory, callAntigravityDesktop } from "./antigravity-ipc.mjs";
|
|
12
|
-
import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
|
|
12
|
+
import { AntigravityDeliveries, antigravityDeliveryError } from "./antigravity-deliveries.mjs";
|
|
13
13
|
import { AntigravityCliSession } from "./antigravity-cli.mjs";
|
|
14
14
|
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
|
+
import { bridgeDiagnostic, withBridgeDiagnostic, preflightFailure } from "./recipient-preflight.mjs";
|
|
18
|
+
import { createReloadControl, exitWhenClientCloses } from "./reload-control.mjs";
|
|
17
19
|
|
|
18
20
|
exitForVersionRequest(import.meta.url);
|
|
19
21
|
|
|
@@ -27,8 +29,21 @@ const caller = process.env.ANTIGRAVITY_BRIDGE_CALLER;
|
|
|
27
29
|
const senderRelay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() });
|
|
28
30
|
const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
29
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
|
+
});
|
|
30
45
|
|
|
31
|
-
async function
|
|
46
|
+
async function identifySender(extra) {
|
|
32
47
|
if (caller === "codex") {
|
|
33
48
|
const context = await resolveCodexSenderContext(extra?._meta, { inspectDesktopTask: async (threadId) => {
|
|
34
49
|
const accounts = requireBridgeAccounts(readBridgeAccounts());
|
|
@@ -46,17 +61,34 @@ async function sender(extra) {
|
|
|
46
61
|
throw new Error("Install this bridge for Codex or Claude; unknown callers cannot read conversations or send work");
|
|
47
62
|
}
|
|
48
63
|
|
|
64
|
+
async function sender(extra) {
|
|
65
|
+
try { return await identifySender(extra); }
|
|
66
|
+
catch (error) {
|
|
67
|
+
error.code ??= "ANTIGRAVITY_SENDER_UNVERIFIED";
|
|
68
|
+
error.phase ??= "sender";
|
|
69
|
+
error.deliveryState = "not_sent";
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
function sameCwd(expected, actual) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
try {
|
|
76
|
+
if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("Expected an absolute project directory");
|
|
77
|
+
const left = fs.realpathSync.native(expected);
|
|
78
|
+
const right = fs.realpathSync.native(actual);
|
|
79
|
+
if (path.relative(left, right)) throw new Error("Antigravity destination must match the verified sender's project directory");
|
|
80
|
+
rootPolicy.assert(left, "Antigravity project");
|
|
81
|
+
return left;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
error.code ??= "ANTIGRAVITY_PROJECT_SCOPE_INVALID";
|
|
84
|
+
error.phase ??= "scope";
|
|
85
|
+
error.deliveryState = "not_sent";
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
function owns(record, identity) {
|
|
59
|
-
if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw
|
|
91
|
+
if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw preflightFailure("ANTIGRAVITY_RECEIPT_OWNER_MISMATCH", "Delivery does not belong to this sending conversation");
|
|
60
92
|
sameCwd(identity.cwd, record.cwd);
|
|
61
93
|
return record;
|
|
62
94
|
}
|
|
@@ -65,20 +97,49 @@ function handoff(prompt, identity) {
|
|
|
65
97
|
return `[Agent handoff from ${identity.provider} conversation ${identity.id}. Native Antigravity permissions remain in effect.]\n\n${prompt}`;
|
|
66
98
|
}
|
|
67
99
|
|
|
68
|
-
const server = new McpServer({ name: "antigravity-bridge", version }, { instructions: "Send authorized work to Antigravity Desktop through its native sidecar or to an explicitly created CLI streaming session. Always select the requested surface; never substitute CLI for Desktop. The exact destination directory must match the verified sending conversation. Desktop delivery acceptance is not an agent reply. CLI sessions are bridge-owned headless sessions, not attached interactive terminals. Preserve requestId across retries and read_antigravity_delivery after pending or unknown outcomes; never resend automatically. Native tool approvals remain in force.
|
|
100
|
+
const server = new McpServer({ name: "antigravity-bridge", version }, { instructions: "Send authorized work to Antigravity Desktop through its native sidecar or to an explicitly created CLI streaming session. Always select the requested surface; never substitute CLI for Desktop. The exact destination directory must match the verified sending conversation. Desktop delivery acceptance is not an agent reply. CLI sessions are bridge-owned headless sessions, not attached interactive terminals. Preserve requestId across retries and read_antigravity_delivery after pending or unknown outcomes; never resend automatically. Native tool approvals remain in force. Antigravity Desktop can send authorized handoffs back to existing Codex Desktop tasks and live Claude Desktop Code sessions in the same project through its separate agent-bridge MCP server. From Antigravity, verify agent_bridge_status, discover list_agent_tasks, then use send_to_agent_task and read_agent_delivery. This inbound server does not initiate those outbound calls, and its status does not verify the outbound server's readiness. Antigravity CLI outbound and automatic App reply forwarding are not supported. An unreachable sidecar does not prove that the Antigravity App is closed; report its diagnostic and inspect sidecar configuration. Never use Computer Use, open an old chat, or create an empty, new, or error chat to load or recover a relay." });
|
|
69
101
|
|
|
70
102
|
function register(name, description, inputSchema, readOnly, handler) {
|
|
71
103
|
server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
const options = { component: "antigravity-bridge", operation: name, requestId: args.requestId, conversationId: args.conversationId, readOnly };
|
|
105
|
+
try {
|
|
106
|
+
const value = await reload.run(() => handler(args, extra));
|
|
107
|
+
const failure = antigravityDeliveryError(value);
|
|
108
|
+
if (failure) return withBridgeDiagnostic({ ...result(value), isError: true }, failure, { ...options, readOnly: false, msgId: value.result?.msgId });
|
|
109
|
+
if (value?.desktop?.available === false) return withBridgeDiagnostic({ ...result(value), isError: true }, { diagnostic: value.desktop.diagnostic }, options);
|
|
110
|
+
return result(value);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const diagnostic = bridgeDiagnostic(error, options);
|
|
113
|
+
return withBridgeDiagnostic({ ...result({ error: diagnostic.reason, code: diagnostic.code, deliveryState: error.deliveryState ?? diagnostic.deliveryState }), isError: true }, { diagnostic }, options);
|
|
114
|
+
}
|
|
74
115
|
});
|
|
75
116
|
}
|
|
76
117
|
|
|
77
|
-
register("antigravity_bridge_status", "Inspect
|
|
118
|
+
register("antigravity_bridge_status", "Inspect native sidecar connectivity and supported inbound/outbound routes without sending prompts. Outbound readiness must be checked with agent_bridge_status inside Antigravity Desktop.", {}, true, async () => {
|
|
78
119
|
let desktop;
|
|
79
120
|
try { desktop = await callAntigravityDesktop({ operation: "status" }, { timeoutMs: 5000 }); }
|
|
80
|
-
catch
|
|
81
|
-
|
|
121
|
+
catch (cause) {
|
|
122
|
+
const error = Object.assign(new Error("The native Antigravity sidecar could not be reached. This does not prove that the App is closed. Inspect its codex-bridge sidecar configuration and include the diagnostic in the report; do not open a chat to load the relay."), { code: "ANTIGRAVITY_SIDECAR_UNAVAILABLE", phase: cause.phase ?? "connect", cause });
|
|
123
|
+
const diagnostic = bridgeDiagnostic(error, { component: "antigravity-bridge", operation: "antigravity_bridge_status", readOnly: true });
|
|
124
|
+
desktop = { available: false, code: diagnostic.code, reason: diagnostic.reason, diagnostic };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
version,
|
|
128
|
+
caller: caller ?? "unconfigured",
|
|
129
|
+
desktop,
|
|
130
|
+
cli: { transport: "stream-json", activeSessions: sessions.size, permissionPolicy: "native-unchanged", interactiveTerminalAttachment: false },
|
|
131
|
+
outbound: {
|
|
132
|
+
supported: true,
|
|
133
|
+
server: "agent-bridge",
|
|
134
|
+
surface: "desktop",
|
|
135
|
+
targets: ["codex-desktop", "claude-desktop-code"],
|
|
136
|
+
scope: "existing-conversations-in-the-same-project",
|
|
137
|
+
readiness: "not_checked",
|
|
138
|
+
statusTool: "agent_bridge_status",
|
|
139
|
+
cliSupported: false,
|
|
140
|
+
automaticReplyForwarding: false,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
82
143
|
});
|
|
83
144
|
|
|
84
145
|
register("list_antigravity_conversations", "List native Desktop conversations and bridge-owned CLI sessions in the sending project.", { surface: z.enum(["desktop", "cli"]) }, true, async ({ surface }, extra) => {
|
|
@@ -94,28 +155,29 @@ register("send_to_antigravity_conversation", "Send one authorized prompt to an e
|
|
|
94
155
|
const identity = await sender(extra);
|
|
95
156
|
const cwd = sameCwd(identity.cwd, args.cwd);
|
|
96
157
|
const prompt = handoff(args.prompt, identity);
|
|
97
|
-
if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw
|
|
158
|
+
if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw preflightFailure("ANTIGRAVITY_PROMPT_INVALID", "Desktop handoff must fit within 30000 characters and contain no NUL");
|
|
98
159
|
if (args.surface === "desktop") return callAntigravityDesktop({ operation: "send", ...args, cwd, prompt, sender: identity });
|
|
99
160
|
const entry = sessions.get(args.conversationId);
|
|
100
|
-
if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw
|
|
161
|
+
if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender and bridge process");
|
|
101
162
|
sameCwd(cwd, entry.session.read().cwd);
|
|
102
163
|
return deliveries.dispatch({ ...args, cwd, sender: identity, prompt }, async () => {
|
|
103
164
|
const current = await sender(extra);
|
|
104
165
|
sameCwd(cwd, current.cwd);
|
|
105
|
-
if (current.id !== identity.id) throw
|
|
166
|
+
if (current.id !== identity.id) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
106
167
|
return entry.session.send(prompt);
|
|
107
|
-
});
|
|
168
|
+
}, { component: "antigravity-bridge", operation: "send_to_antigravity_conversation" });
|
|
108
169
|
});
|
|
109
170
|
|
|
110
171
|
register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity CLI streaming session for explicitly authorized new work; sends the initial prompt and returns a durable delivery receipt.", schema, false, async (args, extra) => {
|
|
111
172
|
const identity = await sender(extra);
|
|
112
173
|
const cwd = sameCwd(identity.cwd, args.cwd);
|
|
113
|
-
if (sessions.size + startingSessions >= 4) throw
|
|
174
|
+
if (sessions.size + startingSessions >= 4) throw preflightFailure("ANTIGRAVITY_SESSION_CAPACITY", "At most four CLI sessions may be open in one bridge");
|
|
114
175
|
return deliveries.dispatch({ ...args, cwd, sender: identity, surface: "cli" }, async () => {
|
|
115
|
-
if (sessions.size + startingSessions >= 4) throw
|
|
176
|
+
if (sessions.size + startingSessions >= 4) throw preflightFailure("ANTIGRAVITY_SESSION_CAPACITY", "CLI session capacity reached");
|
|
116
177
|
startingSessions++;
|
|
117
178
|
let reserved = true;
|
|
118
179
|
let session;
|
|
180
|
+
let sending = false;
|
|
119
181
|
try {
|
|
120
182
|
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^(?:CODEX_|CLAUDE|ANTHROPIC_|MCP_)/i.test(name)));
|
|
121
183
|
session = new AntigravityCliSession({ cwd, env });
|
|
@@ -126,9 +188,15 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
126
188
|
reserved = false;
|
|
127
189
|
const current = await sender(extra);
|
|
128
190
|
sameCwd(cwd, current.cwd);
|
|
129
|
-
if (current.id !== identity.id) throw
|
|
191
|
+
if (current.id !== identity.id) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
192
|
+
sending = true;
|
|
130
193
|
return await session.send(handoff(args.prompt, identity));
|
|
131
194
|
} catch (error) {
|
|
195
|
+
if (!sending) {
|
|
196
|
+
error.code ??= "ANTIGRAVITY_CLI_START_FAILED";
|
|
197
|
+
error.phase ??= "startup";
|
|
198
|
+
error.deliveryState = "not_sent";
|
|
199
|
+
}
|
|
132
200
|
if (error.deliveryState !== "unknown") {
|
|
133
201
|
for (const [id, entry] of sessions) if (entry.session === session) sessions.delete(id);
|
|
134
202
|
await session?.close();
|
|
@@ -137,7 +205,7 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
137
205
|
} finally {
|
|
138
206
|
if (reserved) startingSessions--;
|
|
139
207
|
}
|
|
140
|
-
});
|
|
208
|
+
}, { component: "antigravity-bridge", operation: "start_antigravity_cli_session" });
|
|
141
209
|
});
|
|
142
210
|
|
|
143
211
|
register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means the native API accepted it, not that the agent completed a turn.", { surface: z.enum(["desktop", "cli"]), requestId: z.string().uuid() }, true, async (args, extra) => {
|
|
@@ -149,9 +217,9 @@ register("read_antigravity_delivery", "Read the original receipt without resendi
|
|
|
149
217
|
register("close_antigravity_cli_session", "Close an idle bridge-owned CLI session belonging to the caller.", { conversationId: z.string().uuid() }, false, async ({ conversationId }, extra) => {
|
|
150
218
|
const identity = await sender(extra);
|
|
151
219
|
const entry = sessions.get(conversationId);
|
|
152
|
-
if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw
|
|
220
|
+
if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender");
|
|
153
221
|
sameCwd(identity.cwd, entry.session.read().cwd);
|
|
154
|
-
if (["running", "starting"].includes(entry.session.read().status)) throw
|
|
222
|
+
if (["running", "starting"].includes(entry.session.read().status)) throw preflightFailure("ANTIGRAVITY_SESSION_BUSY", "Wait for the active CLI delivery before closing");
|
|
155
223
|
const closed = await entry.session.close();
|
|
156
224
|
if (closed.status !== "closing") sessions.delete(conversationId);
|
|
157
225
|
return { closed: closed.status !== "closing", conversationId, status: closed.status };
|
|
@@ -165,6 +233,7 @@ async function stop() {
|
|
|
165
233
|
await stopProcessInspectors();
|
|
166
234
|
await server.close();
|
|
167
235
|
}
|
|
168
|
-
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
|
|
169
|
-
process.stdin.once("end", () => { void stop(); });
|
|
170
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();
|
package/src/antigravity-cli.mjs
CHANGED
|
@@ -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.
|
|
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")
|
|
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";
|
|
@@ -2,9 +2,19 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ensureBridgeDirectory } from "./antigravity-ipc.mjs";
|
|
5
|
+
import { bridgeDiagnostic } from "./recipient-preflight.mjs";
|
|
5
6
|
|
|
6
7
|
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
8
|
|
|
9
|
+
export function antigravityDeliveryError(record) {
|
|
10
|
+
if (!record || !UUID.test(record.requestId) || !["failed", "unknown"].includes(record.status)) return null;
|
|
11
|
+
return Object.assign(new Error(record.reason ?? "The original Antigravity delivery did not complete successfully"), {
|
|
12
|
+
code: record.diagnostic?.code ?? (record.status === "unknown" ? "ANTIGRAVITY_DELIVERY_UNKNOWN" : "ANTIGRAVITY_DELIVERY_FAILED"),
|
|
13
|
+
deliveryState: record.diagnostic?.deliveryState ?? "unknown",
|
|
14
|
+
diagnostic: record.diagnostic,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export class AntigravityDeliveries {
|
|
9
19
|
constructor(directory) {
|
|
10
20
|
this.directory = ensureBridgeDirectory(directory);
|
|
@@ -39,7 +49,11 @@ export class AntigravityDeliveries {
|
|
|
39
49
|
record = JSON.parse(buffer.subarray(0, bytes).toString("utf8"));
|
|
40
50
|
} finally { fs.closeSync(descriptor); }
|
|
41
51
|
if (record.requestId !== requestId) throw new Error("Delivery identity mismatch");
|
|
42
|
-
if (record.status === "pending" && !this.active.has(requestId))
|
|
52
|
+
if (record.status === "pending" && !this.active.has(requestId)) {
|
|
53
|
+
const error = Object.assign(new Error("This process cannot observe the original worker. Inspect the original delivery receipt; do not resend or open a chat to load the relay."), { code: "ANTIGRAVITY_WORKER_UNAVAILABLE", phase: "receipt", deliveryState: "unknown" });
|
|
54
|
+
const diagnostic = bridgeDiagnostic(error, { component: "antigravity-deliveries", operation: record.operation ?? "send", requestId, conversationId: record.conversationId });
|
|
55
|
+
return { ...record, status: "unknown", reason: diagnostic.reason, diagnostic };
|
|
56
|
+
}
|
|
43
57
|
return record;
|
|
44
58
|
}
|
|
45
59
|
|
|
@@ -56,28 +70,38 @@ export class AntigravityDeliveries {
|
|
|
56
70
|
fs.renameSync(temporary, this.file(record.requestId));
|
|
57
71
|
}
|
|
58
72
|
|
|
59
|
-
async dispatch(request, operation) {
|
|
73
|
+
async dispatch(request, operation, diagnosticOptions = {}) {
|
|
60
74
|
const { requestId, ...identity } = request;
|
|
61
75
|
const digest = crypto.createHash("sha256").update(JSON.stringify(identity)).digest("hex");
|
|
62
76
|
const existing = this.read(requestId);
|
|
63
77
|
if (existing) {
|
|
64
|
-
if (existing.digest !== digest) throw new Error("requestId was already used for a different delivery");
|
|
78
|
+
if (existing.digest !== digest) throw Object.assign(new Error("requestId was already used for a different delivery; inspect the original receipt before further action"), { code: "ANTIGRAVITY_REQUEST_ID_CONFLICT", phase: "validation", deliveryState: "not_sent" });
|
|
65
79
|
return existing;
|
|
66
80
|
}
|
|
67
|
-
const record = { requestId, digest, status: "pending", cwd: request.cwd, sender: request.sender, conversationId: request.conversationId ?? null, surface: request.surface, createdAt: new Date().toISOString() };
|
|
81
|
+
const record = { requestId, digest, status: "pending", operation: diagnosticOptions.operation ?? "send", cwd: request.cwd, sender: request.sender, conversationId: request.conversationId ?? null, surface: request.surface, createdAt: new Date().toISOString() };
|
|
82
|
+
const diagnose = (error) => bridgeDiagnostic(error, { component: "antigravity-deliveries", operation: record.operation, ...diagnosticOptions, requestId, conversationId: record.conversationId, msgId: error.msgId });
|
|
68
83
|
try { fs.writeFileSync(this.file(requestId), JSON.stringify(record), { flag: "wx", mode: 0o600 }); }
|
|
69
84
|
catch (error) {
|
|
70
|
-
if (error.code !== "EEXIST")
|
|
71
|
-
|
|
85
|
+
if (error.code !== "EEXIST") {
|
|
86
|
+
error.deliveryState = "not_sent";
|
|
87
|
+
error.phase = "receipt";
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
throw Object.assign(new Error("Delivery is owned by another process. Inspect requestId; do not resend."), { code: "ANTIGRAVITY_DELIVERY_IN_PROGRESS", phase: "receipt", deliveryState: "unknown" });
|
|
72
91
|
}
|
|
73
92
|
this.active.add(requestId);
|
|
74
93
|
void Promise.resolve().then(operation).then((result) => {
|
|
75
94
|
record.status = ["accepted", "sent_unconfirmed", "held", "refused"].includes(result?.status) ? result.status : result?.status && result.status !== "SUCCESS" ? "failed" : "completed";
|
|
76
95
|
record.result = result;
|
|
77
96
|
record.conversationId = result?.conversationId ?? result?.conversation_id ?? record.conversationId;
|
|
97
|
+
if (record.status === "failed") {
|
|
98
|
+
record.diagnostic = diagnose(Object.assign(new Error(typeof result.error === "string" ? result.error : "The native Antigravity delivery failed"), { code: result.status, deliveryState: result.deliveryState, sent: result.sent }));
|
|
99
|
+
record.reason = record.diagnostic.reason;
|
|
100
|
+
}
|
|
78
101
|
}, (error) => {
|
|
79
|
-
record.
|
|
80
|
-
record.
|
|
102
|
+
record.diagnostic = diagnose(error);
|
|
103
|
+
record.status = record.diagnostic.deliveryState === "not_sent" ? "failed" : "unknown";
|
|
104
|
+
record.reason = record.diagnostic.reason;
|
|
81
105
|
if (typeof error.msgId === "string" && UUID.test(error.msgId)) record.result = { msgId: error.msgId, completed: false };
|
|
82
106
|
if (error.accountContext) record.result = { ...record.result, accountContext: error.accountContext };
|
|
83
107
|
}).finally(() => {
|
package/src/antigravity-ipc.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
|
|
7
7
|
import { relayFileIdentity, removeRelayFile } from "./native-relay-files.mjs";
|
|
8
|
+
import { bridgeDiagnostic } from "./recipient-preflight.mjs";
|
|
8
9
|
|
|
9
10
|
const LIMIT = 2 * 1024 * 1024;
|
|
10
11
|
|
|
@@ -55,28 +56,30 @@ export function callAntigravityDesktop(request, { directory = antigravityBridgeD
|
|
|
55
56
|
socket.destroy();
|
|
56
57
|
if (error) {
|
|
57
58
|
error.deliveryState ??= dispatched ? "unknown" : "not_sent";
|
|
59
|
+
error.phase ??= dispatched ? "dispatch" : "connect";
|
|
60
|
+
error.diagnostic = bridgeDiagnostic(error, { component: "antigravity-sidecar", operation: request?.operation, requestId: request?.requestId, conversationId: request?.conversationId, readOnly: ["status", "list", "read", "receipt"].includes(request?.operation) });
|
|
58
61
|
reject(error);
|
|
59
62
|
} else resolve(result);
|
|
60
63
|
};
|
|
61
|
-
const timer = setTimeout(() => done(Object.assign(new Error("Antigravity sidecar did not reply; inspect the original requestId before retrying"), {
|
|
64
|
+
const timer = setTimeout(() => done(Object.assign(new Error("Antigravity sidecar did not reply; inspect the original requestId before retrying"), { code: "ANTIGRAVITY_IPC_TIMEOUT" })), timeoutMs);
|
|
62
65
|
socket.on("error", (error) => done(error));
|
|
63
|
-
socket.on("end", () => done(new Error("Antigravity sidecar disconnected before replying")));
|
|
66
|
+
socket.on("end", () => done(Object.assign(new Error("Antigravity sidecar disconnected before replying"), { code: "ANTIGRAVITY_IPC_DISCONNECTED" })));
|
|
64
67
|
socket.on("connect", () => {
|
|
65
68
|
const frame = JSON.stringify(request) + "\n";
|
|
66
|
-
if (Buffer.byteLength(frame) > LIMIT) return done(new Error("Request exceeds size limit"));
|
|
69
|
+
if (Buffer.byteLength(frame) > LIMIT) return done(Object.assign(new Error("Request exceeds size limit"), { code: "ANTIGRAVITY_REQUEST_TOO_LARGE", phase: "validation" }));
|
|
67
70
|
dispatched = true;
|
|
68
71
|
socket.write(frame);
|
|
69
72
|
});
|
|
70
73
|
socket.on("data", (chunk) => {
|
|
71
74
|
buffer = Buffer.concat([buffer, chunk]);
|
|
72
|
-
if (buffer.length > LIMIT) return done(new Error("Sidecar response exceeds size limit"));
|
|
75
|
+
if (buffer.length > LIMIT) return done(Object.assign(new Error("Sidecar response exceeds size limit"), { code: "ANTIGRAVITY_RESPONSE_TOO_LARGE", phase: "response" }));
|
|
73
76
|
const end = buffer.indexOf(10);
|
|
74
77
|
if (end < 0) return;
|
|
75
78
|
try {
|
|
76
79
|
const response = JSON.parse(buffer.subarray(0, end).toString("utf8"));
|
|
77
|
-
if (response.error) return done(Object.assign(new Error(response.error), { deliveryState: response.deliveryState }));
|
|
80
|
+
if (response.error) return done(Object.assign(new Error(response.error), { code: response.code, phase: response.phase, deliveryState: response.deliveryState, diagnostic: response.diagnostic }));
|
|
78
81
|
done(null, response.result);
|
|
79
|
-
} catch (error) { done(error); }
|
|
82
|
+
} catch (error) { done(Object.assign(new Error("Antigravity sidecar returned an invalid response"), { code: "ANTIGRAVITY_RESPONSE_INVALID", phase: "response", cause: error })); }
|
|
80
83
|
});
|
|
81
84
|
});
|
|
82
85
|
}
|
|
@@ -101,12 +104,18 @@ export async function serveAntigravityDesktop(handler, { directory = antigravity
|
|
|
101
104
|
const end = buffer.indexOf(10);
|
|
102
105
|
if (end < 0) return;
|
|
103
106
|
received = true;
|
|
107
|
+
let request;
|
|
104
108
|
try {
|
|
105
|
-
|
|
109
|
+
try { request = JSON.parse(buffer.subarray(0, end).toString("utf8")); }
|
|
110
|
+
catch { throw Object.assign(new Error("Invalid Antigravity sidecar request"), { code: "ANTIGRAVITY_REQUEST_INVALID", phase: "validation", deliveryState: "not_sent" }); }
|
|
111
|
+
const result = await handler(request);
|
|
106
112
|
const frame = JSON.stringify({ result }) + "\n";
|
|
107
|
-
if (Buffer.byteLength(frame) > LIMIT) throw new Error("Sidecar response exceeds size limit");
|
|
113
|
+
if (Buffer.byteLength(frame) > LIMIT) throw Object.assign(new Error("Sidecar response exceeds size limit"), { code: "ANTIGRAVITY_RESPONSE_TOO_LARGE", phase: "response" });
|
|
108
114
|
socket.end(frame);
|
|
109
|
-
} catch (error) {
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const diagnostic = bridgeDiagnostic(error, { component: "antigravity-sidecar", operation: request?.operation, requestId: request?.requestId, conversationId: request?.conversationId, readOnly: ["status", "list", "read", "receipt"].includes(request?.operation), deliveryState: error.deliveryState ?? "unknown" });
|
|
117
|
+
socket.end(JSON.stringify({ error: diagnostic.reason, code: diagnostic.code, phase: diagnostic.phase, deliveryState: diagnostic.deliveryState, diagnostic }) + "\n");
|
|
118
|
+
}
|
|
110
119
|
});
|
|
111
120
|
});
|
|
112
121
|
const privatePath = process.platform === "win32" ? socketPath : path.join(directory, `.${crypto.randomBytes(6).toString("hex")}.sock`);
|