@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.
@@ -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 sender(extra) {
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
- if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("Expected an absolute project directory");
51
- const left = fs.realpathSync.native(expected);
52
- const right = fs.realpathSync.native(actual);
53
- if (path.relative(left, right)) throw new Error("Antigravity destination must match the verified sender's project directory");
54
- rootPolicy.assert(left, "Antigravity project");
55
- return left;
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 new Error("Delivery does not belong to this sending conversation");
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. Do not claim support for Antigravity-origin handoffs into Codex or Claude; this server receives from those hosts." });
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
- try { return result(await handler(args, extra)); }
73
- catch (error) { return { ...result({ error: error.message, deliveryState: error.deliveryState ?? "not_sent" }), isError: true }; }
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 connection capabilities without sending prompts.", {}, true, async () => {
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 { desktop = { available: false, reason: "Open Antigravity with the installed codex-bridge sidecar enabled" }; }
81
- return { version, caller: caller ?? "unconfigured", desktop, cli: { transport: "stream-json", activeSessions: sessions.size, permissionPolicy: "native-unchanged", interactiveTerminalAttachment: false } };
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 new Error("Desktop handoff must fit within 30000 characters and contain no NUL");
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 new Error("CLI session is not owned by this sender and bridge process");
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 Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
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 new Error("At most four CLI sessions may be open in one bridge");
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 Object.assign(new Error("CLI session capacity reached"), { deliveryState: "not_sent" });
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 Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
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 new Error("CLI session is not owned by this sender");
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 new Error("Wait for the active CLI delivery before closing");
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)) return { ...record, status: "unknown", reason: "This process cannot observe the original worker. Inspect the exact destination; do not resend." };
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") throw error;
71
- throw new Error("Delivery is owned by another process. Inspect requestId; do not resend.");
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.status = ["not_sent", "not-sent"].includes(error.deliveryState) ? "failed" : "unknown";
80
- record.reason = error.message;
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(() => {
@@ -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"), { deliveryState: "unknown" })), timeoutMs);
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
- const result = await handler(JSON.parse(buffer.subarray(0, end).toString("utf8")));
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) { socket.end(JSON.stringify({ error: error.message, deliveryState: error.deliveryState }) + "\n"); }
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
- if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("An absolute project directory is required");
27
- const canonical = fs.realpathSync.native(expected);
28
- if (path.relative(canonical, fs.realpathSync.native(actual))) throw new Error("Destination must match the verified Antigravity sender's project");
29
- rootPolicy.assert(canonical, "Antigravity sender project");
30
- return canonical;
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
- const identity = await resolveSender(extra?._meta);
35
- if (identity.status !== "verified" || identity.provider !== "antigravity" || !identity.id || !["bypass", "prompting"].includes(identity.mode)) throw new Error(identity.reason ?? "Antigravity sender is not verified");
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 Object.assign(new Error("Antigravity sender identity or permissions changed before delivery"), { deliveryState: "not_sent" });
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
- try { return result(await handler(args, extra)); }
50
- catch (error) { return { ...result({ error: error.message, deliveryState: error.deliveryState ?? "not_sent" }), isError: true }; }
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
- return { version, sender: identity, metadataFields, targets: ["codex-desktop", "claude-desktop"], permissions: "native-unchanged", createsConversations: false };
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 new Error("Prompt must not contain NUL");
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 new Error("Delivery does not belong to this Antigravity conversation");
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
- return { ...record, nativeReceipt: null, recovery: "The original native receipt is unavailable. Inspect the exact Claude Desktop task before further action; do not resend.", receiptError: error.message };
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 = claudeRecipient(context, sessionId, taskId);
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 };
@@ -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
  }