@sagentlab/navarch-runtime 0.1.142 → 0.1.143

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/README.md CHANGED
@@ -592,7 +592,7 @@ unchanged across the deployment.
592
592
  | `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
593
593
  | `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable it for every runtime on the machine — required to run OpenCode or Muse on an already-isolated host. |
594
594
  | `NAVARCH_NAVI_CONVERSATIONS` | on | Serve read-only Navi repository conversations on this machine. Only the `claude-code` adapter on the host sandbox with the worktree guard on is supported; `navarch-runtime doctor` prints the `navi_chat:` state and reason. Set `off` to stop polling for conversations. |
595
- | `NAVARCH_NAVI_WORKSPACE_CHAT` | off | Explicitly enable writable Navi chat on a supported Claude Code host runtime. Generated Claude Code host connect and supervise commands include it; remove it from both lines to keep chat read-only. Owners can choose **Edit and run commands**; automatic permission review and the worktree guard remain active. Commands run on this host, not in a hard sandbox. Changes stay under `<workspaceRoot>/chat-workspaces/<conversationId>/repo` until the operator removes them. See [workspace chat](../docs/navarch/navi-workspace-chat.md). |
595
+ | `NAVARCH_NAVI_WORKSPACE_CHAT` | off | Explicitly enable writable Navi chat on a supported Claude Code or Codex host runtime. Generated host connect and supervise commands for both adapters include it; remove it from both lines to keep chat read-only. Owners can choose **Edit and run commands**; Claude uses automatic permission review and its worktree hook, while Codex uses a native checkout permission profile with escalation disabled. Commands run on this host, not in a hard sandbox. Changes stay under `<workspaceRoot>/chat-workspaces/<conversationId>/repo` until the operator removes them. See [workspace chat](../docs/navarch/navi-workspace-chat.md). |
596
596
  | `NAVARCH_CONVERSATION_POLL_MS` | `5000` | Idle poll interval for queued conversation turns (1000–30000). |
597
597
  | `NAVARCH_CONVERSATION_MIRROR` | on | Seed new conversation checkouts from a per-repository mirror under `<workspaceRoot>/conversation-mirrors/` (fetch + local clone with independent object files) instead of a full network clone. Set `off` to always clone directly. |
598
598
  | `NAVARCH_CONVERSATION_TURN_TIMEOUT_MS` | `540000` | Time limit for one conversation turn's agent process (at most nine minutes). |
@@ -3,10 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.claudeConversationRunner = exports.ClaudeStreamParser = exports.CLAUDE_WORKSPACE_TOOLS = exports.CLAUDE_CONVERSATION_TOOLS = void 0;
4
4
  exports.claudeConversationArgs = claudeConversationArgs;
5
5
  exports.runClaudeConversationTurn = runClaudeConversationTurn;
6
- const node_child_process_1 = require("node:child_process");
7
- const node_perf_hooks_1 = require("node:perf_hooks");
8
- const kill_agent_cjs_1 = require("../kill-agent.cjs");
9
- const session_processes_cjs_1 = require("../session-processes.cjs");
6
+ const conversation_process_cjs_1 = require("./conversation-process.cjs");
10
7
  /**
11
8
  * Read-only tools a Navi repository question may use. Anything not listed
12
9
  * (Bash, Edit, Write, WebFetch, Task, MCP tools) is absent from the model's
@@ -17,7 +14,6 @@ const session_processes_cjs_1 = require("../session-processes.cjs");
17
14
  */
18
15
  exports.CLAUDE_CONVERSATION_TOOLS = "Read,Grep,Glob";
19
16
  exports.CLAUDE_WORKSPACE_TOOLS = "Read,Grep,Glob,Edit,Write,Bash";
20
- const STDERR_TAIL_BYTES = 8 * 1024;
21
17
  /**
22
18
  * Incremental parser for `claude -p --output-format stream-json --verbose
23
19
  * --include-partial-messages`. Chunks may split a record anywhere. Text is
@@ -194,90 +190,17 @@ function claudeConversationArgs(options) {
194
190
  return args;
195
191
  }
196
192
  async function runClaudeConversationTurn(options) {
197
- const idle = (aborted) => ({
198
- exitCode: null, timedOut: false, aborted, succeeded: false, firstEventMs: null, firstTextMs: null, stderr: "",
199
- });
200
- if (options.signal.aborted)
201
- return idle(true);
202
- const startedAt = node_perf_hooks_1.performance.now();
203
- let firstEventMs = null;
204
- let firstTextMs = null;
205
- let timedOut = false;
206
- let aborted = false;
207
- let closed = false;
208
- let stderr = "";
209
- const deliver = (event) => {
210
- // After stop, timeout or exit, late output must not reach the caller.
211
- if (closed || aborted || timedOut)
212
- return;
213
- if (event.type === "text" && firstTextMs === null)
214
- firstTextMs = node_perf_hooks_1.performance.now() - startedAt;
215
- options.onEvent(event);
216
- };
217
- const parser = new ClaudeStreamParser(deliver, () => {
218
- if (firstEventMs === null)
219
- firstEventMs = node_perf_hooks_1.performance.now() - startedAt;
220
- });
221
- return new Promise((resolve) => {
222
- const child = (0, node_child_process_1.spawn)(options.bin, claudeConversationArgs(options), {
223
- detached: process.platform !== "win32",
224
- cwd: options.cwd,
225
- env: options.env,
226
- stdio: ["pipe", "pipe", "pipe"],
227
- });
228
- child.stdin?.on("error", () => undefined);
229
- child.stdin?.end(options.prompt);
230
- options.onProcessStarted?.(child.pid);
231
- const timer = options.timeoutMs > 0 ? setTimeout(() => {
232
- timedOut = true;
233
- (0, kill_agent_cjs_1.killAgent)(child);
234
- }, options.timeoutMs) : undefined;
235
- const onAbort = () => {
236
- aborted = true;
237
- (0, kill_agent_cjs_1.killAgent)(child);
238
- };
239
- options.signal.addEventListener("abort", onAbort, { once: true });
240
- child.stdout.setEncoding("utf8");
241
- child.stdout.on("data", (chunk) => {
242
- parser.push(chunk);
243
- });
244
- child.stderr.setEncoding("utf8");
245
- child.stderr.on("data", (chunk) => {
246
- stderr = (stderr + chunk).slice(-STDERR_TAIL_BYTES);
247
- });
248
- const finish = (exitCode) => {
249
- if (closed)
250
- return;
251
- parser.flush();
252
- // An answer delivered only in the final result record (no stream deltas).
253
- if (!parser.hasEmittedText && parser.result && !parser.result.isError && parser.result.text) {
254
- deliver({ type: "text", text: parser.result.text });
255
- }
256
- closed = true;
257
- clearTimeout(timer);
258
- options.signal.removeEventListener("abort", onAbort);
259
- resolve({
260
- exitCode, timedOut, aborted,
261
- succeeded: exitCode === 0 && !aborted && !timedOut && !!parser.result && !parser.result.isError,
262
- firstEventMs, firstTextMs, stderr,
263
- });
193
+ return (0, conversation_process_cjs_1.runConversationProcess)(options, claudeConversationArgs(options), (emit, onRecord) => {
194
+ const parser = new ClaudeStreamParser(emit, onRecord);
195
+ return {
196
+ push: (chunk) => parser.push(chunk),
197
+ flush: () => {
198
+ parser.flush();
199
+ if (!parser.hasEmittedText && parser.result && !parser.result.isError && parser.result.text)
200
+ emit({ type: "text", text: parser.result.text });
201
+ },
202
+ get succeeded() { return !!parser.result && !parser.result.isError; },
264
203
  };
265
- child.on("error", (err) => {
266
- stderr = (stderr + `\n${String(err)}`).slice(-STDERR_TAIL_BYTES);
267
- finish(null);
268
- });
269
- let cleanup = Promise.resolve();
270
- child.on("exit", () => {
271
- if (options.mode !== "workspace")
272
- return;
273
- (0, kill_agent_cjs_1.killAgent)(child);
274
- // Start on exit, not close: a detached tool may still hold the output
275
- // pipes open. Reap only this checkout before acknowledging the turn.
276
- cleanup = (0, session_processes_cjs_1.reapSessionProcesses)(options.cwd);
277
- });
278
- child.on("close", (code) => { void cleanup.then(() => finish(code)); });
279
- if (options.signal.aborted)
280
- onAbort();
281
204
  });
282
205
  }
283
206
  exports.claudeConversationRunner = { run: runClaudeConversationTurn };
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codexConversationRunner = exports.CodexConversationParser = void 0;
4
+ exports.codexConversationArgs = codexConversationArgs;
5
+ const conversation_process_cjs_1 = require("./conversation-process.cjs");
6
+ /** Codex emits completed assistant messages; reasoning and command output stay local. */
7
+ class CodexConversationParser {
8
+ emit;
9
+ onRecord;
10
+ buffer = "";
11
+ emitted = false;
12
+ seen = new Set();
13
+ completed = false;
14
+ failed = false;
15
+ get succeeded() { return this.completed && !this.failed; }
16
+ constructor(emit, onRecord = () => undefined) {
17
+ this.emit = emit;
18
+ this.onRecord = onRecord;
19
+ }
20
+ push(chunk) {
21
+ this.buffer += chunk;
22
+ if (this.buffer.length > 2_000_000) {
23
+ this.failed = true;
24
+ this.buffer = "";
25
+ return;
26
+ }
27
+ let end;
28
+ while ((end = this.buffer.indexOf("\n")) >= 0) {
29
+ this.record(this.buffer.slice(0, end));
30
+ this.buffer = this.buffer.slice(end + 1);
31
+ }
32
+ }
33
+ flush() { if (this.buffer.trim())
34
+ this.record(this.buffer); this.buffer = ""; }
35
+ record(line) {
36
+ let event;
37
+ try {
38
+ event = JSON.parse(line);
39
+ }
40
+ catch {
41
+ return;
42
+ }
43
+ if (!event || typeof event !== "object")
44
+ return;
45
+ this.onRecord();
46
+ if (event.type === "turn.failed" || event.type === "error") {
47
+ this.failed = true;
48
+ this.emit({ type: "failure", reason: "codex_turn_failed" });
49
+ }
50
+ else if (event.type === "turn.completed")
51
+ this.completed = true;
52
+ else if (event.type === "item.completed") {
53
+ const item = event.item;
54
+ if (item?.type !== "agent_message" || typeof item.text !== "string" || !item.text)
55
+ return;
56
+ if (typeof item.id === "string") {
57
+ if (this.seen.has(item.id))
58
+ return;
59
+ this.seen.add(item.id);
60
+ }
61
+ this.emit({ type: "text", text: `${this.emitted ? "\n\n" : ""}${item.text}` });
62
+ this.emitted = true;
63
+ }
64
+ }
65
+ }
66
+ exports.CodexConversationParser = CodexConversationParser;
67
+ function codexConversationArgs(options) {
68
+ if (!options.codexGuardArgs?.length)
69
+ throw new Error("Codex chat requires a native checkout permission profile.");
70
+ if (options.mode && options.mode !== "read_only" && options.mode !== "workspace")
71
+ throw new Error("Unknown conversation mode.");
72
+ return [
73
+ "exec", "-", "--json", "--ephemeral", "--ignore-rules",
74
+ ...options.codexGuardArgs,
75
+ "-c", 'approval_policy="never"',
76
+ "-c", `projects.${JSON.stringify(options.cwd)}.trust_level="untrusted"`,
77
+ "-c", 'mcp_servers={}',
78
+ "-c", 'features.multi_agent=false',
79
+ "-c", 'web_search="disabled"',
80
+ ...(options.model ? ["--model", options.model] : []),
81
+ ];
82
+ }
83
+ exports.codexConversationRunner = {
84
+ run: (options) => (0, conversation_process_cjs_1.runConversationProcess)(options, codexConversationArgs(options), (emit, onRecord) => new CodexConversationParser(emit, onRecord)),
85
+ };
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codexAdapter = void 0;
4
4
  exports.runCodexAdapter = runCodexAdapter;
5
5
  exports.hasExplicitPermissionPolicy = hasExplicitPermissionPolicy;
6
+ const codex_conversation_cjs_1 = require("./codex-conversation.cjs");
6
7
  const node_child_process_1 = require("node:child_process");
7
8
  const node_fs_1 = require("node:fs");
8
9
  const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
@@ -368,6 +369,7 @@ function shellQuote(value) {
368
369
  exports.codexAdapter = {
369
370
  agentType: "codex",
370
371
  run: runCodexAdapter,
372
+ conversation: codex_conversation_cjs_1.codexConversationRunner,
371
373
  resumesTurns: true,
372
374
  steering: types_cjs_1.TURN_RESTART_STEERING,
373
375
  // `exec resume` reports the thread's total_token_usage, not the turn's.
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runConversationProcess = runConversationProcess;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_perf_hooks_1 = require("node:perf_hooks");
6
+ const kill_agent_cjs_1 = require("../kill-agent.cjs");
7
+ const session_processes_cjs_1 = require("../session-processes.cjs");
8
+ const STDERR_TAIL_BYTES = 8 * 1024;
9
+ async function runConversationProcess(options, args, makeParser) {
10
+ const idle = (aborted) => ({
11
+ exitCode: null, timedOut: false, aborted, succeeded: false, firstEventMs: null, firstTextMs: null, stderr: "",
12
+ });
13
+ if (options.signal.aborted)
14
+ return idle(true);
15
+ const startedAt = node_perf_hooks_1.performance.now();
16
+ let firstEventMs = null;
17
+ let firstTextMs = null;
18
+ let timedOut = false;
19
+ let aborted = false;
20
+ let closed = false;
21
+ let stderr = "";
22
+ const deliver = (event) => {
23
+ // After stop, timeout or exit, late output must not reach the caller.
24
+ if (closed || aborted || timedOut)
25
+ return;
26
+ if (event.type === "text" && firstTextMs === null)
27
+ firstTextMs = node_perf_hooks_1.performance.now() - startedAt;
28
+ options.onEvent(event);
29
+ };
30
+ const parser = makeParser(deliver, () => {
31
+ if (firstEventMs === null)
32
+ firstEventMs = node_perf_hooks_1.performance.now() - startedAt;
33
+ });
34
+ return new Promise((resolve) => {
35
+ const child = (0, node_child_process_1.spawn)(options.bin, args, {
36
+ detached: process.platform !== "win32",
37
+ cwd: options.cwd,
38
+ env: options.env,
39
+ stdio: ["pipe", "pipe", "pipe"],
40
+ });
41
+ child.stdin?.on("error", () => undefined);
42
+ child.stdin?.end(options.prompt);
43
+ options.onProcessStarted?.(child.pid);
44
+ const timer = options.timeoutMs > 0 ? setTimeout(() => {
45
+ timedOut = true;
46
+ (0, kill_agent_cjs_1.killAgent)(child);
47
+ }, options.timeoutMs) : undefined;
48
+ const onAbort = () => {
49
+ aborted = true;
50
+ (0, kill_agent_cjs_1.killAgent)(child);
51
+ };
52
+ options.signal.addEventListener("abort", onAbort, { once: true });
53
+ child.stdout.setEncoding("utf8");
54
+ child.stdout.on("data", (chunk) => {
55
+ parser.push(chunk);
56
+ });
57
+ child.stderr.setEncoding("utf8");
58
+ child.stderr.on("data", (chunk) => {
59
+ stderr = (stderr + chunk).slice(-STDERR_TAIL_BYTES);
60
+ });
61
+ const finish = (exitCode) => {
62
+ if (closed)
63
+ return;
64
+ parser.flush();
65
+ closed = true;
66
+ clearTimeout(timer);
67
+ options.signal.removeEventListener("abort", onAbort);
68
+ resolve({
69
+ exitCode, timedOut, aborted,
70
+ succeeded: exitCode === 0 && !aborted && !timedOut && parser.succeeded,
71
+ firstEventMs, firstTextMs, stderr,
72
+ });
73
+ };
74
+ child.on("error", (err) => {
75
+ stderr = (stderr + `\n${String(err)}`).slice(-STDERR_TAIL_BYTES);
76
+ finish(null);
77
+ });
78
+ let cleanup = Promise.resolve();
79
+ child.on("exit", () => {
80
+ if (options.mode !== "workspace")
81
+ return;
82
+ (0, kill_agent_cjs_1.killAgent)(child);
83
+ // Start on exit, not close: a detached tool may still hold the output
84
+ // pipes open. Reap only this checkout before acknowledging the turn.
85
+ cleanup = (0, session_processes_cjs_1.reapSessionProcesses)(options.cwd);
86
+ });
87
+ child.on("close", (code) => { void cleanup.then(() => finish(code)); });
88
+ if (options.signal.aborted)
89
+ onAbort();
90
+ });
91
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConversationLoop = void 0;
4
+ const node_fs_1 = require("node:fs");
4
5
  const index_cjs_1 = require("../adapters/index.cjs");
5
6
  const logger_cjs_1 = require("../logger.cjs");
6
7
  const worktree_guard_cjs_1 = require("../worktree-guard.cjs");
@@ -216,7 +217,7 @@ class ConversationLoop {
216
217
  machineId,
217
218
  journal: this.journal,
218
219
  adapter: (0, index_cjs_1.selectAdapter)(config.agentType),
219
- bin: config.claudeBin,
220
+ bin: config.agentType === "codex" ? config.codexBin : config.claudeBin,
220
221
  ...(config.model ? { model: config.model } : {}),
221
222
  env,
222
223
  turnTimeoutMs: config.conversationTurnTimeoutMs,
@@ -230,6 +231,19 @@ class ConversationLoop {
230
231
  ...(this.options.gitRunner ? { runner: this.options.gitRunner } : {}),
231
232
  ...(this.mirrors ? { mirrors: this.mirrors } : {}),
232
233
  });
234
+ if (config.agentType === "codex") {
235
+ const tmp = (0, worktree_guard_cjs_1.sessionCodexTmpDir)(workspace.metaDir);
236
+ await node_fs_1.promises.mkdir(tmp, { recursive: true, mode: 0o700 });
237
+ return {
238
+ cwd: workspace.checkout, settingsPath: null, env: { TMPDIR: tmp, TMP: tmp, TEMP: tmp },
239
+ codexGuardArgs: (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
240
+ workDir: workspace.metaDir, worktreePath: workspace.checkout,
241
+ repositoryPath: workspace.checkout, workspaceRoot: config.workspaceRoot,
242
+ // Chat never imports operator extra roots or task credentials.
243
+ codexBin: config.codexBin, readOnly: mode !== "workspace",
244
+ }),
245
+ };
246
+ }
233
247
  const guard = await (0, worktree_guard_cjs_1.prepareWorktreeGuard)({
234
248
  workDir: workspace.metaDir,
235
249
  worktreePath: workspace.checkout,
@@ -32,7 +32,7 @@ const WORKSPACE_INSTRUCTIONS = [
32
32
  * session, so a follow-up works the same after a runtime restart and no
33
33
  * unbounded transcript is kept on disk.
34
34
  */
35
- function buildConversationPrompt(snapshot, turnId) {
35
+ function buildConversationPrompt(snapshot, turnId, readCommands = false) {
36
36
  const index = snapshot.turns.findIndex((turn) => turn.id === turnId);
37
37
  const current = snapshot.turns[index];
38
38
  if (!current || current.status !== "running")
@@ -56,7 +56,9 @@ function buildConversationPrompt(snapshot, turnId) {
56
56
  }
57
57
  const omitted = prior.length - kept.length;
58
58
  return [
59
- snapshot.mode === "workspace" ? WORKSPACE_INSTRUCTIONS : INSTRUCTIONS,
59
+ snapshot.mode === "workspace" ? WORKSPACE_INSTRUCTIONS : readCommands
60
+ ? INSTRUCTIONS.replace("You cannot edit files, run commands,", "You may run sandboxed read-only inspection commands. You cannot edit files, run mutating commands,")
61
+ : INSTRUCTIONS,
60
62
  kept.length || omitted
61
63
  ? `\nEarlier conversation${omitted ? ` (${omitted} older turn${omitted === 1 ? "" : "s"} omitted)` : ""}:\n${kept.join("\n\n")}`
62
64
  : "",
@@ -297,7 +297,7 @@ async function runConversationTurn(deps, conversationId, turnId) {
297
297
  throw new Error("Unknown conversation mode");
298
298
  if (snapshot.mode === "workspace" && !deps.workspaceEnabled)
299
299
  throw new Error("Workspace chat is disabled on this machine");
300
- prompt = (0, prompt_cjs_1.buildConversationPrompt)(snapshot, journal.turnId);
300
+ prompt = (0, prompt_cjs_1.buildConversationPrompt)(snapshot, journal.turnId, deps.adapter.agentType === "codex");
301
301
  // Older servers omit the working branch: the checkout stays on the default branch.
302
302
  const workingBranch = (0, workspace_cjs_1.validWorkingBranch)(claim.workingBranch);
303
303
  prepared = await deps.prepareWorkspace({ conversationId, repositoryUrl, workingBranch, mode: snapshot.mode ?? "read_only", signal: controller.signal });
@@ -313,7 +313,7 @@ async function runConversationTurn(deps, conversationId, turnId) {
313
313
  run.launchedAt = node_perf_hooks_1.performance.now();
314
314
  const result = await deps.adapter.conversation.run({
315
315
  prompt, bin: deps.bin, ...(deps.model ? { model: deps.model } : {}),
316
- cwd: prepared.cwd, env: deps.env, settingsPath: prepared.settingsPath, mode: snapshot?.mode ?? "read_only",
316
+ cwd: prepared.cwd, env: { ...deps.env, ...prepared.env }, settingsPath: prepared.settingsPath, codexGuardArgs: prepared.codexGuardArgs, mode: snapshot?.mode ?? "read_only",
317
317
  timeoutMs: deps.turnTimeoutMs, signal: controller.signal, onEvent,
318
318
  });
319
319
  metrics = { firstEventMs: result.firstEventMs, firstTextMs: result.firstTextMs };
@@ -423,8 +423,8 @@ function codexWorktreeGuardArgs(options, platform = process.platform) {
423
423
  ":slash_tmp": "write",
424
424
  [workspaceRoot]: "deny",
425
425
  }),
426
- [node_path_1.default.resolve(options.worktreePath)]: "write",
427
- [node_path_1.default.resolve(options.repositoryPath)]: "write",
426
+ [node_path_1.default.resolve(options.worktreePath)]: options.readOnly ? "read" : "write",
427
+ [node_path_1.default.resolve(options.repositoryPath)]: options.readOnly ? "read" : "write",
428
428
  [sessionGhConfigDir(options.workDir)]: "read",
429
429
  };
430
430
  // git runs the credential helper inside the sandbox. Without these grants
@@ -443,7 +443,7 @@ function codexWorktreeGuardArgs(options, platform = process.platform) {
443
443
  // the runtime-created gitdir explicitly, without reopening session metadata.
444
444
  const gitdir = linkedWorktreeGitDir(options.worktreePath, options.repositoryPath);
445
445
  if (gitdir)
446
- filesystem[gitdir] = "write";
446
+ filesystem[gitdir] = options.readOnly ? "read" : "write";
447
447
  const canonicalWorkspace = canonicalPathWithMissingTail(workspaceRoot);
448
448
  const overlapsWorkspace = (root) => {
449
449
  const canonicalRoot = canonicalPathWithMissingTail(root);
@@ -469,7 +469,7 @@ function codexWorktreeGuardArgs(options, platform = process.platform) {
469
469
  // reopen sibling sessions or metadata inside the Navarch workspace.
470
470
  if (overlapsWorkspace(resolved))
471
471
  continue;
472
- filesystem[resolved] = "write";
472
+ filesystem[resolved] = options.readOnly ? "read" : "write";
473
473
  }
474
474
  return [
475
475
  "--ignore-user-config",
@@ -489,7 +489,7 @@ function codexWorktreeGuardArgs(options, platform = process.platform) {
489
489
  // Auto-review escalation, while leaving every non-GitHub destination
490
490
  // blocked. The profile still constrains filesystem access independently.
491
491
  "-c",
492
- `permissions.${CODEX_GUARD_PROFILE}.network.enabled=true`,
492
+ `permissions.${CODEX_GUARD_PROFILE}.network.enabled=${!options.readOnly}`,
493
493
  "-c",
494
494
  `permissions.${CODEX_GUARD_PROFILE}.network.domains=${tomlInlineTable({
495
495
  ...CODEX_GITHUB_NETWORK_DOMAINS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.142",
3
+ "version": "0.1.143",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",