@minhspark/codex-mcp-bridge 1.25.1 → 1.25.3
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 +40 -0
- package/README.md +40 -16
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +40 -10
- 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.mjs +3 -3
- package/src/antigravity-bridge.mjs +79 -25
- package/src/antigravity-deliveries.mjs +32 -8
- package/src/antigravity-ipc.mjs +18 -9
- package/src/antigravity-outbound-bridge.mjs +46 -19
- package/src/antigravity-outbound-targets.mjs +41 -6
- package/src/bridge-readiness.mjs +2 -2
- package/src/claude-bridge.mjs +38 -23
- package/src/claude-code-registration.mjs +2 -1
- package/src/claude-session-router.mjs +8 -15
- package/src/index.mjs +35 -24
- package/src/mcp-supervisor.mjs +91 -10
- package/src/native-relay-companion.mjs +23 -3
- package/src/native-relay.mjs +8 -4
- package/src/peer-protocol.mjs +9 -6
- package/src/platform.mjs +1 -0
- package/src/recipient-preflight.mjs +81 -0
- package/src/relay-wake.mjs +15 -218
- package/src/setup.mjs +38 -18
- package/src/thread-delivery.mjs +28 -21
|
@@ -9,11 +9,12 @@ 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";
|
|
17
18
|
|
|
18
19
|
exitForVersionRequest(import.meta.url);
|
|
19
20
|
|
|
@@ -28,7 +29,7 @@ const senderRelay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath()
|
|
|
28
29
|
const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
29
30
|
const schema = { requestId: z.string().uuid(), cwd: z.string().min(1), prompt: z.string().min(1).max(65536) };
|
|
30
31
|
|
|
31
|
-
async function
|
|
32
|
+
async function identifySender(extra) {
|
|
32
33
|
if (caller === "codex") {
|
|
33
34
|
const context = await resolveCodexSenderContext(extra?._meta, { inspectDesktopTask: async (threadId) => {
|
|
34
35
|
const accounts = requireBridgeAccounts(readBridgeAccounts());
|
|
@@ -46,17 +47,34 @@ async function sender(extra) {
|
|
|
46
47
|
throw new Error("Install this bridge for Codex or Claude; unknown callers cannot read conversations or send work");
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
async function sender(extra) {
|
|
51
|
+
try { return await identifySender(extra); }
|
|
52
|
+
catch (error) {
|
|
53
|
+
error.code ??= "ANTIGRAVITY_SENDER_UNVERIFIED";
|
|
54
|
+
error.phase ??= "sender";
|
|
55
|
+
error.deliveryState = "not_sent";
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
function sameCwd(expected, actual) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
try {
|
|
62
|
+
if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("Expected an absolute project directory");
|
|
63
|
+
const left = fs.realpathSync.native(expected);
|
|
64
|
+
const right = fs.realpathSync.native(actual);
|
|
65
|
+
if (path.relative(left, right)) throw new Error("Antigravity destination must match the verified sender's project directory");
|
|
66
|
+
rootPolicy.assert(left, "Antigravity project");
|
|
67
|
+
return left;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
error.code ??= "ANTIGRAVITY_PROJECT_SCOPE_INVALID";
|
|
70
|
+
error.phase ??= "scope";
|
|
71
|
+
error.deliveryState = "not_sent";
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
function owns(record, identity) {
|
|
59
|
-
if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw
|
|
77
|
+
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
78
|
sameCwd(identity.cwd, record.cwd);
|
|
61
79
|
return record;
|
|
62
80
|
}
|
|
@@ -65,20 +83,49 @@ function handoff(prompt, identity) {
|
|
|
65
83
|
return `[Agent handoff from ${identity.provider} conversation ${identity.id}. Native Antigravity permissions remain in effect.]\n\n${prompt}`;
|
|
66
84
|
}
|
|
67
85
|
|
|
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.
|
|
86
|
+
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
87
|
|
|
70
88
|
function register(name, description, inputSchema, readOnly, handler) {
|
|
71
89
|
server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
const options = { component: "antigravity-bridge", operation: name, requestId: args.requestId, conversationId: args.conversationId, readOnly };
|
|
91
|
+
try {
|
|
92
|
+
const value = await handler(args, extra);
|
|
93
|
+
const failure = antigravityDeliveryError(value);
|
|
94
|
+
if (failure) return withBridgeDiagnostic({ ...result(value), isError: true }, failure, { ...options, readOnly: false, msgId: value.result?.msgId });
|
|
95
|
+
if (value?.desktop?.available === false) return withBridgeDiagnostic({ ...result(value), isError: true }, { diagnostic: value.desktop.diagnostic }, options);
|
|
96
|
+
return result(value);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const diagnostic = bridgeDiagnostic(error, options);
|
|
99
|
+
return withBridgeDiagnostic({ ...result({ error: diagnostic.reason, code: diagnostic.code, deliveryState: error.deliveryState ?? diagnostic.deliveryState }), isError: true }, { diagnostic }, options);
|
|
100
|
+
}
|
|
74
101
|
});
|
|
75
102
|
}
|
|
76
103
|
|
|
77
|
-
register("antigravity_bridge_status", "Inspect
|
|
104
|
+
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
105
|
let desktop;
|
|
79
106
|
try { desktop = await callAntigravityDesktop({ operation: "status" }, { timeoutMs: 5000 }); }
|
|
80
|
-
catch
|
|
81
|
-
|
|
107
|
+
catch (cause) {
|
|
108
|
+
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 });
|
|
109
|
+
const diagnostic = bridgeDiagnostic(error, { component: "antigravity-bridge", operation: "antigravity_bridge_status", readOnly: true });
|
|
110
|
+
desktop = { available: false, code: diagnostic.code, reason: diagnostic.reason, diagnostic };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
version,
|
|
114
|
+
caller: caller ?? "unconfigured",
|
|
115
|
+
desktop,
|
|
116
|
+
cli: { transport: "stream-json", activeSessions: sessions.size, permissionPolicy: "native-unchanged", interactiveTerminalAttachment: false },
|
|
117
|
+
outbound: {
|
|
118
|
+
supported: true,
|
|
119
|
+
server: "agent-bridge",
|
|
120
|
+
surface: "desktop",
|
|
121
|
+
targets: ["codex-desktop", "claude-desktop-code"],
|
|
122
|
+
scope: "existing-conversations-in-the-same-project",
|
|
123
|
+
readiness: "not_checked",
|
|
124
|
+
statusTool: "agent_bridge_status",
|
|
125
|
+
cliSupported: false,
|
|
126
|
+
automaticReplyForwarding: false,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
82
129
|
});
|
|
83
130
|
|
|
84
131
|
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 +141,29 @@ register("send_to_antigravity_conversation", "Send one authorized prompt to an e
|
|
|
94
141
|
const identity = await sender(extra);
|
|
95
142
|
const cwd = sameCwd(identity.cwd, args.cwd);
|
|
96
143
|
const prompt = handoff(args.prompt, identity);
|
|
97
|
-
if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw
|
|
144
|
+
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
145
|
if (args.surface === "desktop") return callAntigravityDesktop({ operation: "send", ...args, cwd, prompt, sender: identity });
|
|
99
146
|
const entry = sessions.get(args.conversationId);
|
|
100
|
-
if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw
|
|
147
|
+
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
148
|
sameCwd(cwd, entry.session.read().cwd);
|
|
102
149
|
return deliveries.dispatch({ ...args, cwd, sender: identity, prompt }, async () => {
|
|
103
150
|
const current = await sender(extra);
|
|
104
151
|
sameCwd(cwd, current.cwd);
|
|
105
|
-
if (current.id !== identity.id) throw
|
|
152
|
+
if (current.id !== identity.id) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
106
153
|
return entry.session.send(prompt);
|
|
107
|
-
});
|
|
154
|
+
}, { component: "antigravity-bridge", operation: "send_to_antigravity_conversation" });
|
|
108
155
|
});
|
|
109
156
|
|
|
110
157
|
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
158
|
const identity = await sender(extra);
|
|
112
159
|
const cwd = sameCwd(identity.cwd, args.cwd);
|
|
113
|
-
if (sessions.size + startingSessions >= 4) throw
|
|
160
|
+
if (sessions.size + startingSessions >= 4) throw preflightFailure("ANTIGRAVITY_SESSION_CAPACITY", "At most four CLI sessions may be open in one bridge");
|
|
114
161
|
return deliveries.dispatch({ ...args, cwd, sender: identity, surface: "cli" }, async () => {
|
|
115
|
-
if (sessions.size + startingSessions >= 4) throw
|
|
162
|
+
if (sessions.size + startingSessions >= 4) throw preflightFailure("ANTIGRAVITY_SESSION_CAPACITY", "CLI session capacity reached");
|
|
116
163
|
startingSessions++;
|
|
117
164
|
let reserved = true;
|
|
118
165
|
let session;
|
|
166
|
+
let sending = false;
|
|
119
167
|
try {
|
|
120
168
|
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^(?:CODEX_|CLAUDE|ANTHROPIC_|MCP_)/i.test(name)));
|
|
121
169
|
session = new AntigravityCliSession({ cwd, env });
|
|
@@ -126,9 +174,15 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
126
174
|
reserved = false;
|
|
127
175
|
const current = await sender(extra);
|
|
128
176
|
sameCwd(cwd, current.cwd);
|
|
129
|
-
if (current.id !== identity.id) throw
|
|
177
|
+
if (current.id !== identity.id) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
|
|
178
|
+
sending = true;
|
|
130
179
|
return await session.send(handoff(args.prompt, identity));
|
|
131
180
|
} catch (error) {
|
|
181
|
+
if (!sending) {
|
|
182
|
+
error.code ??= "ANTIGRAVITY_CLI_START_FAILED";
|
|
183
|
+
error.phase ??= "startup";
|
|
184
|
+
error.deliveryState = "not_sent";
|
|
185
|
+
}
|
|
132
186
|
if (error.deliveryState !== "unknown") {
|
|
133
187
|
for (const [id, entry] of sessions) if (entry.session === session) sessions.delete(id);
|
|
134
188
|
await session?.close();
|
|
@@ -137,7 +191,7 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
|
|
|
137
191
|
} finally {
|
|
138
192
|
if (reserved) startingSessions--;
|
|
139
193
|
}
|
|
140
|
-
});
|
|
194
|
+
}, { component: "antigravity-bridge", operation: "start_antigravity_cli_session" });
|
|
141
195
|
});
|
|
142
196
|
|
|
143
197
|
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 +203,9 @@ register("read_antigravity_delivery", "Read the original receipt without resendi
|
|
|
149
203
|
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
204
|
const identity = await sender(extra);
|
|
151
205
|
const entry = sessions.get(conversationId);
|
|
152
|
-
if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw
|
|
206
|
+
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
207
|
sameCwd(identity.cwd, entry.session.read().cwd);
|
|
154
|
-
if (["running", "starting"].includes(entry.session.read().status)) throw
|
|
208
|
+
if (["running", "starting"].includes(entry.session.read().status)) throw preflightFailure("ANTIGRAVITY_SESSION_BUSY", "Wait for the active CLI delivery before closing");
|
|
155
209
|
const closed = await entry.session.close();
|
|
156
210
|
if (closed.status !== "closing") sessions.delete(conversationId);
|
|
157
211
|
return { closed: closed.status !== "closing", conversationId, status: closed.status };
|
|
@@ -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`);
|
|
@@ -7,12 +7,13 @@ 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
15
|
import { assertAccountIdentity } from "./bridge-account-context.mjs";
|
|
16
|
+
import { bridgeDiagnostic, withBridgeDiagnostic, preflightFailure } from "./recipient-preflight.mjs";
|
|
16
17
|
|
|
17
18
|
const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
18
19
|
const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
@@ -20,19 +21,34 @@ const targetSchema = { target: z.enum(["codex", "claude"]) };
|
|
|
20
21
|
const taskSchema = { ...targetSchema, conversationId: z.string().uuid(), taskId: z.string().min(1).max(256).optional() };
|
|
21
22
|
|
|
22
23
|
export function createAntigravityOutboundBridge({ resolveSender = resolveAntigravitySenderContext, targets = createAntigravityOutboundTargets(), deliveries = new AntigravityDeliveries(path.join(antigravityBridgeDirectory(), "outbound-deliveries")), rootPolicy = createHardenedRootPolicy() } = {}) {
|
|
23
|
-
const server = new McpServer({ name: "agent-bridge", version }, { instructions: "Send user-authorized work FROM the current Antigravity Desktop conversation to an exact existing Codex Desktop task or Claude Desktop Code session in the same project. Use list_agent_tasks first, verify title and identity, then send_to_agent_task with a fresh requestId. Keep requestId after uncertainty and read_agent_delivery; never automatically resend. Native permissions and sender identity are verified on every call. Do not launch a CLI substitute, impersonate Codex/Claude, or change permissions to overcome a rejection. Native acceptance is not task completion; read_agent_task for Codex or read_agent_delivery for a Claude reply. Independent new work requires a separate native conversation; this bridge only continues existing tasks. Agent-authored handoffs must be in English and name Antigravity as the author." });
|
|
24
|
+
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." });
|
|
24
25
|
|
|
25
26
|
function scope(expected, actual) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
try {
|
|
28
|
+
if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("An absolute project directory is required");
|
|
29
|
+
const canonical = fs.realpathSync.native(expected);
|
|
30
|
+
if (path.relative(canonical, fs.realpathSync.native(actual))) throw new Error("Destination must match the verified Antigravity sender's project");
|
|
31
|
+
rootPolicy.assert(canonical, "Antigravity sender project");
|
|
32
|
+
return canonical;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
error.code ??= "ANTIGRAVITY_PROJECT_SCOPE_INVALID";
|
|
35
|
+
error.phase ??= "scope";
|
|
36
|
+
error.deliveryState = "not_sent";
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
async function sender(extra) {
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
let identity;
|
|
43
|
+
try {
|
|
44
|
+
identity = await resolveSender(extra?._meta);
|
|
45
|
+
if (identity.status !== "verified" || identity.provider !== "antigravity" || !identity.id || !["bypass", "prompting"].includes(identity.mode)) throw new Error(identity.reason ?? "Antigravity sender is not verified");
|
|
46
|
+
} catch (error) {
|
|
47
|
+
error.code ??= "ANTIGRAVITY_SENDER_UNVERIFIED";
|
|
48
|
+
error.phase ??= "sender";
|
|
49
|
+
error.deliveryState = "not_sent";
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
36
52
|
scope(identity.cwd, identity.cwd);
|
|
37
53
|
return identity;
|
|
38
54
|
}
|
|
@@ -40,44 +56,54 @@ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigra
|
|
|
40
56
|
function recheck(identity, extra) {
|
|
41
57
|
return async () => {
|
|
42
58
|
const current = await sender(extra);
|
|
43
|
-
if (JSON.stringify(current) !== JSON.stringify(identity)) throw
|
|
59
|
+
if (JSON.stringify(current) !== JSON.stringify(identity)) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Antigravity sender identity or permissions changed before delivery");
|
|
44
60
|
};
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
function register(name, description, inputSchema, readOnly, handler) {
|
|
48
64
|
server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
const options = { component: "agent-bridge", operation: name, requestId: args.requestId, conversationId: args.conversationId, readOnly };
|
|
66
|
+
try {
|
|
67
|
+
const value = await handler(args, extra);
|
|
68
|
+
const failure = antigravityDeliveryError(value);
|
|
69
|
+
if (failure) return withBridgeDiagnostic({ ...result(value), isError: true }, failure, { ...options, readOnly: false, msgId: value.result?.msgId });
|
|
70
|
+
if (value?.diagnostic && (value.nativeReceipt === null || value.sender?.status !== "verified")) return withBridgeDiagnostic({ ...result(value), isError: true }, { diagnostic: value.diagnostic }, options);
|
|
71
|
+
return result(value);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const diagnostic = bridgeDiagnostic(error, options);
|
|
74
|
+
return withBridgeDiagnostic({ ...result({ error: diagnostic.reason, code: diagnostic.code, deliveryState: error.deliveryState ?? diagnostic.deliveryState }), isError: true }, { diagnostic }, options);
|
|
75
|
+
}
|
|
51
76
|
});
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
register("agent_bridge_status", "Check this Antigravity caller's native identity and outbound bridge capabilities without sending work.", {}, true, async (_, extra) => {
|
|
55
80
|
const identity = await resolveSender(extra?._meta);
|
|
56
81
|
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
|
-
|
|
82
|
+
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 });
|
|
83
|
+
return { version, sender: identity, metadataFields, targets: ["codex-desktop", "claude-desktop"], permissions: "native-unchanged", createsConversations: false, ...(diagnostic ? { diagnostic } : {}) };
|
|
58
84
|
});
|
|
59
85
|
|
|
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) => {
|
|
86
|
+
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
87
|
const identity = await sender(extra);
|
|
62
88
|
return targets.list({ ...args, sender: identity, beforeSend: recheck(identity, extra) });
|
|
63
89
|
});
|
|
64
90
|
|
|
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) => {
|
|
91
|
+
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
92
|
const identity = await sender(extra);
|
|
67
93
|
const cwd = scope(identity.cwd, args.cwd);
|
|
68
|
-
if (args.prompt.includes("\0")) throw
|
|
94
|
+
if (args.prompt.includes("\0")) throw preflightFailure("ANTIGRAVITY_PROMPT_INVALID", "Prompt must not contain NUL");
|
|
69
95
|
const prompt = `[From Antigravity Desktop · ${path.basename(cwd)} · Authorized task handoff]\n[Source conversation: ${identity.id}]\n\n${args.prompt}`;
|
|
70
96
|
return deliveries.dispatch({ ...args, cwd, surface: args.target, sender: { provider: identity.provider, id: identity.id }, prompt }, async () => {
|
|
71
97
|
await recheck(identity, extra)();
|
|
72
98
|
const sent = await targets.send({ ...args, cwd, prompt, sender: identity, ...(args.target === "codex" ? { threadId: args.conversationId } : { sessionId: args.conversationId }), beforeSend: recheck(identity, extra) });
|
|
73
99
|
return { ...sent, completed: false };
|
|
74
|
-
});
|
|
100
|
+
}, { component: "agent-bridge", operation: "send_to_agent_task" });
|
|
75
101
|
});
|
|
76
102
|
|
|
77
103
|
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
104
|
const identity = await sender(extra);
|
|
79
105
|
const record = deliveries.read(requestId);
|
|
80
|
-
if (!record || record.sender?.provider !== "antigravity" || record.sender.id !== identity.id) throw
|
|
106
|
+
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
107
|
scope(identity.cwd, record.cwd);
|
|
82
108
|
if (record.result?.accountContext) assertAccountIdentity(record.result.accountContext);
|
|
83
109
|
if (record.surface === "claude" && record.result?.msgId) {
|
|
@@ -85,7 +111,8 @@ export function createAntigravityOutboundBridge({ resolveSender = resolveAntigra
|
|
|
85
111
|
return { ...record, nativeReceipt: await targets.read({ target: "claude", msgId: record.result.msgId, sender: identity, beforeSend: recheck(identity, extra) }) };
|
|
86
112
|
} catch (error) {
|
|
87
113
|
if (error.code !== "ANTIGRAVITY_NATIVE_RECEIPT_UNAVAILABLE") throw error;
|
|
88
|
-
|
|
114
|
+
const diagnostic = bridgeDiagnostic(error, { component: "agent-bridge", operation: "read_agent_delivery", phase: "receipt", readOnly: true, requestId, msgId: record.result.msgId, conversationId: record.conversationId });
|
|
115
|
+
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
116
|
}
|
|
90
117
|
}
|
|
91
118
|
return record;
|
|
@@ -3,10 +3,10 @@ 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
12
|
|
|
@@ -42,6 +42,7 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
42
42
|
const readAccounts = options.readAccounts ?? readBridgeAccounts;
|
|
43
43
|
const assertAccounts = options.assertAccounts ?? ((identity) => assertAccountIdentity(identity, readAccounts()));
|
|
44
44
|
const listSessions = options.listSessions ?? listClaudeSessions;
|
|
45
|
+
const listTasks = options.listTasks ?? ((account) => readClaudeDesktopTasks({ account }));
|
|
45
46
|
const readContext = options.readContext ?? ((session, account) => readClaudeDesktopContext(session, { account }));
|
|
46
47
|
const readInbound = options.readInbound ?? readClaudeInboundPolicy;
|
|
47
48
|
const assertProcess = options.assertProcess ?? assertClaudeSessionProcess;
|
|
@@ -122,6 +123,29 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
122
123
|
return { ...session, desktop, inbound: readInbound(session.cwd) };
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
function savedClaudeTask(context, tasks, sessionId, taskId) {
|
|
127
|
+
if (!Array.isArray(tasks) || tasks.length > 8192) throw blocked("Saved Claude Desktop tasks cannot be verified");
|
|
128
|
+
const matches = tasks.filter((task) => task.taskId === taskId);
|
|
129
|
+
const task = matches[0];
|
|
130
|
+
if (matches.length !== 1 || typeof taskId !== "string" || !taskId.startsWith("local_") || !UUID.test(taskId.slice(6)) || task.fileTaskId !== taskId || task.isArchived !== false
|
|
131
|
+
|| !UUID.test(sessionId ?? "") || task.cliSessionId !== sessionId || !sameDirectory(captureDirectory(task.cwd).path, context.cwd)
|
|
132
|
+
|| 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");
|
|
133
|
+
return { target: "claude", sessionId, taskId, cwd: context.cwd, title: task.title, status: "dormant", recoverable: false, code: "CLAUDE_SESSION_NOT_READY" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function recoverClaudeRecipient(context, sessionId, taskId) {
|
|
137
|
+
if (listSessions().some((session) => session.sessionId === sessionId && session.alive)) return claudeRecipient(context, sessionId, taskId);
|
|
138
|
+
savedClaudeTask(context, await listTasks(context.accounts.claude), sessionId, taskId);
|
|
139
|
+
await context.check();
|
|
140
|
+
const resolved = await recoverClaudeDesktopSession({
|
|
141
|
+
target: "auto", expectedCwd: context.cwd, expectedTaskId: taskId, sessions: listSessions(), account: context.accounts.claude, readContext,
|
|
142
|
+
}, {
|
|
143
|
+
listTasks: () => listTasks(context.accounts.claude), listSessions, beforeInspect: context.check,
|
|
144
|
+
});
|
|
145
|
+
await context.check();
|
|
146
|
+
return claudeRecipient(context, resolved.session.sessionId, taskId);
|
|
147
|
+
}
|
|
148
|
+
|
|
125
149
|
async function list({ target, sender, beforeSend } = {}) {
|
|
126
150
|
const context = operation(sender, beforeSend);
|
|
127
151
|
await context.check();
|
|
@@ -148,9 +172,15 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
148
172
|
try {
|
|
149
173
|
const desktop = readContext(session, context.accounts.claude);
|
|
150
174
|
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 });
|
|
175
|
+
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
176
|
} catch {}
|
|
153
177
|
}
|
|
178
|
+
const tasks = await listTasks(context.accounts.claude);
|
|
179
|
+
if (!Array.isArray(tasks) || tasks.length > 8192) throw blocked("Saved Claude Desktop tasks cannot be verified");
|
|
180
|
+
for (const task of tasks) {
|
|
181
|
+
if (rows.some((row) => row.taskId === task.taskId) || listSessions().some((session) => session.sessionId === task.cliSessionId && session.alive)) continue;
|
|
182
|
+
try { rows.push(savedClaudeTask(context, tasks, task.cliSessionId, task.taskId)); } catch {}
|
|
183
|
+
}
|
|
154
184
|
await context.check();
|
|
155
185
|
return rows;
|
|
156
186
|
}
|
|
@@ -178,7 +208,7 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
178
208
|
}
|
|
179
209
|
}
|
|
180
210
|
if (target !== "claude") throw blocked("Target must be codex or claude");
|
|
181
|
-
const recipient =
|
|
211
|
+
const recipient = await recoverClaudeRecipient(context, sessionId, taskId);
|
|
182
212
|
assertRecipientClass(recipient, context.identity);
|
|
183
213
|
if (!peer) peer = peerFactory({ name: `antigravity-${process.pid}`, cwd: context.cwd, signalExit: false, env, protectSocket: protectCurrentUserPipe });
|
|
184
214
|
await peer.start();
|
|
@@ -189,7 +219,7 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
189
219
|
recipient: { permissionMode: recipient.desktop.permissionMode ?? null, permissionClass: recipient.desktop.permissionClass ?? null, inboundPolicy: recipient.inbound?.value ?? null },
|
|
190
220
|
beforeSend: async () => {
|
|
191
221
|
await context.check();
|
|
192
|
-
const current = claudeRecipient(context, sessionId, taskId);
|
|
222
|
+
const current = claudeRecipient(context, recipient.sessionId, taskId);
|
|
193
223
|
if (!sameClaudeDesktopRecipient(recipient, current)) throw blocked("Claude session process or socket changed before delivery");
|
|
194
224
|
assertRecipientClass(current, context.identity);
|
|
195
225
|
await context.check();
|
|
@@ -198,7 +228,7 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
198
228
|
msgId = outcome.msgId;
|
|
199
229
|
receipts.set(msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
|
|
200
230
|
await context.check();
|
|
201
|
-
return { target, sessionId, taskId, cwd: context.cwd, msgId, status: outcome.delivery?.status ?? "sent_unconfirmed", completed: false, accountContext: context.accountContext };
|
|
231
|
+
return { target, sessionId: recipient.sessionId, taskId, cwd: context.cwd, msgId, status: outcome.delivery?.status ?? "sent_unconfirmed", completed: false, accountContext: context.accountContext };
|
|
202
232
|
} catch (error) {
|
|
203
233
|
if (error.msgId) receipts.set(error.msgId, { sender: context.identity, accountContext: context.accountContext, cwd: context.cwd });
|
|
204
234
|
if (msgId) error.msgId = msgId;
|
|
@@ -221,6 +251,11 @@ export function createAntigravityOutboundTargets(options = {}) {
|
|
|
221
251
|
}
|
|
222
252
|
if (target !== undefined && target !== "claude") throw blocked("Target must be codex or claude");
|
|
223
253
|
if (msgId === undefined) {
|
|
254
|
+
if (!listSessions().some((session) => session.sessionId === sessionId && session.alive)) {
|
|
255
|
+
const task = savedClaudeTask(context, await listTasks(context.accounts.claude), sessionId, taskId);
|
|
256
|
+
await context.check();
|
|
257
|
+
return task;
|
|
258
|
+
}
|
|
224
259
|
const recipient = claudeRecipient(context, sessionId, taskId);
|
|
225
260
|
await context.check();
|
|
226
261
|
return { target: "claude", sessionId, taskId, cwd: context.cwd, title: recipient.desktop.title ?? null, permissionClass: recipient.desktop.permissionClass ?? null, inboundPolicy: recipient.inbound?.value ?? null };
|
package/src/bridge-readiness.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RELAY_DOWN_GUIDANCE } from "./thread-delivery.mjs";
|
|
2
2
|
|
|
3
|
-
export function bridgeReadiness({ sender, scope, targetCwd, nativeAvailable = true, verifyTargetProject }) {
|
|
3
|
+
export function bridgeReadiness({ sender, scope, targetCwd, nativeAvailable = true, nativeDiagnostic, verifyTargetProject }) {
|
|
4
4
|
const issues = [];
|
|
5
5
|
if (sender?.status !== "verified") issues.push({ code: "SENDER_UNVERIFIED", detail: sender?.reason ?? "No current Code session identity", action: "Load the user-level Claude Code bridge entry in this existing task; a shared Desktop entry cannot identify the caller. Run the Code registration check, then reload the affected client." });
|
|
6
6
|
const projectPolicy = scope?.status?.(sender?.status === "verified" ? sender.cwd : undefined) ?? null;
|
|
@@ -20,6 +20,6 @@ export function bridgeReadiness({ sender, scope, targetCwd, nativeAvailable = tr
|
|
|
20
20
|
issues.push({ code: error.code ?? "PROJECT_SCOPE_UNVERIFIED", detail: error.message, action: "Select a destination in the verified sender's project. A project grant does not authorize cross-project delivery." });
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
if (!nativeAvailable) issues.push({ code: "RELAY_UNAVAILABLE", action: RELAY_DOWN_GUIDANCE });
|
|
23
|
+
if (!nativeAvailable) issues.push({ code: nativeDiagnostic?.code ?? "RELAY_UNAVAILABLE", detail: nativeDiagnostic?.reason, action: RELAY_DOWN_GUIDANCE, ...(nativeDiagnostic ? { diagnostic: nativeDiagnostic } : {}) });
|
|
24
24
|
return { ready: issues.length === 0, readyScope: "transport-identity-and-project", hostToolPermission: "evaluated-by-client-at-call-time", sender, projectPolicy, target, issues };
|
|
25
25
|
}
|