@sagentlab/navarch-runtime 0.1.80 → 0.1.82
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 +13 -1
- package/dist/adapters/pi.cjs +30 -7
- package/dist/session.cjs +5 -0
- package/dist/worktree-guard.cjs +27 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -543,6 +543,15 @@ The Claude hook is a strong guardrail rather than a hard security boundary
|
|
|
543
543
|
because shell paths are screened lexically. Codex's permission profile is
|
|
544
544
|
enforced by its OS sandbox. For container-grade whole-process isolation use
|
|
545
545
|
`NAVARCH_SANDBOX_MODE=docker`; neither host guard is installed in Docker mode.
|
|
546
|
+
On Linux, Codex uses an explicit allowlist without a denied workspace-parent
|
|
547
|
+
mount, avoiding bubblewrap startup failures with linked Git worktrees. Its
|
|
548
|
+
`TMPDIR`, `TMP`, and `TEMP` point to a private `tmp` directory inside the session;
|
|
549
|
+
the profile does not grant all of `/tmp`. Extra/tool roots that contain the
|
|
550
|
+
workspace are omitted on Linux so they cannot expose sibling sessions or
|
|
551
|
+
session metadata. This also applies when the workspace itself lives under `/tmp`.
|
|
552
|
+
To verify the linked-worktree boundary on a Linux host with Codex installed,
|
|
553
|
+
run `NAVARCH_TEST_REAL_CODEX=1 npm test -- tests/codex-linux-sandbox.test.cts`
|
|
554
|
+
from `runtime/`. The smoke test uses local shell commands and makes no model calls.
|
|
546
555
|
An operator-supplied `--settings` in `NAVARCH_CLAUDE_EXTRA_ARGS`, or an
|
|
547
556
|
explicit Codex permission/sandbox option in `NAVARCH_CODEX_EXTRA_ARGS`, takes
|
|
548
557
|
precedence over the generated policy.
|
|
@@ -959,7 +968,10 @@ different endpoint (for example, Ollama's `http://127.0.0.1:11434/v1`).
|
|
|
959
968
|
`NAVARCH_PI_BIN` overrides the executable; `NAVARCH_PI_EXTRA_ARGS` adds CLI
|
|
960
969
|
arguments. `--model` at connect pins this machine's model as for other agents.
|
|
961
970
|
|
|
962
|
-
Bare model IDs
|
|
971
|
+
Bare model IDs first resolve a unique matching model in the operator's catalog;
|
|
972
|
+
ambiguous matches require an explicit `provider/model`. Without a match, they
|
|
973
|
+
use the generated `local` provider. Setting `NAVARCH_PI_LOCAL_BASE_URL` explicitly
|
|
974
|
+
selects that generated provider. Exact `provider/model`
|
|
963
975
|
references use the machine's existing Pi catalog and credentials from
|
|
964
976
|
`~/.pi/agent/models.json` and `auth.json` (or `PI_CODING_AGENT_DIR`). Run
|
|
965
977
|
`pi --list-models` to inspect that catalog. The UI puts the local default first
|
package/dist/adapters/pi.cjs
CHANGED
|
@@ -165,10 +165,20 @@ function attachPiOutput(result) {
|
|
|
165
165
|
let costUsd;
|
|
166
166
|
let sawUsage = false;
|
|
167
167
|
let reportText = "";
|
|
168
|
+
let terminalError;
|
|
168
169
|
for (const event of events) {
|
|
169
170
|
const message = event.message;
|
|
171
|
+
if (event.type === "error") {
|
|
172
|
+
terminalError = typeof event.error === "string" ? event.error :
|
|
173
|
+
typeof event.errorMessage === "string" ? event.errorMessage : "Pi provider error";
|
|
174
|
+
}
|
|
170
175
|
if (event.type !== "message_end" || !message || message.role !== "assistant")
|
|
171
176
|
continue;
|
|
177
|
+
// Pi can settle an exhausted provider retry with process exit code zero.
|
|
178
|
+
// A later successful assistant message supersedes a recovered error.
|
|
179
|
+
terminalError = message.stopReason === "error" || message.errorMessage
|
|
180
|
+
? typeof message.errorMessage === "string" ? message.errorMessage : "Pi provider error"
|
|
181
|
+
: undefined;
|
|
172
182
|
const text = extractText(message.content);
|
|
173
183
|
if (text.trim())
|
|
174
184
|
reportText = text.trim();
|
|
@@ -193,6 +203,9 @@ function attachPiOutput(result) {
|
|
|
193
203
|
}
|
|
194
204
|
return {
|
|
195
205
|
...result,
|
|
206
|
+
...(terminalError && result.exitCode === 0 && !result.timedOut && !result.killedByLeaseLoss
|
|
207
|
+
? { exitCode: 1, stderr: `${result.stderr}${result.stderr ? "\n" : ""}Pi provider failed: ${terminalError}` }
|
|
208
|
+
: {}),
|
|
196
209
|
...(sawUsage
|
|
197
210
|
? {
|
|
198
211
|
// AdapterResult semantics: tokensIn folds every input category
|
|
@@ -269,13 +282,23 @@ async function runOnHost(options, args, local = false) {
|
|
|
269
282
|
const configPath = node_path_1.default.join(agentDir, "models.json");
|
|
270
283
|
const config = JSON.parse(await node_fs_1.promises.readFile(configPath, "utf8").catch(() => "{}"));
|
|
271
284
|
config.providers ??= {};
|
|
272
|
-
|
|
273
|
-
config.providers.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
const endpointOverride = options.env.NAVARCH_PI_LOCAL_BASE_URL ?? process.env.NAVARCH_PI_LOCAL_BASE_URL;
|
|
286
|
+
const matches = endpointOverride ? [] : Object.entries(config.providers).filter(([, provider]) => provider.models?.some((entry) => entry.id === model)).map(([name]) => name);
|
|
287
|
+
if (matches.length > 1)
|
|
288
|
+
throw new Error(`Pi model ${model} matches multiple providers; select an explicit provider/model.`);
|
|
289
|
+
if (matches.length === 1) {
|
|
290
|
+
args = [...args];
|
|
291
|
+
args[args.lastIndexOf("--model") + 1] = `${matches[0]}/${model}`;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Without a catalog match, retain the default local endpoint behavior.
|
|
295
|
+
config.providers.local = {
|
|
296
|
+
baseUrl: endpointOverride ?? "http://127.0.0.1:8080/v1",
|
|
297
|
+
api: "openai-completions", apiKey: "local",
|
|
298
|
+
models: [{ id: model, reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }],
|
|
299
|
+
};
|
|
300
|
+
await node_fs_1.promises.writeFile(configPath, JSON.stringify(config), { mode: 0o600 });
|
|
301
|
+
}
|
|
279
302
|
}
|
|
280
303
|
if (options.mcpConfigPath) {
|
|
281
304
|
await node_fs_1.promises.copyFile(options.mcpConfigPath, node_path_1.default.join(agentDir, "mcp.json"));
|
package/dist/session.cjs
CHANGED
|
@@ -378,6 +378,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
else if (runtime === "codex") {
|
|
381
|
+
if (process.platform === "linux") {
|
|
382
|
+
const tmpDir = (0, worktree_guard_cjs_1.sessionCodexTmpDir)(workDir);
|
|
383
|
+
await node_fs_1.promises.mkdir(tmpDir, { recursive: true, mode: 0o700 });
|
|
384
|
+
Object.assign(sessionEnv, { TMPDIR: tmpDir, TMP: tmpDir, TEMP: tmpDir });
|
|
385
|
+
}
|
|
381
386
|
codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
|
|
382
387
|
workDir,
|
|
383
388
|
worktreePath: gitWorktree.worktreePath,
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -9,6 +9,7 @@ exports.prepareWorktreeGuard = prepareWorktreeGuard;
|
|
|
9
9
|
exports.codexToolReadRoots = codexToolReadRoots;
|
|
10
10
|
exports.macosDeveloperWriteRoots = macosDeveloperWriteRoots;
|
|
11
11
|
exports.sessionGhConfigDir = sessionGhConfigDir;
|
|
12
|
+
exports.sessionCodexTmpDir = sessionCodexTmpDir;
|
|
12
13
|
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
13
14
|
exports.geminiSandboxMounts = geminiSandboxMounts;
|
|
14
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -172,6 +173,9 @@ function macosDeveloperWriteRoots(env = process.env, platform = process.platform
|
|
|
172
173
|
function sessionGhConfigDir(workDir) {
|
|
173
174
|
return node_path_1.default.join(workDir, SESSION_GH_CONFIG_DIRNAME);
|
|
174
175
|
}
|
|
176
|
+
function sessionCodexTmpDir(workDir) {
|
|
177
|
+
return node_path_1.default.resolve(workDir, "tmp");
|
|
178
|
+
}
|
|
175
179
|
/**
|
|
176
180
|
* Builds one-off Codex permission-profile arguments for a host session.
|
|
177
181
|
*
|
|
@@ -180,33 +184,46 @@ function sessionGhConfigDir(workDir) {
|
|
|
180
184
|
* this boundary. Authentication still comes from CODEX_HOME. The profile
|
|
181
185
|
* grants only the runtime paths common tools need, the session worktree, the
|
|
182
186
|
* shared Git directory, temp space, and operator-approved extra roots. The
|
|
183
|
-
* surrounding Navarch workspace is denied
|
|
184
|
-
*
|
|
187
|
+
* surrounding Navarch workspace is denied (implicitly on Linux), with only
|
|
188
|
+
* this session's paths granted access. The worktree
|
|
185
189
|
* is marked untrusted for configuration purposes so a checked-in legacy
|
|
186
190
|
* sandbox_mode cannot disable the generated permission profile; repository
|
|
187
191
|
* instructions such as AGENTS.md still load normally.
|
|
188
192
|
*/
|
|
189
|
-
function codexWorktreeGuardArgs(options) {
|
|
193
|
+
function codexWorktreeGuardArgs(options, platform = process.platform) {
|
|
194
|
+
const workspaceRoot = node_path_1.default.resolve(options.workspaceRoot);
|
|
195
|
+
const linux = platform === "linux";
|
|
196
|
+
// On Linux, a denied ancestor is an empty read-only bwrap mount. Codex
|
|
197
|
+
// can then try to protect a linked worktree's gitdir before reopening the
|
|
198
|
+
// bare repository, failing to mkdir repository.git/worktrees/repo beneath
|
|
199
|
+
// that mount. Use restricted-read's implicit deny instead. Broad temp or
|
|
200
|
+
// ancestor grants would expose the workspace without the explicit mask,
|
|
201
|
+
// so Linux gets only a session temp root and non-overlapping extra roots.
|
|
190
202
|
const filesystem = {
|
|
191
203
|
":minimal": "read",
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
...(linux
|
|
205
|
+
? { [sessionCodexTmpDir(options.workDir)]: "write" }
|
|
206
|
+
: {
|
|
207
|
+
":tmpdir": "write",
|
|
208
|
+
":slash_tmp": "write",
|
|
209
|
+
[workspaceRoot]: "deny",
|
|
210
|
+
}),
|
|
195
211
|
[node_path_1.default.resolve(options.worktreePath)]: "write",
|
|
196
212
|
[node_path_1.default.resolve(options.repositoryPath)]: "write",
|
|
197
213
|
[sessionGhConfigDir(options.workDir)]: "read",
|
|
198
214
|
};
|
|
199
|
-
|
|
215
|
+
const overlapsWorkspace = (root) => isPathInside(root, workspaceRoot) || (linux && isPathInside(workspaceRoot, root));
|
|
216
|
+
for (const root of codexToolReadRoots(process.env, platform)) {
|
|
200
217
|
const resolved = node_path_1.default.resolve(root);
|
|
201
|
-
if (
|
|
218
|
+
if (overlapsWorkspace(resolved))
|
|
202
219
|
continue;
|
|
203
220
|
filesystem[resolved] = "read";
|
|
204
221
|
}
|
|
205
|
-
for (const root of [...macosDeveloperWriteRoots(), ...(options.extraRoots ?? [])]) {
|
|
222
|
+
for (const root of [...macosDeveloperWriteRoots(process.env, platform), ...(options.extraRoots ?? [])]) {
|
|
206
223
|
const resolved = node_path_1.default.resolve(root);
|
|
207
224
|
// Match the Claude hook's denied-root precedence: an extra root cannot
|
|
208
225
|
// reopen sibling sessions or metadata inside the Navarch workspace.
|
|
209
|
-
if (
|
|
226
|
+
if (overlapsWorkspace(resolved))
|
|
210
227
|
continue;
|
|
211
228
|
filesystem[resolved] = "write";
|
|
212
229
|
}
|
package/package.json
CHANGED