@sagentlab/navarch-runtime 0.1.107 → 0.1.112

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.
@@ -2,329 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.geminiAdapter = void 0;
4
4
  exports.runGeminiAdapter = runGeminiAdapter;
5
- const node_child_process_1 = require("node:child_process");
6
- const node_fs_1 = require("node:fs");
7
- const kill_agent_cjs_1 = require("../kill-agent.cjs");
8
- /**
9
- * Headless Google Gemini CLI adapter. Gemini's `stream-json` output gives the
10
- * same normalized boundary as the Claude and Codex adapters: raw process
11
- * signals plus an optional final report and token counts. Gemini CLI does not
12
- * report USD cost, so costUsd remains absent.
13
- *
14
- * The CLI reads MCP servers from settings rather than accepting Claude's
15
- * `--mcp-config` flag. The adapter converts the lease-scoped config into a
16
- * temporary system-settings file and restricts the run to those server names.
17
- *
18
- * The prompt arrives on stdin rather than `--prompt`: Gemini runs headlessly
19
- * whenever stdin is not a TTY and uses the piped text as the prompt. Its
20
- * native sandbox puts stdin back into argv, so that path still has an argv
21
- * limit: Linux's 128 KiB per-string limit for container sandboxes and Linux
22
- * hosts, or macOS's 1 MiB total for sandbox-exec. Gemini also caps stdin at
23
- * 8 MiB.
24
- */
5
+ /** Tombstone for stale claims. Provider IDs and credentials are never aliased. */
25
6
  async function runGeminiAdapter(options) {
26
- if (options.signal?.aborted) {
27
- return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
28
- }
29
- const promptBytes = Buffer.byteLength(options.prompt, "utf8");
30
- // Gemini's readStdin() silently truncates at 8 MiB. Reject before any
31
- // settings writes or process launch rather than execute a partial task.
32
- if (promptBytes > 8 * 1024 * 1024) {
33
- return inputFailure(`Gemini CLI stdin is limited to 8 MiB; the task prompt is ${promptBytes} bytes. Reduce the context bundle or select another runtime.`);
34
- }
35
- const args = [];
36
- const env = { ...options.env };
37
- if (!hasArg(options.extraArgs, "--output-format", "-o")) {
38
- args.push("--output-format", "stream-json");
39
- }
40
- if (!hasArg(options.extraArgs, "--skip-trust")) {
41
- args.push("--skip-trust");
42
- }
43
- if (!hasExplicitApprovalMode(options.extraArgs)) {
44
- // Navarch sessions have no interactive operator. Host sessions still use
45
- // Gemini's sandbox below when the worktree guard is enabled.
46
- args.push("--approval-mode", "yolo");
47
- }
48
- const sandboxMounts = [...(options.geminiSandboxMounts ?? [])];
49
- if (options.mcpConfigPath) {
50
- const mcp = await prepareGeminiMcpSettings(options.mcpConfigPath, env);
51
- env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = mcp.settingsPath;
52
- sandboxMounts.push(`${mcp.settingsPath}:${mcp.settingsPath}:ro`);
53
- if (mcp.serverNames.length > 0 && !hasArg(options.extraArgs, "--allowed-mcp-server-names")) {
54
- args.push("--allowed-mcp-server-names", mcp.serverNames.join(","));
55
- }
56
- }
57
- if (!hasArg(options.extraArgs, "--sandbox", "-s")) {
58
- if (options.geminiSandboxMounts) {
59
- args.push("--sandbox");
60
- env.SANDBOX_MOUNTS = sandboxMounts.join(",");
61
- }
62
- else {
63
- // YOLO mode enables Gemini's own sandbox implicitly. Disable that
64
- // nested sandbox when Navarch already supplied Docker isolation, or
65
- // when an operator explicitly disabled the host worktree guard.
66
- args.push("--sandbox=false");
67
- }
68
- }
69
- args.push(...options.extraArgs);
70
- if (options.model)
71
- args.push("--model", options.model);
72
- // Gemini's sandbox launcher reads stdin and injects it back into --prompt
73
- // for its child process (packages/cli/src/gemini.tsx injectStdinIntoArgs).
74
- // Moving our first spawn's prompt to stdin cannot fix that second spawn.
75
- // Check the effective sandbox, including operator overrides, without
76
- // disabling the worktree guard. Even on macOS a Docker sandbox may launch on
77
- // Linux, while macOS sandbox-exec re-quotes the prompt into one `sh -c`
78
- // string that only the 1 MiB total ARG_MAX bounds.
79
- const platform = options.dockerExec ? "linux" : process.platform;
80
- const sandboxCommand = geminiSandboxCommand(args, geminiProcessEnv(options, env), platform);
81
- if (sandboxCommand === "sandbox-exec" && platform === "darwin") {
82
- // shell-quote backslash-escapes these inside the double-quoted prompt.
83
- // Leave the rest of ARG_MAX for the environment and Gemini's own args.
84
- const quotedBytes = promptBytes + (options.prompt.match(/["\\$`!]/g)?.length ?? 0);
85
- if (quotedBytes > 900 * 1024) {
86
- return inputFailure(`Gemini CLI re-encodes stdin as --prompt when launching its macOS sandbox-exec sandbox; the ${promptBytes}-byte task prompt exceeds macOS's 1 MiB argv limit. Reduce the context bundle or select another runtime that supports large prompts in its sandbox.`);
87
- }
88
- }
89
- else if (sandboxCommand && promptBytes >= 128 * 1024) {
90
- return inputFailure(`Gemini CLI re-encodes stdin as --prompt when launching its native sandbox; the ${promptBytes}-byte task prompt exceeds Linux's argv limit. Reduce the context bundle or select another runtime that supports large prompts in its sandbox.`);
91
- }
92
- const runOptions = { ...options, env };
93
- const raw = runOptions.dockerExec
94
- ? await runViaDocker(runOptions, args)
95
- : await runOnHost(runOptions, args);
96
- return attachGeminiOutput(raw);
97
- }
98
- function inputFailure(stderr) {
99
- return { exitCode: 1, timedOut: false, killedByLeaseLoss: false, stdout: "", stderr };
100
- }
101
- function geminiProcessEnv(options, env) {
102
- // Docker exec forwards only the session env names into the container.
103
- return options.dockerExec ? env : { ...process.env, ...env };
104
- }
105
- /**
106
- * Mirrors Gemini's getSandboxCommand (packages/cli/src/config/sandboxConfig.ts):
107
- * GEMINI_SANDBOX overrides the --sandbox flag and may name the command;
108
- * otherwise an enabled sandbox is sandbox-exec on macOS and a container
109
- * (docker or podman) elsewhere. Returns null when no sandbox relaunch occurs.
110
- */
111
- function geminiSandboxCommand(args, env, platform) {
112
- const configured = env.GEMINI_SANDBOX?.toLowerCase().trim() ?? "";
113
- let sandbox = configured.length > 0 ? configured : geminiSandboxFlag(args);
114
- if (sandbox === "1" || sandbox === "true")
115
- sandbox = true;
116
- else if (sandbox === "0" || sandbox === "false" || !sandbox)
117
- sandbox = false;
118
- if (sandbox === false)
119
- return null;
120
- if (typeof sandbox === "string")
121
- return sandbox;
122
- return platform === "darwin" ? "sandbox-exec" : "docker";
123
- }
124
- function geminiSandboxFlag(args) {
125
- // There is always a generated --sandbox flag unless extraArgs supplies one.
126
- // Like yargs, later explicit values override earlier ones.
127
- let enabled = false;
128
- for (let index = 0; index < args.length; index++) {
129
- const arg = args[index];
130
- if (arg === "--sandbox" || arg === "-s") {
131
- enabled = args[index + 1] !== "false";
132
- }
133
- else if (arg.startsWith("--sandbox=") || arg.startsWith("-s=")) {
134
- enabled = arg.slice(arg.indexOf("=") + 1) !== "false";
135
- }
136
- else if (arg === "--no-sandbox") {
137
- enabled = false;
138
- }
139
- }
140
- return enabled;
141
- }
142
- function hasArg(args, ...flags) {
143
- return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
144
- }
145
- function hasExplicitApprovalMode(args) {
146
- return hasArg(args, "--approval-mode", "--yolo", "-y");
147
- }
148
- async function prepareGeminiMcpSettings(mcpConfigPath, env) {
149
- const parsed = JSON.parse(await node_fs_1.promises.readFile(mcpConfigPath, "utf8"));
150
- const mcpServers = {};
151
- for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
152
- const httpUrl = server.httpUrl ?? (server.type === "http" ? server.url : undefined);
153
- if (!httpUrl && !server.url)
154
- continue;
155
- const headers = {};
156
- let headerIndex = 0;
157
- for (const [header, value] of Object.entries(server.headers ?? {})) {
158
- const envName = `NAVARCH_GEMINI_MCP_${safeEnvSegment(name)}_${headerIndex++}`;
159
- env[envName] = value;
160
- headers[header] = `\${${envName}}`;
161
- }
162
- mcpServers[name] = {
163
- ...(httpUrl ? { httpUrl } : { url: server.url }),
164
- ...(Object.keys(headers).length > 0 ? { headers } : {}),
165
- };
166
- }
167
- const settingsPath = `${mcpConfigPath}.gemini-settings.json`;
168
- await node_fs_1.promises.writeFile(settingsPath, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
169
- await node_fs_1.promises.chmod(settingsPath, 0o600);
170
- return { settingsPath, serverNames: Object.keys(mcpServers) };
171
- }
172
- function safeEnvSegment(value) {
173
- return value.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
174
- }
175
- function parseGeminiStream(stdout) {
176
- const events = [];
177
- for (const line of stdout.split(/\r?\n/)) {
178
- if (!line.trim())
179
- continue;
180
- try {
181
- const event = JSON.parse(line);
182
- if (event && typeof event === "object")
183
- events.push(event);
184
- }
185
- catch {
186
- // Parser drift must not turn a successfully completed CLI run into a
187
- // runtime crash. Unknown lines remain available in the transcript.
188
- }
189
- }
190
- return events;
191
- }
192
- function attachGeminiOutput(result) {
193
- const events = parseGeminiStream(result.stdout);
194
- if (events.length === 0)
195
- return result;
196
- let reportText = "";
197
- let tokensIn;
198
- let tokensOut;
199
- for (const event of events) {
200
- if (event.type === "tool_use") {
201
- // Gemini emits pre-tool assistant text as message deltas. Only retain
202
- // the answer produced after the last tool call as the completion report.
203
- reportText = "";
204
- }
205
- else if (event.type === "message" &&
206
- event.role === "assistant" &&
207
- typeof event.content === "string") {
208
- reportText = event.delta === false ? event.content : reportText + event.content;
209
- }
210
- else if (event.type === "result" && event.stats) {
211
- const reportedInput = nonNegativeMetric(event.stats.input_tokens);
212
- const reportedOutput = nonNegativeMetric(event.stats.output_tokens);
213
- if (reportedInput !== undefined)
214
- tokensIn = reportedInput;
215
- if (reportedOutput !== undefined)
216
- tokensOut = reportedOutput;
217
- }
218
- }
219
7
  return {
220
- ...result,
221
- ...(tokensIn !== undefined ? { tokensIn } : {}),
222
- ...(tokensOut !== undefined ? { tokensOut } : {}),
223
- ...(reportText.trim() ? { reportText: reportText.trim() } : {}),
8
+ exitCode: options.signal?.aborted ? null : 1,
9
+ timedOut: false,
10
+ killedByLeaseLoss: options.signal?.aborted ?? false,
11
+ stdout: "",
12
+ stderr: "Gemini CLI is retired. Explicitly install and authenticate Antigravity, select antigravity, and update the task's runtime pin. No Gemini credentials or conversation IDs are transferred.",
224
13
  };
225
14
  }
226
- function nonNegativeMetric(value) {
227
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
228
- }
229
- async function runOnHost(options, args) {
230
- return new Promise((resolve) => {
231
- let stdout = "";
232
- let stderr = "";
233
- let timedOut = false;
234
- let killedByLeaseLoss = false;
235
- const child = (0, node_child_process_1.spawn)(options.bin, args, {
236
- detached: process.platform !== "win32",
237
- cwd: options.cwd,
238
- env: { ...process.env, ...options.env },
239
- });
240
- // Ignore EPIPE from a child that exits before reading; `close` reports it.
241
- child.stdin?.on("error", () => undefined);
242
- child.stdin?.end(options.prompt);
243
- options.onProcessStarted?.(child.pid);
244
- const timer = options.timeoutMs > 0 ? setTimeout(() => {
245
- timedOut = true;
246
- (0, kill_agent_cjs_1.killAgent)(child);
247
- }, options.timeoutMs) : undefined;
248
- const onAbort = () => {
249
- killedByLeaseLoss = true;
250
- (0, kill_agent_cjs_1.killAgent)(child);
251
- };
252
- options.signal?.addEventListener("abort", onAbort, { once: true });
253
- if (options.signal?.aborted)
254
- onAbort();
255
- child.stdout.on("data", (data) => {
256
- options.onActivity?.();
257
- const text = data.toString();
258
- stdout += text;
259
- options.onOutput?.("stdout", text);
260
- });
261
- child.stderr.on("data", (data) => {
262
- options.onActivity?.();
263
- const text = data.toString();
264
- stderr += text;
265
- options.onOutput?.("stderr", text);
266
- });
267
- child.on("error", (err) => {
268
- clearTimeout(timer);
269
- options.signal?.removeEventListener("abort", onAbort);
270
- stderr += `\n${String(err)}`;
271
- resolve({ exitCode: null, timedOut, killedByLeaseLoss, stdout, stderr });
272
- });
273
- child.on("close", (code) => {
274
- clearTimeout(timer);
275
- options.signal?.removeEventListener("abort", onAbort);
276
- resolve({ exitCode: code, timedOut, killedByLeaseLoss, stdout, stderr });
277
- });
278
- });
279
- }
280
- async function runViaDocker(options, args) {
281
- const { containerName, runner } = options.dockerExec;
282
- const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ${[
283
- options.bin,
284
- ...args,
285
- ].map(shellQuote).join(" ")}`;
286
- let killedByLeaseLoss = false;
287
- const onAbort = () => {
288
- killedByLeaseLoss = true;
289
- runner.run("docker", ["kill", containerName], { timeoutMs: 10_000 }).catch(() => undefined);
290
- };
291
- options.signal?.addEventListener("abort", onAbort, { once: true });
292
- if (options.signal?.aborted)
293
- onAbort();
294
- try {
295
- const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
296
- const result = await runner.run("docker", ["exec", "-i", ...forwardedEnv, containerName, "sh", "-c", command], { input: options.prompt,
297
- timeoutMs: options.timeoutMs,
298
- onActivity: options.onActivity,
299
- onStdout: (chunk) => options.onOutput?.("stdout", chunk),
300
- onStderr: (chunk) => options.onOutput?.("stderr", chunk),
301
- signal: options.signal,
302
- env: { ...process.env, ...options.env }, });
303
- return {
304
- exitCode: result.code,
305
- timedOut: false,
306
- killedByLeaseLoss,
307
- stdout: result.stdout,
308
- stderr: result.stderr,
309
- };
310
- }
311
- catch (err) {
312
- return {
313
- exitCode: null,
314
- timedOut: false,
315
- killedByLeaseLoss,
316
- stdout: "",
317
- stderr: String(err),
318
- };
319
- }
320
- finally {
321
- options.signal?.removeEventListener("abort", onAbort);
322
- }
323
- }
324
- function shellQuote(value) {
325
- return `'${value.replace(/'/g, `'\\''`)}'`;
326
- }
327
- exports.geminiAdapter = {
328
- agentType: "gemini",
329
- run: runGeminiAdapter,
330
- };
15
+ exports.geminiAdapter = { agentType: "gemini", run: runGeminiAdapter };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runLocalPiAdapter = exports.localPiAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
3
+ exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runLocalPiAdapter = exports.localPiAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runAntigravityAdapter = exports.antigravityAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
4
4
  exports.selectAdapter = selectAdapter;
5
5
  const claude_cjs_1 = require("./claude.cjs");
6
6
  Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_cjs_1.claudeCodeAdapter; } });
@@ -8,6 +8,9 @@ Object.defineProperty(exports, "runClaudeCodeAdapter", { enumerable: true, get:
8
8
  const codex_cjs_1 = require("./codex.cjs");
9
9
  Object.defineProperty(exports, "codexAdapter", { enumerable: true, get: function () { return codex_cjs_1.codexAdapter; } });
10
10
  Object.defineProperty(exports, "runCodexAdapter", { enumerable: true, get: function () { return codex_cjs_1.runCodexAdapter; } });
11
+ const antigravity_cjs_1 = require("./antigravity.cjs");
12
+ Object.defineProperty(exports, "antigravityAdapter", { enumerable: true, get: function () { return antigravity_cjs_1.antigravityAdapter; } });
13
+ Object.defineProperty(exports, "runAntigravityAdapter", { enumerable: true, get: function () { return antigravity_cjs_1.runAntigravityAdapter; } });
11
14
  const gemini_cjs_1 = require("./gemini.cjs");
12
15
  Object.defineProperty(exports, "geminiAdapter", { enumerable: true, get: function () { return gemini_cjs_1.geminiAdapter; } });
13
16
  Object.defineProperty(exports, "runGeminiAdapter", { enumerable: true, get: function () { return gemini_cjs_1.runGeminiAdapter; } });
@@ -37,6 +40,8 @@ function selectAdapter(agentType) {
37
40
  switch (agentType) {
38
41
  case "codex":
39
42
  return codex_cjs_1.codexAdapter;
43
+ case "antigravity":
44
+ return antigravity_cjs_1.antigravityAdapter;
40
45
  case "gemini":
41
46
  return gemini_cjs_1.geminiAdapter;
42
47
  case "opencode":
@@ -43,6 +43,8 @@ function matchingLine(text) {
43
43
  function detectAgentAuthFailure(result) {
44
44
  if (result.killedByLeaseLoss || result.timedOut || result.capacityError)
45
45
  return null;
46
+ if (result.authError)
47
+ return { detail: result.authError };
46
48
  const claude = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout) ?? (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stderr);
47
49
  if (claude) {
48
50
  if (!claude.is_error && result.exitCode === 0)
package/dist/config.cjs CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OPENCODE_HOST_GUARD_MESSAGE = exports.RUNTIME_AGENT_TYPES = exports.DEFAULT_SANDBOX_IMAGE = void 0;
6
+ exports.OPENCODE_HOST_GUARD_MESSAGE = exports.ANTIGRAVITY_HOST_GUARD_MESSAGE = exports.RUNTIME_AGENT_TYPES = exports.DEFAULT_SANDBOX_IMAGE = void 0;
7
7
  exports.isRuntimeAgentType = isRuntimeAgentType;
8
8
  exports.unknownRuntimeIds = unknownRuntimeIds;
9
9
  exports.opencodeBlockedOnHost = opencodeBlockedOnHost;
@@ -21,7 +21,7 @@ exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
21
21
  exports.RUNTIME_AGENT_TYPES = [
22
22
  "claude-code",
23
23
  "codex",
24
- "gemini",
24
+ "antigravity",
25
25
  "opencode",
26
26
  "acp",
27
27
  "qoder",
@@ -77,6 +77,9 @@ function envList(env, name, fallback) {
77
77
  function opencodeBlockedOnHost(sandboxMode, worktreeGuard) {
78
78
  return sandboxMode === "host" && worktreeGuard;
79
79
  }
80
+ exports.ANTIGRAVITY_HOST_GUARD_MESSAGE = "Antigravity cannot enforce Navarch's host worktree boundary or read-only Navi mode. " +
81
+ "Use an isolated Docker image with agy and Node installed, or explicitly set NAVARCH_WORKTREE_GUARD=off " +
82
+ "only on a dedicated trusted machine. Antigravity inherits that machine's global configuration.";
80
83
  exports.OPENCODE_HOST_GUARD_MESSAGE = "OpenCode cannot enforce Navarch's host worktree boundary for shell side effects. " +
81
84
  "Use NAVARCH_SANDBOX_MODE=docker, or explicitly set NAVARCH_WORKTREE_GUARD=off " +
82
85
  "only on an otherwise isolated machine.";
@@ -86,6 +89,9 @@ exports.OPENCODE_HOST_GUARD_MESSAGE = "OpenCode cannot enforce Navarch's host wo
86
89
  * it is trivially unit-testable without mutating global state.
87
90
  */
88
91
  function loadRuntimeConfig(env = process.env) {
92
+ if (env.NAVARCH_AGENT === "gemini" || envList(env, "NAVARCH_RUNTIMES", []).includes("gemini")) {
93
+ throw new Error("Gemini CLI is retired. Install and authenticate Antigravity separately, then explicitly select NAVARCH_AGENT=antigravity (or --agent antigravity) and update NAVARCH_RUNTIMES. Existing Gemini config, credentials and sessions are not migrated.");
94
+ }
89
95
  const configDir = env.NAVARCH_CONFIG_DIR ?? node_path_1.default.join(node_os_1.default.homedir(), ".navarch");
90
96
  const leaseHeartbeatIntervalMs = envInt(env, "NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS", 5 * 60 * 1000);
91
97
  // Run the agent with the resources already available on its machine by
@@ -108,10 +114,17 @@ function loadRuntimeConfig(env = process.env) {
108
114
  // time; keep it out of the claim instead of discovering this per session.
109
115
  const runnableRuntimes = requestedRuntimes.filter((runtime) => runtime === "pi-local"
110
116
  ? sandboxMode === "host" && !worktreeGuard
111
- : runtime !== "opencode" || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
117
+ : runtime === "antigravity"
118
+ ? process.platform !== "win32" && !opencodeBlockedOnHost(sandboxMode, worktreeGuard)
119
+ : runtime !== "opencode" || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
112
120
  if (requestedRuntimes.includes("pi-local") && runnableRuntimes.length === 0) {
113
121
  throw new Error("Pi (local) requires NAVARCH_SANDBOX_MODE=host and NAVARCH_WORKTREE_GUARD=off");
114
122
  }
123
+ if (requestedRuntimes.includes("antigravity") && runnableRuntimes.length === 0) {
124
+ if (process.platform === "win32")
125
+ throw new Error("Antigravity's private lease MCP relay requires a Linux or macOS worker; Windows is unsupported.");
126
+ throw new Error(exports.ANTIGRAVITY_HOST_GUARD_MESSAGE);
127
+ }
115
128
  if (requestedRuntimes.includes("opencode") && runnableRuntimes.length === 0) {
116
129
  throw new Error(exports.OPENCODE_HOST_GUARD_MESSAGE);
117
130
  }
@@ -143,6 +156,8 @@ function loadRuntimeConfig(env = process.env) {
143
156
  claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
144
157
  codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
145
158
  codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
159
+ antigravityBin: env.NAVARCH_ANTIGRAVITY_BIN ?? "agy",
160
+ antigravityExtraArgs: envList(env, "NAVARCH_ANTIGRAVITY_EXTRA_ARGS", []),
146
161
  geminiBin: env.NAVARCH_GEMINI_BIN ?? "gemini",
147
162
  geminiExtraArgs: envList(env, "NAVARCH_GEMINI_EXTRA_ARGS", []),
148
163
  opencodeBin: env.NAVARCH_OPENCODE_BIN ?? "opencode",
@@ -40,7 +40,7 @@ function conversationCapability(config) {
40
40
  }
41
41
  /** Per-adapter support table for docs and diagnostics (host sandbox, guard on, feature enabled). */
42
42
  function conversationSupportMatrix() {
43
- const agents = ["claude-code", "codex", "gemini", "opencode", "acp", "qoder", "pi-local", "pi"];
43
+ const agents = ["claude-code", "codex", "gemini", "antigravity", "opencode", "acp", "qoder", "pi-local", "pi"];
44
44
  return agents.map((agentType) => ({
45
45
  agentType,
46
46
  capability: conversationCapability({
@@ -209,9 +209,9 @@ class ConversationLoop {
209
209
  ...(shutdownSignal ? { signal: shutdownSignal } : {}),
210
210
  ...this.options.turnOverrides,
211
211
  log: this.log,
212
- prepareWorkspace: async ({ conversationId, repositoryUrl, mode, signal }) => {
212
+ prepareWorkspace: async ({ conversationId, repositoryUrl, workingBranch, mode, signal }) => {
213
213
  const workspace = await (0, workspace_cjs_1.prepareConversationWorkspace)({
214
- workspaceRoot: config.workspaceRoot, conversationId, repositoryUrl, env, signal, mode,
214
+ workspaceRoot: config.workspaceRoot, conversationId, repositoryUrl, workingBranch, env, signal, mode,
215
215
  ...(this.options.gitRunner ? { runner: this.options.gitRunner } : {}),
216
216
  });
217
217
  const guard = await (0, worktree_guard_cjs_1.prepareWorktreeGuard)({
@@ -298,7 +298,9 @@ async function runConversationTurn(deps, conversationId, turnId) {
298
298
  if (snapshot.mode === "workspace" && !deps.workspaceEnabled)
299
299
  throw new Error("Workspace chat is disabled on this machine");
300
300
  prompt = (0, prompt_cjs_1.buildConversationPrompt)(snapshot, journal.turnId);
301
- prepared = await deps.prepareWorkspace({ conversationId, repositoryUrl, mode: snapshot.mode ?? "read_only", signal: controller.signal });
301
+ // Older servers omit the working branch: the checkout stays on the default branch.
302
+ const workingBranch = (0, workspace_cjs_1.validWorkingBranch)(claim.workingBranch);
303
+ prepared = await deps.prepareWorkspace({ conversationId, repositoryUrl, workingBranch, mode: snapshot.mode ?? "read_only", signal: controller.signal });
302
304
  }
303
305
  catch (error) {
304
306
  if (!run.lost)
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.conversationEnv = conversationEnv;
7
7
  exports.validRepositoryUrl = validRepositoryUrl;
8
+ exports.validWorkingBranch = validWorkingBranch;
8
9
  exports.conversationRoot = conversationRoot;
9
10
  exports.prepareConversationWorkspace = prepareConversationWorkspace;
10
11
  exports.removeConversationWorkspace = removeConversationWorkspace;
@@ -50,6 +51,22 @@ function validRepositoryUrl(value) {
50
51
  return null;
51
52
  return `https://github.com/${fullName}.git`;
52
53
  }
54
+ /**
55
+ * The project's working branch from the claim, checked as strictly as the
56
+ * hosted path's `buildWorkingBranchCheckoutCommand`
57
+ * (worker/hosted-agent/delivery.ts): a plain ref name that cannot be read as a
58
+ * git option. Absent (an older server, or no working branch) is null; anything
59
+ * else that is not a plain name throws, so the turn never runs on a branch it
60
+ * was not given.
61
+ */
62
+ function validWorkingBranch(value) {
63
+ if (value === undefined || value === null)
64
+ return null;
65
+ if (typeof value !== "string" || value.length > 255 || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(value) || value.includes("..")) {
66
+ throw new Error("working branch is not a valid branch name");
67
+ }
68
+ return value;
69
+ }
53
70
  function conversationRoot(workspaceRoot, conversationId) {
54
71
  return node_path_1.default.join(workspaceRoot, "conversations", (0, journal_cjs_1.assertUuid)(conversationId, "conversation id"));
55
72
  }
@@ -67,6 +84,11 @@ async function prepareConversationWorkspace(options) {
67
84
  const url = validRepositoryUrl(options.repositoryUrl);
68
85
  if (!url)
69
86
  throw new Error("repository URL is not a plain https URL");
87
+ const branch = validWorkingBranch(options.workingBranch);
88
+ // The marker records what the checkout was cloned from, so a changed
89
+ // repository or working branch is noticed. Default-branch checkouts keep the
90
+ // bare URL that earlier runtimes wrote.
91
+ const identity = branch ? `${url}\nbranch=${branch}` : url;
70
92
  const runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
71
93
  const root = options.mode === "workspace"
72
94
  ? node_path_1.default.join(options.workspaceRoot, "chat-workspaces", (0, journal_cjs_1.assertUuid)(options.conversationId, "conversation id"))
@@ -83,7 +105,7 @@ async function prepareConversationWorkspace(options) {
83
105
  if (options.mode === "workspace") {
84
106
  await node_fs_1.promises.writeFile(node_path_1.default.join(workspace.metaDir, "preserve-workspace"), "Workspace chat: retain until the operator removes it.\n", { mode: 0o600 });
85
107
  }
86
- if (hasCheckout && recorded === url)
108
+ if (hasCheckout && recorded === identity)
87
109
  return workspace;
88
110
  if (options.mode === "workspace") {
89
111
  const exists = await node_fs_1.promises.lstat(workspace.checkout).then(() => true, (error) => {
@@ -94,7 +116,7 @@ async function prepareConversationWorkspace(options) {
94
116
  if (exists)
95
117
  throw new Error("Checkout identity changed; preserving the existing workspace. Start a new conversation.");
96
118
  }
97
- // New conversation, a changed repository URL, or an interrupted clone.
119
+ // New conversation, a changed repository URL or working branch, or an interrupted clone.
98
120
  await node_fs_1.promises.rm(workspace.checkout, { recursive: true, force: true });
99
121
  const env = {
100
122
  ...options.env,
@@ -104,12 +126,12 @@ async function prepareConversationWorkspace(options) {
104
126
  GIT_CONFIG_KEY_0: "core.hooksPath",
105
127
  GIT_CONFIG_VALUE_0: process.platform === "win32" ? "NUL" : "/dev/null",
106
128
  };
107
- const result = await runner.run("git", ["clone", "--quiet", "--filter=blob:none", "--no-tags", "--", url, workspace.checkout], { cwd: root, env, timeoutMs: CLONE_TIMEOUT_MS, signal: options.signal });
129
+ const result = await runner.run("git", ["clone", "--quiet", "--filter=blob:none", "--no-tags", ...(branch ? ["--branch", branch] : []), "--", url, workspace.checkout], { cwd: root, env, timeoutMs: CLONE_TIMEOUT_MS, signal: options.signal });
108
130
  if (result.code !== 0) {
109
131
  await node_fs_1.promises.rm(workspace.checkout, { recursive: true, force: true }).catch(() => undefined);
110
132
  throw new Error(`git clone failed with exit ${result.code}: ${result.stderr.slice(-500)}`);
111
133
  }
112
- await node_fs_1.promises.writeFile(marker, url, { encoding: "utf8", mode: 0o600 });
134
+ await node_fs_1.promises.writeFile(marker, identity, { encoding: "utf8", mode: 0o600 });
113
135
  return workspace;
114
136
  }
115
137
  async function removeConversationWorkspace(workspaceRoot, conversationId) {
@@ -22,7 +22,7 @@ exports.mapExitCondition = mapExitCondition;
22
22
  exports.AGENT_AUTH_FAILURE_PREFIX = "Coding agent authentication failed";
23
23
  function agentAuthFailureReport(detail) {
24
24
  return `${exports.AGENT_AUTH_FAILURE_PREFIX}: ${detail} ` +
25
- "Log the coding agent in again on this machine (`claude /login` or `codex login`), then resume the agent.";
25
+ "Log the coding agent in again on this machine (`claude /login`, `codex login`, or an interactive `agy` session), then resume the agent.";
26
26
  }
27
27
  /**
28
28
  * Best-effort parse of `claude -p --output-format json` (or `stream-json`,