@sagentlab/navarch-runtime 0.1.19 → 0.1.21
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 +14 -9
- package/dist/adapters/codex.cjs +7 -4
- package/dist/codex-pricing.cjs +94 -0
- package/dist/git-worktree.cjs +59 -4
- package/dist/session.cjs +3 -3
- package/dist/worktree-guard.cjs +12 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -290,9 +290,16 @@ unchanged across the deployment.
|
|
|
290
290
|
## Worktree boundary guard (host mode)
|
|
291
291
|
|
|
292
292
|
A machine typically runs several sessions concurrently (`NAVARCH_MAX_SESSIONS`),
|
|
293
|
-
each in its own git worktree. The runtime enforces the same boundary for
|
|
293
|
+
each in its own git worktree. The runtime enforces the same boundary for all
|
|
294
294
|
supported coding agents, using each CLI's native enforcement point:
|
|
295
295
|
|
|
296
|
+
Every adapter receives an empty, session-owned `GH_CONFIG_DIR` and, when the
|
|
297
|
+
project supplies GitHub credentials, the lease-scoped `GITHUB_TOKEN`. This
|
|
298
|
+
keeps `gh` independent of the operator's selected account and prevents tasks
|
|
299
|
+
from reading unrelated GitHub credentials. Docker already mounts the session
|
|
300
|
+
metadata directory; Gemini's nested host sandbox receives the `gh` directory
|
|
301
|
+
as an explicit read-only mount.
|
|
302
|
+
|
|
296
303
|
- **Claude Code:** the runtime uses `--permission-mode auto`, which sends
|
|
297
304
|
approval decisions through Claude Code's background safety classifier. It
|
|
298
305
|
does not blanket-preapprove the lease-scoped Navarch MCP tools. A generated
|
|
@@ -307,19 +314,17 @@ supported coding agents, using each CLI's native enforcement point:
|
|
|
307
314
|
Codex's OS sandbox grants read/write access only to the allowed roots and
|
|
308
315
|
denies the surrounding multi-session workspace. Its network policy allows
|
|
309
316
|
GitHub and GitHub-hosted content so authenticated `gh` and git operations
|
|
310
|
-
required by the task stay inside the sandbox.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
inaccessible. Other destinations remain blocked and eligible escalations
|
|
314
|
-
are decided by the automatic reviewer rather than waiting for human input.
|
|
317
|
+
required by the task stay inside the sandbox. Other destinations remain
|
|
318
|
+
blocked and eligible escalations are decided by the automatic reviewer
|
|
319
|
+
rather than waiting for human input.
|
|
315
320
|
`--ignore-user-config` and an untrusted project-config override prevent a
|
|
316
321
|
user or checked-in legacy `sandbox_mode` from silently disabling the
|
|
317
322
|
generated profile; Codex authentication still comes from `CODEX_HOME`, and
|
|
318
323
|
repository instructions such as `AGENTS.md` still load.
|
|
319
324
|
- **Gemini:** the runtime uses Gemini CLI's native sandbox with only the
|
|
320
|
-
current worktree, shared gitdir, and approved extra
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
current worktree, shared gitdir, session `gh` config, and approved extra
|
|
326
|
+
roots mounted. The `gh` config and lease MCP settings are read-only; header
|
|
327
|
+
values remain environment-variable references rather than literals.
|
|
323
328
|
Docker-mode sessions disable Gemini's implicit YOLO sandbox to avoid nesting
|
|
324
329
|
it inside Navarch's already isolated session container.
|
|
325
330
|
|
package/dist/adapters/codex.cjs
CHANGED
|
@@ -6,6 +6,7 @@ exports.hasExplicitPermissionPolicy = hasExplicitPermissionPolicy;
|
|
|
6
6
|
const node_child_process_1 = require("node:child_process");
|
|
7
7
|
const node_fs_1 = require("node:fs");
|
|
8
8
|
const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
9
|
+
const codex_pricing_cjs_1 = require("../codex-pricing.cjs");
|
|
9
10
|
/**
|
|
10
11
|
* Headless OpenAI Codex CLI adapter — the Codex sibling of claude.cts's
|
|
11
12
|
* `runClaudeCodeAdapter`, implementing the same AgentAdapter interface
|
|
@@ -63,7 +64,7 @@ async function runCodexAdapter(options) {
|
|
|
63
64
|
const raw = runOptions.dockerExec
|
|
64
65
|
? await runViaDocker(runOptions, args)
|
|
65
66
|
: await runOnHost(runOptions, args);
|
|
66
|
-
return attachUsage(raw);
|
|
67
|
+
return attachUsage(raw, options.model);
|
|
67
68
|
}
|
|
68
69
|
/** Operator policy wins over Navarch's generated host-mode profile. */
|
|
69
70
|
function hasExplicitPermissionPolicy(args) {
|
|
@@ -139,13 +140,15 @@ function safeEnvSegment(value) {
|
|
|
139
140
|
/**
|
|
140
141
|
* Parses stdout for `codex exec --json` usage/final-message events and folds
|
|
141
142
|
* them onto the raw result. Codex currently reports token counts but no USD
|
|
142
|
-
* cost, so
|
|
143
|
+
* cost, so known models receive a standard-tier API-equivalent estimate. An
|
|
144
|
+
* explicit cost from a compatible future event always takes precedence.
|
|
143
145
|
*/
|
|
144
|
-
function attachUsage(result) {
|
|
146
|
+
function attachUsage(result, model) {
|
|
145
147
|
const events = (0, exit_conditions_cjs_1.parseCodexJsonEvents)(result.stdout);
|
|
146
148
|
if (events.length === 0)
|
|
147
149
|
return result;
|
|
148
150
|
const usage = (0, exit_conditions_cjs_1.extractUsageFromCodexEvents)(events);
|
|
151
|
+
const costUsd = usage.costUsd ?? (0, codex_pricing_cjs_1.estimateCodexCostUsd)(events, model);
|
|
149
152
|
const reportText = (0, exit_conditions_cjs_1.extractFinalMessageFromCodexEvents)(events) ?? undefined;
|
|
150
153
|
return {
|
|
151
154
|
...result,
|
|
@@ -153,7 +156,7 @@ function attachUsage(result) {
|
|
|
153
156
|
// measured zero — leave the fields unset so aggregation skips them.
|
|
154
157
|
...(usage.tokensIn !== undefined ? { tokensIn: usage.tokensIn } : {}),
|
|
155
158
|
...(usage.tokensOut !== undefined ? { tokensOut: usage.tokensOut } : {}),
|
|
156
|
-
...(
|
|
159
|
+
...(costUsd !== undefined ? { costUsd } : {}),
|
|
157
160
|
...(reportText !== undefined ? { reportText } : {}),
|
|
158
161
|
};
|
|
159
162
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.estimateCodexCostUsd = estimateCodexCostUsd;
|
|
4
|
+
/**
|
|
5
|
+
* Standard-tier USD prices per 1M tokens. Codex's JSONL stream reports usage
|
|
6
|
+
* but not dollars, so the runtime needs a local price snapshot to turn that
|
|
7
|
+
* usage into project spend. Keep this deliberately limited to models Navarch
|
|
8
|
+
* offers rather than silently applying the wrong price to custom/gateway ids.
|
|
9
|
+
*
|
|
10
|
+
* Source (checked 2026-08-05): https://developers.openai.com/api/docs/pricing
|
|
11
|
+
*/
|
|
12
|
+
const CODEX_RATES = {
|
|
13
|
+
"gpt-5.6-sol": {
|
|
14
|
+
input: 5,
|
|
15
|
+
cachedInput: 0.5,
|
|
16
|
+
cacheWrite: 6.25,
|
|
17
|
+
output: 30,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
function nonnegative(value) {
|
|
21
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
22
|
+
}
|
|
23
|
+
function priceUsage(usage, rates) {
|
|
24
|
+
const input = nonnegative(usage.input);
|
|
25
|
+
// Cached reads and cache writes are components of input_tokens, not extra
|
|
26
|
+
// tokens. Clamp malformed/provider-newer detail fields to the reported total
|
|
27
|
+
// so an odd event cannot produce negative uncached input or inflated spend.
|
|
28
|
+
const cachedInput = Math.min(input, nonnegative(usage.cachedInput));
|
|
29
|
+
const cacheWrite = Math.min(input - cachedInput, nonnegative(usage.cacheWrite));
|
|
30
|
+
const uncachedInput = input - cachedInput - cacheWrite;
|
|
31
|
+
const output = nonnegative(usage.output);
|
|
32
|
+
return (uncachedInput * rates.input +
|
|
33
|
+
cachedInput * rates.cachedInput +
|
|
34
|
+
cacheWrite * rates.cacheWrite +
|
|
35
|
+
output * rates.output) / 1_000_000;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Estimate standard-tier API-equivalent USD cost for a Codex JSONL run.
|
|
39
|
+
* Verified turn.completed events are per-turn and must be summed; legacy
|
|
40
|
+
* token_count envelopes are cumulative, so only their final total is priced.
|
|
41
|
+
* Returns undefined for unknown models or when no usage event was observed.
|
|
42
|
+
*/
|
|
43
|
+
function estimateCodexCostUsd(events, model) {
|
|
44
|
+
if (!model)
|
|
45
|
+
return undefined;
|
|
46
|
+
const rates = CODEX_RATES[model];
|
|
47
|
+
if (!rates)
|
|
48
|
+
return undefined;
|
|
49
|
+
let sawTurnUsage = false;
|
|
50
|
+
let turnCost = 0;
|
|
51
|
+
let legacyUsage;
|
|
52
|
+
for (const event of events) {
|
|
53
|
+
if (event.type === "turn.completed" && event.usage) {
|
|
54
|
+
sawTurnUsage = true;
|
|
55
|
+
turnCost += priceUsage({
|
|
56
|
+
input: event.usage.input_tokens,
|
|
57
|
+
cachedInput: event.usage.cached_input_tokens,
|
|
58
|
+
cacheWrite: event.usage.cache_write_input_tokens,
|
|
59
|
+
output: event.usage.output_tokens,
|
|
60
|
+
}, rates);
|
|
61
|
+
}
|
|
62
|
+
if (event.msg?.type === "token_count") {
|
|
63
|
+
const usage = event.msg.info?.total_token_usage;
|
|
64
|
+
legacyUsage = usage
|
|
65
|
+
? {
|
|
66
|
+
input: usage.input_tokens,
|
|
67
|
+
cachedInput: usage.cached_input_tokens,
|
|
68
|
+
cacheWrite: usage.cache_write_input_tokens,
|
|
69
|
+
output: usage.output_tokens,
|
|
70
|
+
}
|
|
71
|
+
: {
|
|
72
|
+
// In the oldest flat envelope cached_input_tokens was additional
|
|
73
|
+
// to input_tokens; normalize it to the newer inclusive total.
|
|
74
|
+
input: nonnegative(event.msg.input_tokens) + nonnegative(event.msg.cached_input_tokens),
|
|
75
|
+
cachedInput: event.msg.cached_input_tokens,
|
|
76
|
+
output: event.msg.output_tokens,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (event.type === "event_msg" && event.payload?.type === "token_count") {
|
|
80
|
+
const usage = event.payload.info?.total_token_usage;
|
|
81
|
+
if (usage) {
|
|
82
|
+
legacyUsage = {
|
|
83
|
+
input: usage.input_tokens,
|
|
84
|
+
cachedInput: usage.cached_input_tokens,
|
|
85
|
+
cacheWrite: usage.cache_write_input_tokens,
|
|
86
|
+
output: usage.output_tokens,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (sawTurnUsage)
|
|
92
|
+
return turnCost;
|
|
93
|
+
return legacyUsage ? priceUsage(legacyUsage, rates) : undefined;
|
|
94
|
+
}
|
package/dist/git-worktree.cjs
CHANGED
|
@@ -24,6 +24,8 @@ class GitWorktree {
|
|
|
24
24
|
cloneUrl;
|
|
25
25
|
githubToken;
|
|
26
26
|
taskBranchSuffix;
|
|
27
|
+
legacyTaskBranchSuffix;
|
|
28
|
+
taskOwnershipTrailer;
|
|
27
29
|
constructor(options) {
|
|
28
30
|
const projectKey = safePathSegment(options.projectId);
|
|
29
31
|
const sessionKey = safePathSegment(options.sessionId);
|
|
@@ -34,7 +36,10 @@ class GitWorktree {
|
|
|
34
36
|
// One delivery task owns one remote branch across every retry. A
|
|
35
37
|
// session-scoped branch lets a failed attempt push useful work, then makes
|
|
36
38
|
// the retry start from main and open a second PR for the same task.
|
|
37
|
-
|
|
39
|
+
const normalizedTaskId = taskKey.toLowerCase();
|
|
40
|
+
this.taskBranchSuffix = `-${normalizedTaskId.slice(0, 8)}`;
|
|
41
|
+
this.legacyTaskBranchSuffix = `-${normalizedTaskId}`;
|
|
42
|
+
this.taskOwnershipTrailer = `Navarch-Task-ID: ${normalizedTaskId}`;
|
|
38
43
|
this.branch = `navarch/${branchSlug(options)}${this.taskBranchSuffix}`;
|
|
39
44
|
this.runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
|
|
40
45
|
this.cloneUrl = options.cloneUrl;
|
|
@@ -84,11 +89,13 @@ class GitWorktree {
|
|
|
84
89
|
// A failed session may have pushed before it could report completion. The
|
|
85
90
|
// next session must resume that task-owned branch, not fork again from the
|
|
86
91
|
// default branch and create a competing delivery PR.
|
|
87
|
-
const
|
|
92
|
+
const taskRef = await this.resolveTaskBranchRef();
|
|
93
|
+
const startRef = taskRef ?? (await this.resolveStartRef());
|
|
88
94
|
if (!startRef) {
|
|
89
95
|
// Empty remote (no commits yet): there is nothing to base the session on,
|
|
90
96
|
// so bootstrap an orphan branch the session can push as the first commit.
|
|
91
97
|
await this.addOrphanWorktree();
|
|
98
|
+
await this.createTaskOwnershipCommit();
|
|
92
99
|
return;
|
|
93
100
|
}
|
|
94
101
|
await this.runGit([
|
|
@@ -101,16 +108,32 @@ class GitWorktree {
|
|
|
101
108
|
this.worktreePath,
|
|
102
109
|
startRef,
|
|
103
110
|
], false);
|
|
111
|
+
if (!taskRef)
|
|
112
|
+
await this.createTaskOwnershipCommit();
|
|
104
113
|
}
|
|
105
114
|
/** Returns the fetched task-owned remote branch when an earlier attempt pushed it. */
|
|
106
115
|
async resolveTaskBranchRef() {
|
|
107
116
|
const refs = await this.runGit(["--git-dir", this.repositoryPath, "for-each-ref", "--format=%(refname)", "refs/remotes/origin/navarch"], false);
|
|
108
|
-
const
|
|
117
|
+
const suffixMatches = refs.stdout
|
|
109
118
|
.split("\n")
|
|
110
119
|
.map((line) => line.trim())
|
|
111
120
|
.filter((ref) => ref.endsWith(this.taskBranchSuffix));
|
|
112
|
-
|
|
121
|
+
const legacyMatches = refs.stdout
|
|
122
|
+
.split("\n")
|
|
123
|
+
.map((line) => line.trim())
|
|
124
|
+
.filter((ref) => ref.endsWith(this.legacyTaskBranchSuffix));
|
|
125
|
+
const shortMatches = suffixMatches.filter((ref) => !legacyMatches.includes(ref));
|
|
126
|
+
const ownedShortMatches = (await Promise.all(shortMatches.map(async (ref) => ({ ref, owned: await this.hasTaskOwnershipMarker(ref) }))))
|
|
127
|
+
.filter(({ owned }) => owned)
|
|
128
|
+
.map(({ ref }) => ref);
|
|
129
|
+
const matches = [...ownedShortMatches, ...legacyMatches];
|
|
130
|
+
if (matches.length === 0) {
|
|
131
|
+
if (shortMatches.length > 0) {
|
|
132
|
+
throw new Error(`Remote branch ${shortMatches[0]} shares task suffix ${this.taskBranchSuffix} but does not ` +
|
|
133
|
+
`contain ownership marker ${this.taskOwnershipTrailer}; refusing to resume or fork.`);
|
|
134
|
+
}
|
|
113
135
|
return null;
|
|
136
|
+
}
|
|
114
137
|
if (matches.length > 1) {
|
|
115
138
|
throw new Error(`Multiple remote branches claim task suffix ${this.taskBranchSuffix}; refusing to fork another delivery branch.`);
|
|
116
139
|
}
|
|
@@ -118,6 +141,38 @@ class GitWorktree {
|
|
|
118
141
|
this.branch = remoteRef.replace(/^refs\/remotes\/origin\//, "");
|
|
119
142
|
return remoteRef;
|
|
120
143
|
}
|
|
144
|
+
/** Short branch names are ambiguous without a full task-ID marker in their history. */
|
|
145
|
+
async hasTaskOwnershipMarker(ref) {
|
|
146
|
+
const log = await this.runGit([
|
|
147
|
+
"--git-dir",
|
|
148
|
+
this.repositoryPath,
|
|
149
|
+
"log",
|
|
150
|
+
"--format=%B",
|
|
151
|
+
"--fixed-strings",
|
|
152
|
+
`--grep=${this.taskOwnershipTrailer}`,
|
|
153
|
+
ref,
|
|
154
|
+
], false);
|
|
155
|
+
return log.stdout
|
|
156
|
+
.split("\n")
|
|
157
|
+
.some((line) => line.trim() === this.taskOwnershipTrailer);
|
|
158
|
+
}
|
|
159
|
+
/** Records full task ownership without lengthening the human-facing branch name. */
|
|
160
|
+
async createTaskOwnershipCommit() {
|
|
161
|
+
await this.runGit([
|
|
162
|
+
"-C",
|
|
163
|
+
this.worktreePath,
|
|
164
|
+
"-c",
|
|
165
|
+
"user.name=Navarch",
|
|
166
|
+
"-c",
|
|
167
|
+
"user.email=runtime@navarch.local",
|
|
168
|
+
"commit",
|
|
169
|
+
"--allow-empty",
|
|
170
|
+
"-m",
|
|
171
|
+
"chore(navarch): claim task branch",
|
|
172
|
+
"-m",
|
|
173
|
+
this.taskOwnershipTrailer,
|
|
174
|
+
], false);
|
|
175
|
+
}
|
|
121
176
|
/**
|
|
122
177
|
* Resolves the remote-tracking ref new session branches start from, or null
|
|
123
178
|
* when the remote has no branches at all (a freshly provisioned empty repo).
|
package/dist/session.cjs
CHANGED
|
@@ -96,6 +96,9 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
const sessionEnv = toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail);
|
|
99
|
+
const ghConfigDir = (0, worktree_guard_cjs_1.sessionGhConfigDir)(workDir);
|
|
100
|
+
await node_fs_1.promises.mkdir(ghConfigDir, { recursive: true, mode: 0o700 });
|
|
101
|
+
sessionEnv.GH_CONFIG_DIR = ghConfigDir;
|
|
99
102
|
const cloneUrl = bundle.repository?.clone_url ??
|
|
100
103
|
(task.repo ? `https://github.com/${task.repo.replace(/\.git$/, "")}.git` : null);
|
|
101
104
|
if (!cloneUrl) {
|
|
@@ -242,9 +245,6 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
247
|
else if (runtime === "codex") {
|
|
245
|
-
const ghConfigDir = (0, worktree_guard_cjs_1.codexGhConfigDir)(workDir);
|
|
246
|
-
await node_fs_1.promises.mkdir(ghConfigDir, { recursive: true, mode: 0o700 });
|
|
247
|
-
sessionEnv.GH_CONFIG_DIR = ghConfigDir;
|
|
248
248
|
codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
|
|
249
249
|
workDir,
|
|
250
250
|
worktreePath: gitWorktree.worktreePath,
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -7,14 +7,14 @@ exports.guardHookScriptPath = guardHookScriptPath;
|
|
|
7
7
|
exports.readClaudeApiKeyHelper = readClaudeApiKeyHelper;
|
|
8
8
|
exports.prepareWorktreeGuard = prepareWorktreeGuard;
|
|
9
9
|
exports.codexToolReadRoots = codexToolReadRoots;
|
|
10
|
-
exports.
|
|
10
|
+
exports.sessionGhConfigDir = sessionGhConfigDir;
|
|
11
11
|
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
12
12
|
exports.geminiSandboxMounts = geminiSandboxMounts;
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const node_fs_1 = require("node:fs");
|
|
15
15
|
const node_os_1 = __importDefault(require("node:os"));
|
|
16
16
|
const CODEX_GUARD_PROFILE = "navarch-worktree";
|
|
17
|
-
const
|
|
17
|
+
const SESSION_GH_CONFIG_DIRNAME = "gh-config";
|
|
18
18
|
const CODEX_GITHUB_NETWORK_DOMAINS = {
|
|
19
19
|
"**.github.com": "allow",
|
|
20
20
|
"**.githubusercontent.com": "allow",
|
|
@@ -129,15 +129,15 @@ function codexToolReadRoots(env = process.env) {
|
|
|
129
129
|
];
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
|
-
* Isolated gh configuration root for one
|
|
132
|
+
* Isolated gh configuration root for one coding-agent session.
|
|
133
133
|
*
|
|
134
134
|
* `gh` reads its config file before it considers `GITHUB_TOKEN`. Pointing it
|
|
135
135
|
* at this empty, session-owned directory lets the CLI use the lease token
|
|
136
136
|
* without granting the agent read access to the operator's ~/.config/gh,
|
|
137
137
|
* which may contain unrelated account credentials.
|
|
138
138
|
*/
|
|
139
|
-
function
|
|
140
|
-
return node_path_1.default.join(workDir,
|
|
139
|
+
function sessionGhConfigDir(workDir) {
|
|
140
|
+
return node_path_1.default.join(workDir, SESSION_GH_CONFIG_DIRNAME);
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
143
|
* Builds one-off Codex permission-profile arguments for a host session.
|
|
@@ -161,7 +161,7 @@ function codexWorktreeGuardArgs(options) {
|
|
|
161
161
|
[node_path_1.default.resolve(options.workspaceRoot)]: "deny",
|
|
162
162
|
[node_path_1.default.resolve(options.worktreePath)]: "write",
|
|
163
163
|
[node_path_1.default.resolve(options.repositoryPath)]: "write",
|
|
164
|
-
[
|
|
164
|
+
[sessionGhConfigDir(options.workDir)]: "read",
|
|
165
165
|
};
|
|
166
166
|
for (const root of codexToolReadRoots()) {
|
|
167
167
|
const resolved = node_path_1.default.resolve(root);
|
|
@@ -204,8 +204,9 @@ function codexWorktreeGuardArgs(options) {
|
|
|
204
204
|
}
|
|
205
205
|
/**
|
|
206
206
|
* Mounts only the current session roots into Gemini CLI's native sandbox.
|
|
207
|
-
* Gemini mounts cwd itself; explicit same-path mounts keep the shared gitdir
|
|
208
|
-
* and approved external roots reachable without exposing
|
|
207
|
+
* Gemini mounts cwd itself; explicit same-path mounts keep the shared gitdir,
|
|
208
|
+
* isolated gh config, and approved external roots reachable without exposing
|
|
209
|
+
* sibling sessions.
|
|
209
210
|
*/
|
|
210
211
|
function geminiSandboxMounts(options) {
|
|
211
212
|
const workspaceRoot = node_path_1.default.resolve(options.workspaceRoot);
|
|
@@ -215,7 +216,9 @@ function geminiSandboxMounts(options) {
|
|
|
215
216
|
if (!isPathInside(resolved, workspaceRoot))
|
|
216
217
|
roots.push(resolved);
|
|
217
218
|
}
|
|
218
|
-
|
|
219
|
+
const writableMounts = [...new Set(roots.map((root) => node_path_1.default.resolve(root)))].map((root) => `${root}:${root}:rw`);
|
|
220
|
+
const ghConfigDir = sessionGhConfigDir(options.workDir);
|
|
221
|
+
return [...writableMounts, `${ghConfigDir}:${ghConfigDir}:ro`];
|
|
219
222
|
}
|
|
220
223
|
function isPathInside(candidate, root) {
|
|
221
224
|
const relative = node_path_1.default.relative(root, candidate);
|
package/package.json
CHANGED