@nathapp/nax-agent 0.87.1 → 0.88.0-canary.2
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/CHANGELOG.md +14 -0
- package/dist/coding-tools/coding-tool-support.d.ts +16 -0
- package/dist/coding-tools/coding-tool-support.js +12 -0
- package/dist/command-safety/rule-scorer.d.ts +1 -1
- package/dist/command-safety/rule-scorer.js +11 -113
- package/dist/config/schemas-sandbox.d.ts +1 -0
- package/dist/config/schemas-sandbox.js +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/process-kill.d.ts +6 -2
- package/dist/internal/process-kill.js +6 -2
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/mcp/connect.js +15 -1
- package/dist/mcp/types.d.ts +6 -0
- package/dist/native/auth.d.ts +2 -2
- package/dist/native/auth.js +4 -4
- package/dist/native/errors.js +7 -0
- package/dist/native/failed-round-trip-spend.d.ts +16 -0
- package/dist/native/failed-round-trip-spend.js +33 -0
- package/dist/native/session/partial-spend.d.ts +20 -0
- package/dist/native/session/partial-spend.js +14 -0
- package/dist/native/session/session.js +11 -4
- package/dist/native/session/turn-complete-step.js +23 -3
- package/dist/native/session-adapter.js +3 -2
- package/dist/permissions/bash-lex.js +36 -4
- package/dist/sandbox/claude-settings.d.ts +46 -0
- package/dist/sandbox/claude-settings.js +76 -0
- package/dist/sandbox/index.d.ts +2 -1
- package/dist/sandbox/index.js +1 -0
- package/dist/sandbox/launcher.js +4 -0
- package/dist/sandbox/messages.d.ts +1 -1
- package/dist/sandbox/messages.js +15 -3
- package/dist/sandbox/policy-builder.js +27 -2
- package/dist/sandbox/session-sandbox.d.ts +3 -0
- package/dist/sandbox/session-sandbox.js +9 -0
- package/dist/sandbox/srt-backend.d.ts +10 -1
- package/dist/sandbox/srt-backend.js +14 -4
- package/dist/sandbox/types.d.ts +23 -0
- package/dist/session/agent-session-options.js +1 -1
- package/dist/session/agent-session-types.d.ts +1 -1
- package/dist/session/native-backend-options.js +5 -3
- package/dist/session/session-backend.d.ts +9 -0
- package/dist/session/session-tool-support.d.ts +1 -1
- package/dist/session/session-tool-support.js +4 -2
- package/dist/session/session-types.d.ts +10 -0
- package/dist/tools/bash.d.ts +7 -1
- package/dist/tools/bash.js +18 -3
- package/dist/tools/denial-redirect.d.ts +22 -7
- package/dist/tools/denial-redirect.js +29 -13
- package/dist/tools/git-flags/locate.d.ts +47 -0
- package/dist/tools/git-flags/locate.js +361 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/nax-dirs.d.ts +22 -0
- package/dist/tools/nax-dirs.js +89 -0
- package/dist/tools/nax-evasion.d.ts +19 -0
- package/dist/tools/nax-evasion.js +88 -0
- package/dist/tools/nax-revert.d.ts +34 -0
- package/dist/tools/nax-revert.js +233 -0
- package/dist/tools/nax-stash-restore.d.ts +7 -0
- package/dist/tools/nax-stash-restore.js +92 -0
- package/dist/tools/nax-state-guard.d.ts +18 -0
- package/dist/tools/nax-state-guard.js +478 -0
- package/dist/tools/policy-bash-raw.d.ts +3 -0
- package/dist/tools/policy-bash-raw.js +40 -12
- package/dist/tools/policy-bash.d.ts +3 -0
- package/dist/tools/policy-bash.js +38 -9
- package/dist/tools/policy-command-branch.d.ts +3 -0
- package/dist/tools/policy-command-branch.js +3 -1
- package/dist/tools/policy.js +13 -0
- package/dist/tools/provider-advertise.d.ts +13 -0
- package/dist/tools/provider-advertise.js +41 -1
- package/dist/tools/provider-instructions.d.ts +33 -0
- package/dist/tools/provider-instructions.js +63 -0
- package/dist/tools/provider-types.d.ts +12 -0
- package/dist/tools/run-command.d.ts +15 -0
- package/dist/tools/run-command.js +16 -1
- package/dist/tools/tool-audit.d.ts +1 -1
- package/package.json +3 -3
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { getSafeLogger } from "#src/infra/index";
|
|
22
22
|
import { applyHistoryPatch } from "./loop-events/cache-boundary.js";
|
|
23
|
+
import { takePartialSpend } from "./partial-spend.js";
|
|
24
|
+
import { usageBeat } from "./turn-accumulator.js";
|
|
23
25
|
import { runOverflowCompaction } from "./turn-compaction-step.js";
|
|
24
26
|
import { realSleep, retryTransportFault } from "./turn-retry.js";
|
|
25
27
|
/**
|
|
@@ -89,8 +91,26 @@ export async function completeWithRecovery(args) {
|
|
|
89
91
|
honoured = wire.honoured;
|
|
90
92
|
return deps.complete(wire.messages, tools, options, turnEvents.deltaSink(roundTrip));
|
|
91
93
|
};
|
|
94
|
+
// A failed round trip was still billed for what it streamed. Folding it in
|
|
95
|
+
// here, per attempt, is what makes a retry count both attempts and an
|
|
96
|
+
// aborted or timed-out one count what it cost; `takePartialSpend` consumes
|
|
97
|
+
// the entry, and the retry loop rethrows the error it was given without
|
|
98
|
+
// re-entering this wrapper, so nothing counts twice.
|
|
99
|
+
const countedRequest = async (msgs) => {
|
|
100
|
+
try {
|
|
101
|
+
return await request(msgs);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const spend = takePartialSpend(err);
|
|
105
|
+
if (spend !== undefined) {
|
|
106
|
+
usage.add(spend.usage, spend.costUsd, spend.rates);
|
|
107
|
+
deps.onActivity?.(usageBeat(spend.usage, spend.costUsd));
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
92
112
|
try {
|
|
93
|
-
res = await
|
|
113
|
+
res = await countedRequest(messages);
|
|
94
114
|
}
|
|
95
115
|
catch (err) {
|
|
96
116
|
// Written as one guarded `if` (not a separate `canRetry` boolean) so
|
|
@@ -109,7 +129,7 @@ export async function completeWithRecovery(args) {
|
|
|
109
129
|
if (deps.transportRetry === undefined)
|
|
110
130
|
throw err;
|
|
111
131
|
res = await retryTransportFault(err, {
|
|
112
|
-
attempt: () =>
|
|
132
|
+
attempt: () => countedRequest(messages),
|
|
113
133
|
config: deps.transportRetry,
|
|
114
134
|
deadline: deps.deadline,
|
|
115
135
|
signal,
|
|
@@ -161,7 +181,7 @@ export async function completeWithRecovery(args) {
|
|
|
161
181
|
turnEvents.emit({ type: "compaction", reason: "overflow" });
|
|
162
182
|
// Retried once. A second overflow propagates: compacting further would be
|
|
163
183
|
// guessing, and the failure now carries a correct diagnosis.
|
|
164
|
-
res = await
|
|
184
|
+
res = await countedRequest(messages);
|
|
165
185
|
}
|
|
166
186
|
}
|
|
167
187
|
return { res, messages, compacted, honoured, boundary: compacted };
|
|
@@ -15,6 +15,7 @@ import { _adapterDeps, authFields, isProtocolStreamError } from "./adapter-deps.
|
|
|
15
15
|
import { _clientDeps, getNativeClient } from "./client.js";
|
|
16
16
|
import { createSessionCredentialStore } from "./credentials/session-source.js";
|
|
17
17
|
import { toAdapterFailure } from "./errors.js";
|
|
18
|
+
import { billedOnFailure } from "./failed-round-trip-spend.js";
|
|
18
19
|
import { buildRateCard, NATIVE_AGENT, parseNativeModel, resolveContextWindow, toThinkingLevel } from "./models.js";
|
|
19
20
|
import { createInstructionInteraction } from "./session/instruction-interaction.js";
|
|
20
21
|
import { closeNativeSession, createNativeSessionState, markNativeTurnOutcome, openNativeSession, systemFieldFor, } from "./session/session.js";
|
|
@@ -305,7 +306,7 @@ export class NativeSessionAdapter {
|
|
|
305
306
|
const requestThinking = requestOptions?.thinking === false ? undefined : thinking;
|
|
306
307
|
try {
|
|
307
308
|
instructionInteraction.acknowledge();
|
|
308
|
-
const res = await streamComplete(client, resolved, {
|
|
309
|
+
const res = await billedOnFailure(rates, () => streamComplete(client, resolved, {
|
|
309
310
|
messages,
|
|
310
311
|
...(tools.length > 0 ? { tools } : {}),
|
|
311
312
|
sessionId,
|
|
@@ -324,7 +325,7 @@ export class NativeSessionAdapter {
|
|
|
324
325
|
// to reuse the entry, so a cache write there costs more than it
|
|
325
326
|
// saves.
|
|
326
327
|
cacheRetention: "short",
|
|
327
|
-
}, onDelta);
|
|
328
|
+
}, onDelta));
|
|
328
329
|
const usage = res.usage;
|
|
329
330
|
// Single `priceCall` invocation: `costUsd` and `resolvedRates`
|
|
330
331
|
// come from the same call so they cannot diverge — the
|
|
@@ -128,23 +128,55 @@ function lexDoubleQuote(state, i) {
|
|
|
128
128
|
const end = doubleQuoteEnd(state.command, i + 1);
|
|
129
129
|
if (end === -1)
|
|
130
130
|
return { kind: "refuse", construct: "an unbalanced double quote" };
|
|
131
|
+
// The checks read the text with line continuations joined, as sh does: `"$\<NL>(id)"` runs `id`.
|
|
132
|
+
// Joining over-matches only after an escaped backslash, which errs toward refusing.
|
|
131
133
|
const inner = state.command.slice(i + 1, end);
|
|
132
|
-
|
|
134
|
+
const joined = inner.replaceAll("\\\n", "");
|
|
135
|
+
if (joined.includes("$("))
|
|
133
136
|
return { kind: "refuse", construct: "a command substitution `$(...)`" };
|
|
134
|
-
if (
|
|
137
|
+
if (joined.includes("`"))
|
|
135
138
|
return { kind: "refuse", construct: "a backtick command substitution" };
|
|
136
|
-
if (
|
|
139
|
+
if (joined.includes("$"))
|
|
137
140
|
state.opaque = true;
|
|
138
|
-
state.word += inner;
|
|
141
|
+
state.word += doubleQuotedWord(inner);
|
|
139
142
|
state.started = true;
|
|
140
143
|
return { kind: "advance", by: end + 1 - i };
|
|
141
144
|
}
|
|
145
|
+
/** The characters a backslash escapes inside double quotes; before any other, it is literal. */
|
|
146
|
+
const DQ_ESCAPABLE = new Set(["$", "`", '"', "\\"]);
|
|
147
|
+
/**
|
|
148
|
+
* The word sh reads from double-quoted text (nax#2551): `\<newline>` is removed, `\$ \` \" \\`
|
|
149
|
+
* keep only the escaped character, and any other backslash stays. A scan, not a replace: in
|
|
150
|
+
* `\\<newline>` the first backslash escapes the second, so the newline stays.
|
|
151
|
+
*/
|
|
152
|
+
function doubleQuotedWord(inner) {
|
|
153
|
+
let out = "";
|
|
154
|
+
for (let i = 0; i < inner.length; i++) {
|
|
155
|
+
const c = inner[i];
|
|
156
|
+
const next = inner[i + 1] ?? "";
|
|
157
|
+
if (c !== "\\" || next === "") {
|
|
158
|
+
out += c;
|
|
159
|
+
}
|
|
160
|
+
else if (next === "\n") {
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
out += DQ_ESCAPABLE.has(next) ? next : c + next;
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
142
170
|
function lexEscape(state, i) {
|
|
143
171
|
if (state.command[i] !== "\\")
|
|
144
172
|
return undefined;
|
|
145
173
|
const next = state.command[i + 1];
|
|
146
174
|
if (next === undefined)
|
|
147
175
|
return { kind: "refuse", construct: "a trailing backslash" };
|
|
176
|
+
// nax#2551: a line continuation contributes nothing — `r\<NL>m` is the word `rm` to sh, so it
|
|
177
|
+
// must be to the policy too. It starts no word, so between words it adds no empty token.
|
|
178
|
+
if (next === "\n")
|
|
179
|
+
return { kind: "advance", by: 2 };
|
|
148
180
|
state.word += next;
|
|
149
181
|
state.started = true;
|
|
150
182
|
return { kind: "advance", by: 2 };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-006: the two session-root Claude Code settings files.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code loads `allow` rules, hooks and MCP servers from these files at
|
|
5
|
+
* the session ROOT, so a write into one weakens every later session started in
|
|
6
|
+
* that workdir — a privilege escalation that survives the run that made it.
|
|
7
|
+
*
|
|
8
|
+
* One canonical list, because two independent guards consume it and they must
|
|
9
|
+
* never diverge:
|
|
10
|
+
*
|
|
11
|
+
* - the OS sandbox denies writes to these paths (`buildSandboxPolicy`), which
|
|
12
|
+
* covers a sandboxed command (Bash, RunCommand) and the ACP edit path;
|
|
13
|
+
* - the native tool policy refuses the same writes (`claudeSettingsWriteRefusal`
|
|
14
|
+
* through `compileToolPolicy`), which is the ONLY guard on the native
|
|
15
|
+
* `Write` / `Edit` / `Delete` tools — they run through Node's fs APIs with no
|
|
16
|
+
* sandbox around them (branch review B1).
|
|
17
|
+
*
|
|
18
|
+
* `.claude` itself is deliberately NOT listed: a project keeps skills, commands
|
|
19
|
+
* and agents under it, so this is a file deny, never a directory one (on Linux
|
|
20
|
+
* srt turns a directory deny into a bind mount over all of them). The sandbox
|
|
21
|
+
* resolves each entry through the same `literal()`/`realOrRaw` path as every
|
|
22
|
+
* other entry, so the deny holds whether or not the file — or `.claude/` itself
|
|
23
|
+
* — exists yet.
|
|
24
|
+
*
|
|
25
|
+
* Only the session root's own files are listed. A monorepo package's
|
|
26
|
+
* `packages/<pkg>/.claude/settings.json` is out of scope (spec, "Out of Scope").
|
|
27
|
+
*/
|
|
28
|
+
export declare const CLAUDE_SETTINGS_FILES: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* Why `tool` may not modify the root-relative path `rel`, or undefined.
|
|
31
|
+
*
|
|
32
|
+
* `rel` must be the policy's OWN canonical spelling (root-relative, `/`-joined,
|
|
33
|
+
* symlink-resolved), never the caller's arguments: matching the caller's
|
|
34
|
+
* spelling would let `./.claude/settings.json` or `.claude/../.claude/x` walk
|
|
35
|
+
* past this guard while reaching the same file.
|
|
36
|
+
*
|
|
37
|
+
* Because `rel` is symlink-resolved, each settings entry is resolved the same
|
|
38
|
+
* way before it is compared (branch review B2): a `.claude/settings.json` that
|
|
39
|
+
* is a symlink, or a `.claude/` that is one, arrives here as its target, and a
|
|
40
|
+
* literal-only match would let the write through. Resolved per call, not at
|
|
41
|
+
* policy compile time, so a symlink created mid-session is still seen.
|
|
42
|
+
*
|
|
43
|
+
* Segment-exact on the full path — a sibling such as `settings.json.bak`, or any
|
|
44
|
+
* other file under `.claude/`, is an ordinary path a tool must still reach.
|
|
45
|
+
*/
|
|
46
|
+
export declare function claudeSettingsWriteRefusal(tool: string, rel: string, resolvedRoot: string): string | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { join, relative, sep } from "node:path";
|
|
2
|
+
import { realOrRaw } from "#src/internal/realpath";
|
|
3
|
+
/**
|
|
4
|
+
* US-006: the two session-root Claude Code settings files.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code loads `allow` rules, hooks and MCP servers from these files at
|
|
7
|
+
* the session ROOT, so a write into one weakens every later session started in
|
|
8
|
+
* that workdir — a privilege escalation that survives the run that made it.
|
|
9
|
+
*
|
|
10
|
+
* One canonical list, because two independent guards consume it and they must
|
|
11
|
+
* never diverge:
|
|
12
|
+
*
|
|
13
|
+
* - the OS sandbox denies writes to these paths (`buildSandboxPolicy`), which
|
|
14
|
+
* covers a sandboxed command (Bash, RunCommand) and the ACP edit path;
|
|
15
|
+
* - the native tool policy refuses the same writes (`claudeSettingsWriteRefusal`
|
|
16
|
+
* through `compileToolPolicy`), which is the ONLY guard on the native
|
|
17
|
+
* `Write` / `Edit` / `Delete` tools — they run through Node's fs APIs with no
|
|
18
|
+
* sandbox around them (branch review B1).
|
|
19
|
+
*
|
|
20
|
+
* `.claude` itself is deliberately NOT listed: a project keeps skills, commands
|
|
21
|
+
* and agents under it, so this is a file deny, never a directory one (on Linux
|
|
22
|
+
* srt turns a directory deny into a bind mount over all of them). The sandbox
|
|
23
|
+
* resolves each entry through the same `literal()`/`realOrRaw` path as every
|
|
24
|
+
* other entry, so the deny holds whether or not the file — or `.claude/` itself
|
|
25
|
+
* — exists yet.
|
|
26
|
+
*
|
|
27
|
+
* Only the session root's own files are listed. A monorepo package's
|
|
28
|
+
* `packages/<pkg>/.claude/settings.json` is out of scope (spec, "Out of Scope").
|
|
29
|
+
*/
|
|
30
|
+
export const CLAUDE_SETTINGS_FILES = [".claude/settings.json", ".claude/settings.local.json"];
|
|
31
|
+
/**
|
|
32
|
+
* The path-mutating tools this deny covers: the three that resolve a path
|
|
33
|
+
* through `compileToolPolicy` and then write it with Node's fs APIs.
|
|
34
|
+
*
|
|
35
|
+
* `GitCommit` is deliberately absent — committing a settings file does not
|
|
36
|
+
* change it, and the deny is about the file's CONTENT. Read-only tools are
|
|
37
|
+
* absent for the other reason: a session must be able to read the settings it
|
|
38
|
+
* is bound by, or a refusal becomes indistinguishable from a broken environment.
|
|
39
|
+
*/
|
|
40
|
+
const CLAUDE_SETTINGS_WRITE_TOOLS = new Set(["Write", "Edit", "Delete"]);
|
|
41
|
+
/**
|
|
42
|
+
* Why `tool` may not modify the root-relative path `rel`, or undefined.
|
|
43
|
+
*
|
|
44
|
+
* `rel` must be the policy's OWN canonical spelling (root-relative, `/`-joined,
|
|
45
|
+
* symlink-resolved), never the caller's arguments: matching the caller's
|
|
46
|
+
* spelling would let `./.claude/settings.json` or `.claude/../.claude/x` walk
|
|
47
|
+
* past this guard while reaching the same file.
|
|
48
|
+
*
|
|
49
|
+
* Because `rel` is symlink-resolved, each settings entry is resolved the same
|
|
50
|
+
* way before it is compared (branch review B2): a `.claude/settings.json` that
|
|
51
|
+
* is a symlink, or a `.claude/` that is one, arrives here as its target, and a
|
|
52
|
+
* literal-only match would let the write through. Resolved per call, not at
|
|
53
|
+
* policy compile time, so a symlink created mid-session is still seen.
|
|
54
|
+
*
|
|
55
|
+
* Segment-exact on the full path — a sibling such as `settings.json.bak`, or any
|
|
56
|
+
* other file under `.claude/`, is an ordinary path a tool must still reach.
|
|
57
|
+
*/
|
|
58
|
+
export function claudeSettingsWriteRefusal(tool, rel, resolvedRoot) {
|
|
59
|
+
if (!CLAUDE_SETTINGS_WRITE_TOOLS.has(tool))
|
|
60
|
+
return undefined;
|
|
61
|
+
const entry = CLAUDE_SETTINGS_FILES.find((e) => e === rel || resolvedSettingsRel(resolvedRoot, e) === rel);
|
|
62
|
+
if (entry === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const subject = entry === rel
|
|
65
|
+
? `"${rel}" is a Claude Code settings file`
|
|
66
|
+
: `"${rel}" is the target of the Claude Code settings file "${entry}"`;
|
|
67
|
+
return (`${subject}: Claude loads allow rules, hooks and MCP servers from it for every ` +
|
|
68
|
+
"session started in this workdir, so a change here outlives this run and is not made from inside it. " +
|
|
69
|
+
"The sandbox refuses writes to it as well; a human changes it by hand, outside the run.");
|
|
70
|
+
}
|
|
71
|
+
/** `entry`'s symlink-resolved spelling, relative to the already-resolved root, `/`-joined. */
|
|
72
|
+
function resolvedSettingsRel(resolvedRoot, entry) {
|
|
73
|
+
return relative(resolvedRoot, realOrRaw(join(resolvedRoot, entry)))
|
|
74
|
+
.split(sep)
|
|
75
|
+
.join("/");
|
|
76
|
+
}
|
package/dist/sandbox/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { buildSandboxPolicy, type SandboxPolicyInput } from "./policy-builder.ts
|
|
|
6
6
|
export { _policyInputDeps, defaultTempRoots, type GitLayout, listCredentialFiles, listNaxEntries, resolveGitLayout, runTempRoots, } from "./policy-inputs.ts";
|
|
7
7
|
export { _probeDeps, probeSandbox } from "./probe.ts";
|
|
8
8
|
export { _resetSandboxRegistryForTests, _sandboxRegistryDeps, probeSandboxOnce, resetSandboxBackend, sandboxBackendFor, warnSandboxUnavailableOnce, } from "./registry.ts";
|
|
9
|
+
export { sessionSandboxFrom } from "./session-sandbox.ts";
|
|
9
10
|
export { _sessionTmpDeps, runTmpRoot, sessionTmpDir, sessionTmpDirUnder } from "./session-tmp.ts";
|
|
10
11
|
export { _srtBackendDeps, createSrtBackend } from "./srt-backend.ts";
|
|
11
|
-
export type { CommandLauncher, LaunchRequest, LaunchResult, LaunchSpec, ProbeResult, SandboxBackend, SandboxBackendName, SandboxNetworkPolicy, SandboxPolicy, SandboxRecord, SandboxState, SandboxWrapRequest, } from "./types.ts";
|
|
12
|
+
export type { CommandLauncher, LaunchRequest, LaunchResult, LaunchSpec, ProbeResult, SandboxBackend, SandboxBackendName, SandboxNetworkPolicy, SandboxPolicy, SandboxRecord, SandboxState, SandboxWrapRequest, SessionSandbox, } from "./types.ts";
|
package/dist/sandbox/index.js
CHANGED
|
@@ -6,5 +6,6 @@ export { buildSandboxPolicy } from "./policy-builder.js";
|
|
|
6
6
|
export { _policyInputDeps, defaultTempRoots, listCredentialFiles, listNaxEntries, resolveGitLayout, runTempRoots, } from "./policy-inputs.js";
|
|
7
7
|
export { _probeDeps, probeSandbox } from "./probe.js";
|
|
8
8
|
export { _resetSandboxRegistryForTests, _sandboxRegistryDeps, probeSandboxOnce, resetSandboxBackend, sandboxBackendFor, warnSandboxUnavailableOnce, } from "./registry.js";
|
|
9
|
+
export { sessionSandboxFrom } from "./session-sandbox.js";
|
|
9
10
|
export { _sessionTmpDeps, runTmpRoot, sessionTmpDir, sessionTmpDirUnder } from "./session-tmp.js";
|
|
10
11
|
export { _srtBackendDeps, createSrtBackend } from "./srt-backend.js";
|
package/dist/sandbox/launcher.js
CHANGED
|
@@ -156,5 +156,9 @@ export function createCommandLauncher(opts) {
|
|
|
156
156
|
await opts.afterWrapped?.();
|
|
157
157
|
}
|
|
158
158
|
},
|
|
159
|
+
// nax#2500: `false` keeps /tmp/claude writable; Claude's own srt forces that TMPDIR.
|
|
160
|
+
...(opts.policyFor !== undefined
|
|
161
|
+
? { policyAt: (root) => opts.policyFor(root, false) }
|
|
162
|
+
: {}),
|
|
159
163
|
};
|
|
160
164
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* always know whether it is sandboxed and what it may write (spec S5).
|
|
5
5
|
*/
|
|
6
6
|
export declare const LIKELY_SANDBOX_DENIAL: RegExp;
|
|
7
|
-
export declare function sandboxSentence(network: "open" | readonly string[], sharedTmp?: boolean): string;
|
|
7
|
+
export declare function sandboxSentence(network: "open" | readonly string[], sharedTmp?: boolean, platform?: NodeJS.Platform): string;
|
|
8
8
|
export declare function unsandboxedSentence(reason: string): string;
|
|
9
9
|
export declare function rawBashRefusalReason(reason: string): string;
|
|
10
10
|
/**
|
package/dist/sandbox/messages.js
CHANGED
|
@@ -18,12 +18,24 @@ function describeNetwork(network) {
|
|
|
18
18
|
* write; a shared-temp session keeps the wording it had before.
|
|
19
19
|
*/
|
|
20
20
|
const tempRootsPhrase = (sharedTmp) => sharedTmp ? "the system temp directories" : "this run's temp directory ($TMPDIR)";
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* US-006: on macOS srt refuses every setuid program, `/bin/ps` among them, and
|
|
23
|
+
* kills a non-setuid copy with SIGKILL. A test that shells out to ps therefore
|
|
24
|
+
* fails for a reason that has nothing to do with the test, and an agent reading
|
|
25
|
+
* only "Operation not permitted" has no way to tell the two apart. Appended on
|
|
26
|
+
* darwin alone: on Linux ps runs, where the sentence would excuse a real
|
|
27
|
+
* failure. It is deliberately in `sandboxSentence` rather than at each call
|
|
28
|
+
* site so every description that advertises a WORKING sandbox carries it.
|
|
29
|
+
*/
|
|
30
|
+
const DARWIN_SETUID_REFUSAL_SENTENCE = 'On macOS the sandbox also refuses ps and other setuid programs ("Operation not permitted"), so a test that ' +
|
|
31
|
+
"shells out to ps fails here for that reason alone.";
|
|
32
|
+
export function sandboxSentence(network, sharedTmp = true, platform) {
|
|
33
|
+
const sentence = `inside an OS sandbox: writes are allowed only under the repository root, ${tempRootsPhrase(sharedTmp)} and ` +
|
|
23
34
|
"package-manager caches -- and nothing under .nax/ except .nax/scratchpad/, since .nax/ is nax's own state " +
|
|
24
35
|
"(change a feature's acceptance test with the Edit tool); credential files (~/.ssh, ~/.aws, ~/.npmrc, nax credentials and similar) are unreadable; " +
|
|
25
36
|
`${describeNetwork(network)}. A write anywhere else fails with "Operation not permitted" or "Read-only file system" ` +
|
|
26
|
-
"-- that is the sandbox, not a bug in your command."
|
|
37
|
+
"-- that is the sandbox, not a bug in your command.";
|
|
38
|
+
return platform === "darwin" ? `${sentence} ${DARWIN_SETUID_REFUSAL_SENTENCE}` : sentence;
|
|
27
39
|
}
|
|
28
40
|
export function unsandboxedSentence(reason) {
|
|
29
41
|
return `Commands are NOT sandboxed on this machine (${reason}).`;
|
|
@@ -10,6 +10,7 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
10
10
|
import { NaxError } from "#src/infra/index";
|
|
11
11
|
import { realOrRaw } from "#src/internal/realpath";
|
|
12
12
|
import { SANDBOX_GLOB_CHARS } from "../config/schemas-sandbox.js";
|
|
13
|
+
import { CLAUDE_SETTINGS_FILES } from "./claude-settings.js";
|
|
13
14
|
import { BUILTIN_CACHE_WRITE_ROOTS, BUILTIN_CREDENTIAL_READ_DENIES, MACOS_CACHE_WRITE_ROOT, SRT_MACOS_TMPDIR, SRT_MACOS_TMPDIR_DENIES, } from "./defaults.js";
|
|
14
15
|
import { WORKTREE_COMMON_WRITE_DIRS, WORKTREE_CONFIG_FILE } from "./git-guards.js";
|
|
15
16
|
function expandHome(p, home) {
|
|
@@ -35,7 +36,13 @@ function gitDenies(root, git) {
|
|
|
35
36
|
const common = git.kind === "worktree" ? git.commonDir : git.gitDir;
|
|
36
37
|
// config.worktree is read when extensions.worktreeConfig is on; an absent one
|
|
37
38
|
// is safe to deny (srt stubs it empty, and empty is valid config) (#2198).
|
|
38
|
-
|
|
39
|
+
// info/ holds exclude and sparse-checkout: an agent-written ignore rule hides files from review guards (#2552).
|
|
40
|
+
const shared = [
|
|
41
|
+
join(common, "hooks"),
|
|
42
|
+
join(common, "config"),
|
|
43
|
+
join(common, "info"),
|
|
44
|
+
join(common, WORKTREE_CONFIG_FILE),
|
|
45
|
+
];
|
|
39
46
|
if (git.kind === "main")
|
|
40
47
|
return shared;
|
|
41
48
|
// A worktree's `.git` is a pointer FILE, and gitdir/commondir point back;
|
|
@@ -47,6 +54,8 @@ function gitDenies(root, git) {
|
|
|
47
54
|
join(git.gitDir, "gitdir"),
|
|
48
55
|
join(git.gitDir, "commondir"),
|
|
49
56
|
join(git.gitDir, WORKTREE_CONFIG_FILE),
|
|
57
|
+
// A linked worktree keeps its own info/ (sparse-checkout) beside the shared one (#2552).
|
|
58
|
+
join(git.gitDir, "info"),
|
|
50
59
|
];
|
|
51
60
|
}
|
|
52
61
|
/**
|
|
@@ -141,6 +150,10 @@ export function buildSandboxPolicy(input) {
|
|
|
141
150
|
...input.gitGuardFiles,
|
|
142
151
|
...(input.approvalsFile !== undefined ? [input.approvalsFile] : []),
|
|
143
152
|
...(input.trustStoreFile !== undefined ? [input.trustStoreFile] : []),
|
|
153
|
+
// US-006: the two session-root Claude settings files. One canonical list
|
|
154
|
+
// with the native tool policy that refuses the same paths (B1) -- see
|
|
155
|
+
// ./claude-settings.ts for why each is denied and why `.claude` is not.
|
|
156
|
+
...CLAUDE_SETTINGS_FILES.map((rel) => join(root, rel)),
|
|
144
157
|
...(darwin && confined ? SRT_MACOS_TMPDIR_DENIES : []),
|
|
145
158
|
]);
|
|
146
159
|
const denyRead = literal([
|
|
@@ -149,5 +162,17 @@ export function buildSandboxPolicy(input) {
|
|
|
149
162
|
...config.filesystem.denyRead.map((p) => resolve(root, expandHome(p, home))),
|
|
150
163
|
]);
|
|
151
164
|
const allowed = config.network.allowedDomains;
|
|
152
|
-
|
|
165
|
+
// US-002: the flag is carried only when the config asks for it, so an absent
|
|
166
|
+
// -- or explicitly false -- config leaves `network` without the key. The type
|
|
167
|
+
// only admits `true` (see SandboxNetworkPolicy), and srt treats absent as off.
|
|
168
|
+
const loopback = config.network.allowLocalBinding === true;
|
|
169
|
+
return {
|
|
170
|
+
writeRoots,
|
|
171
|
+
denyWrite,
|
|
172
|
+
denyRead,
|
|
173
|
+
network: {
|
|
174
|
+
...(allowed === undefined ? {} : { allowedDomains: [...allowed] }),
|
|
175
|
+
...(loopback ? { allowLocalBinding: true } : {}),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
153
178
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** nax#2500: a launcher's sandbox as plain data for a backend that runs its own shell. */
|
|
2
|
+
import type { CommandLauncher, SessionSandbox } from "./types.ts";
|
|
3
|
+
export declare function sessionSandboxFrom(launcher: CommandLauncher | undefined, root: string): Promise<SessionSandbox | undefined>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function sessionSandboxFrom(launcher, root) {
|
|
2
|
+
if (launcher === undefined || launcher.state.kind === "disabled")
|
|
3
|
+
return undefined;
|
|
4
|
+
if (launcher.state.kind === "unavailable")
|
|
5
|
+
return { kind: "unavailable", reason: launcher.state.reason };
|
|
6
|
+
if (launcher.policyAt === undefined)
|
|
7
|
+
return { kind: "unavailable", reason: "the launcher exposes no policy" };
|
|
8
|
+
return { kind: "policy", policy: await launcher.policyAt(root) };
|
|
9
|
+
}
|
|
@@ -12,8 +12,17 @@ import { mkdir } from "node:fs/promises";
|
|
|
12
12
|
import type { SandboxConfig } from "../config/schemas-sandbox.ts";
|
|
13
13
|
import type { SandboxBackend } from "./types.ts";
|
|
14
14
|
type SrtModule = typeof import("@anthropic-ai/sandbox-runtime");
|
|
15
|
+
/**
|
|
16
|
+
* The slice of the srt module this backend calls. Narrowed on purpose: this seam
|
|
17
|
+
* is what a test replaces, and a double for the whole module needs an
|
|
18
|
+
* `as unknown as` cast, which test/ forbids (check-test-as-unknown-as). The real
|
|
19
|
+
* `typeof import(...)` satisfies this structurally, so nothing is lost.
|
|
20
|
+
*/
|
|
21
|
+
type SrtModuleSlice = {
|
|
22
|
+
SandboxManager: Pick<SrtModule["SandboxManager"], "initialize" | "isSupportedPlatform" | "wrapWithSandboxArgv" | "annotateStderrWithSandboxFailures" | "cleanupAfterCommand" | "reset">;
|
|
23
|
+
};
|
|
15
24
|
export declare const _srtBackendDeps: {
|
|
16
|
-
load: () => Promise<
|
|
25
|
+
load: () => Promise<SrtModuleSlice>;
|
|
17
26
|
platform: () => NodeJS.Platform;
|
|
18
27
|
mkdir: typeof mkdir;
|
|
19
28
|
};
|
|
@@ -22,11 +22,18 @@ export const _srtBackendDeps = {
|
|
|
22
22
|
* `network`, so the object must exist. This is the one cast in the module; the
|
|
23
23
|
* live suite pins the behaviour (open => no proxy variables in the argv), so
|
|
24
24
|
* an srt bump that changes it fails a test rather than silently restricting.
|
|
25
|
+
*
|
|
26
|
+
* US-002 (#2538): `allowLocalBinding` rides along. srt reads it once per process
|
|
27
|
+
* from THIS object (`getAllowLocalBinding`, sandbox-manager.js:1171), never from
|
|
28
|
+
* a per-command `customConfig`, so it is emitted only here. Only `true` is
|
|
29
|
+
* emitted: srt's default is off, so absent and `false` are equivalent, and the
|
|
30
|
+
* policy type (`SandboxNetworkPolicy`) cannot express an explicit `false`.
|
|
25
31
|
*/
|
|
26
|
-
function srtNetwork(allowedDomains) {
|
|
32
|
+
function srtNetwork(allowedDomains, allowLocalBinding) {
|
|
33
|
+
const localBinding = allowLocalBinding === true ? { allowLocalBinding: true } : {};
|
|
27
34
|
if (allowedDomains !== undefined)
|
|
28
|
-
return { allowedDomains: [...allowedDomains], deniedDomains: [] };
|
|
29
|
-
const open = { deniedDomains: [] };
|
|
35
|
+
return { allowedDomains: [...allowedDomains], deniedDomains: [], ...localBinding };
|
|
36
|
+
const open = { deniedDomains: [], ...localBinding };
|
|
30
37
|
return open;
|
|
31
38
|
}
|
|
32
39
|
function customConfig(policy) {
|
|
@@ -36,6 +43,9 @@ function customConfig(policy) {
|
|
|
36
43
|
allowWrite: [...policy.writeRoots],
|
|
37
44
|
denyWrite: [...policy.denyWrite],
|
|
38
45
|
},
|
|
46
|
+
// US-002: `allowLocalBinding` is deliberately NOT here. srt applies it
|
|
47
|
+
// process-wide, from the `initialize` config, so a copy on the per-command
|
|
48
|
+
// config would promise a per-command effect it does not have.
|
|
39
49
|
...(policy.network.allowedDomains !== undefined ? { network: srtNetwork(policy.network.allowedDomains) } : {}),
|
|
40
50
|
};
|
|
41
51
|
}
|
|
@@ -49,7 +59,7 @@ export function createSrtBackend(network) {
|
|
|
49
59
|
if (_srtBackendDeps.platform() === "darwin")
|
|
50
60
|
await _srtBackendDeps.mkdir(SRT_MACOS_TMPDIR, { recursive: true });
|
|
51
61
|
await mod.SandboxManager.initialize({
|
|
52
|
-
network: srtNetwork(network.allowedDomains),
|
|
62
|
+
network: srtNetwork(network.allowedDomains, network.allowLocalBinding),
|
|
53
63
|
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
|
|
54
64
|
});
|
|
55
65
|
loaded = mod;
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ export type SandboxBackendName = "srt";
|
|
|
10
10
|
export interface SandboxNetworkPolicy {
|
|
11
11
|
/** Absent = unrestricted. */
|
|
12
12
|
readonly allowedDomains?: readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* US-002: srt may let a command bind to -- and reach -- loopback ports, so a
|
|
15
|
+
* DB-backed test run sees a local server that is up (#2538).
|
|
16
|
+
*
|
|
17
|
+
* `true` only. srt's own default is off, so absent and `false` are the same
|
|
18
|
+
* thing to it, and a policy that cannot express "off" cannot contradict the
|
|
19
|
+
* config it was built from.
|
|
20
|
+
*/
|
|
21
|
+
readonly allowLocalBinding?: true;
|
|
13
22
|
}
|
|
14
23
|
export interface SandboxPolicy {
|
|
15
24
|
readonly writeRoots: readonly string[];
|
|
@@ -17,6 +26,18 @@ export interface SandboxPolicy {
|
|
|
17
26
|
readonly denyRead: readonly string[];
|
|
18
27
|
readonly network: SandboxNetworkPolicy;
|
|
19
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* nax#2500: the sandbox an embedder hands a backend that runs its own shell
|
|
31
|
+
* (ACP Claude). Absent = sandbox disabled by config. `unavailable` = enabled but
|
|
32
|
+
* unusable on this host; the backend must refuse rather than run unsandboxed.
|
|
33
|
+
*/
|
|
34
|
+
export type SessionSandbox = {
|
|
35
|
+
readonly kind: "policy";
|
|
36
|
+
readonly policy: SandboxPolicy;
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: "unavailable";
|
|
39
|
+
readonly reason: string;
|
|
40
|
+
};
|
|
20
41
|
export type ProbeResult = {
|
|
21
42
|
readonly available: true;
|
|
22
43
|
} | {
|
|
@@ -99,4 +120,6 @@ export interface LaunchResult extends ArgvExecResult {
|
|
|
99
120
|
export interface CommandLauncher {
|
|
100
121
|
readonly state: SandboxState;
|
|
101
122
|
run(req: LaunchRequest): Promise<LaunchResult>;
|
|
123
|
+
/** nax#2500: the policy this launcher would apply at `root`, without the session TMPDIR. Present when available. */
|
|
124
|
+
policyAt?(root: string): Promise<SandboxPolicy>;
|
|
102
125
|
}
|
|
@@ -36,7 +36,7 @@ const OptionsSchema = z.strictObject({
|
|
|
36
36
|
.string()
|
|
37
37
|
.regex(SESSION_ID, "must be 1-128 letters, digits, '.', '_' or '-', starting with a letter or digit")
|
|
38
38
|
.optional(),
|
|
39
|
-
profile: z.enum(["none", "read", "ask", "full"]),
|
|
39
|
+
profile: z.enum(["none", "read", "review", "ask", "full"]),
|
|
40
40
|
workdir: z
|
|
41
41
|
.string()
|
|
42
42
|
.refine((dir) => isAbsolute(dir), "must be an absolute path")
|
|
@@ -13,7 +13,7 @@ import type { JSONSchema } from "./tool-descriptor.ts";
|
|
|
13
13
|
* What the session's agent may touch. Capability statements, not tool lists
|
|
14
14
|
* (spec 4.5). `ask`: every mutating action is approved through `answer()`.
|
|
15
15
|
*/
|
|
16
|
-
export type AgentSessionProfile = "none" | "read" | "ask" | "full";
|
|
16
|
+
export type AgentSessionProfile = "none" | "read" | "review" | "ask" | "full";
|
|
17
17
|
export interface EmbedderToolContext {
|
|
18
18
|
readonly sessionId: string;
|
|
19
19
|
/** The provider's tool-call id: the same id as the `tool_call` / `tool_result` events. */
|
|
@@ -69,12 +69,14 @@ export function parseNativeBackendOptions(input) {
|
|
|
69
69
|
return { raw: options, provider: providerOf(options.model) };
|
|
70
70
|
}
|
|
71
71
|
export function nativeProfileRules(profile, raw) {
|
|
72
|
-
const tools = profile === "full" || profile === "ask";
|
|
72
|
+
const tools = profile === "full" || profile === "ask" || profile === "review";
|
|
73
73
|
if (!tools && raw.bashApproval !== undefined) {
|
|
74
|
-
throw invalid('bashApproval applies to profiles "ask" and "
|
|
74
|
+
throw invalid('bashApproval applies to profiles "ask", "full" and "review" only', { path: "backend.bashApproval" });
|
|
75
75
|
}
|
|
76
76
|
if (!tools && raw.allowUnsandboxed !== undefined) {
|
|
77
|
-
throw invalid('allowUnsandboxed applies to profiles "ask" and "
|
|
77
|
+
throw invalid('allowUnsandboxed applies to profiles "ask", "full" and "review" only', {
|
|
78
|
+
path: "backend.allowUnsandboxed",
|
|
79
|
+
});
|
|
78
80
|
}
|
|
79
81
|
const bashApproval = raw.bashApproval ?? "gated";
|
|
80
82
|
if (profile === "ask" && bashApproval !== "gated") {
|
|
@@ -64,6 +64,15 @@ export interface BackendOpenContext {
|
|
|
64
64
|
readonly metadata: Readonly<Record<string, string>>;
|
|
65
65
|
/** Aborted when the session starts closing. */
|
|
66
66
|
readonly openSignal: AbortSignal;
|
|
67
|
+
/** nax#2500: present only for profiles review/full when the embedder supplied one. */
|
|
68
|
+
readonly sandbox?: import("#src/sandbox/types").SessionSandbox;
|
|
69
|
+
/**
|
|
70
|
+
* The rendered `## MCP servers` section for this open (#2517). The ACP
|
|
71
|
+
* backend (nax-agent-acp) offers it to its tool host, so the agent's own MCP
|
|
72
|
+
* client receives it in `initialize`; the native backend leaves it alone —
|
|
73
|
+
* native reaches the same text through `OpenSessionOpts.providerInstructions`.
|
|
74
|
+
*/
|
|
75
|
+
readonly providerInstructions?: string;
|
|
67
76
|
}
|
|
68
77
|
export interface OpenedBackend {
|
|
69
78
|
readonly adapter: AgentSessionAdapter;
|
|
@@ -29,7 +29,7 @@ export interface SessionLauncherArgs {
|
|
|
29
29
|
readonly bashApproval: BashApprovalMode;
|
|
30
30
|
readonly allowUnsandboxed: boolean;
|
|
31
31
|
}
|
|
32
|
-
/** The sandbox floor (spec 6.3): "ask" and "
|
|
32
|
+
/** The sandbox floor (spec 6.3): "ask", "full" and "review" need a usable sandbox unless gated + allowUnsandboxed. */
|
|
33
33
|
export declare function resolveSessionLauncher(args: SessionLauncherArgs): Promise<CommandLauncher | undefined>;
|
|
34
34
|
export interface SessionToolSupportArgs {
|
|
35
35
|
readonly profile: AgentSessionProfile;
|
|
@@ -21,6 +21,8 @@ export function declaredToolsFor(profile, protectedPaths) {
|
|
|
21
21
|
return [...UNIVERSAL_CODING_TOOLS];
|
|
22
22
|
if (profile === "read")
|
|
23
23
|
return [...UNIVERSAL_CODING_TOOLS, ...READ_TOOLS];
|
|
24
|
+
if (profile === "review")
|
|
25
|
+
return [...UNIVERSAL_CODING_TOOLS, ...READ_TOOLS, "Bash"];
|
|
24
26
|
// GitCommit refuses every path without ignore patterns, so it is offered only with them.
|
|
25
27
|
const commit = protectedPaths.gitIgnorePatterns.length > 0 ? ["GitCommit"] : [];
|
|
26
28
|
return [...UNIVERSAL_CODING_TOOLS, ...READ_TOOLS, ...WRITE_TOOLS, ...commit];
|
|
@@ -57,9 +59,9 @@ export function defaultProtectedPaths(ownCredentials) {
|
|
|
57
59
|
const credentialDir = ownCredentials ? undefined : configuredCredentialDir();
|
|
58
60
|
return credentialDir === undefined ? base : { ...base, credentialDir };
|
|
59
61
|
}
|
|
60
|
-
/** The sandbox floor (spec 6.3): "ask" and "
|
|
62
|
+
/** The sandbox floor (spec 6.3): "ask", "full" and "review" need a usable sandbox unless gated + allowUnsandboxed. */
|
|
61
63
|
export async function resolveSessionLauncher(args) {
|
|
62
|
-
if (args.profile !== "full" && args.profile !== "ask")
|
|
64
|
+
if (args.profile !== "full" && args.profile !== "ask" && args.profile !== "review")
|
|
63
65
|
return undefined;
|
|
64
66
|
const launcher = await resolveSessionSandbox({
|
|
65
67
|
config: DEFAULT_SANDBOX_CONFIG,
|
|
@@ -155,6 +155,8 @@ export interface OpenSessionOpts extends TrackedSpawnDeadlineOptions {
|
|
|
155
155
|
providerTools?: readonly import("#src/tools/registry").CodingTool[];
|
|
156
156
|
/** ACP: the op's materialized declaration, which narrows the session profile (nax#2444, D2). */
|
|
157
157
|
declaredTools?: readonly string[];
|
|
158
|
+
/** nax#2500: the dispatch sandbox for ACP backends that run their own shell (claude). Native ignores it. */
|
|
159
|
+
sandbox?: import("#src/sandbox/types").SessionSandbox;
|
|
158
160
|
/** ACP: the transport-less surface draft; the adapter stamps transport + profile (nax#2410, D4). */
|
|
159
161
|
toolSurface?: import("#src/tools/tool-audit").ToolSurfaceDraft;
|
|
160
162
|
/**
|
|
@@ -194,6 +196,14 @@ export interface OpenSessionOpts extends TrackedSpawnDeadlineOptions {
|
|
|
194
196
|
* facade sets it from `instructions`; nax leaves it unset. ACP ignores it.
|
|
195
197
|
*/
|
|
196
198
|
systemPrompt?: string;
|
|
199
|
+
/**
|
|
200
|
+
* Native: the rendered `## MCP servers` section, appended to `systemPrompt`
|
|
201
|
+
* after a blank line (#2517) — one `system` field, so the instructions reach
|
|
202
|
+
* the model exactly where `systemPrompt` already goes and no second state
|
|
203
|
+
* map is needed. Set together with `systemPrompt` by nax
|
|
204
|
+
* (`openSessionExtras`); either may be set alone. ACP ignores it.
|
|
205
|
+
*/
|
|
206
|
+
providerInstructions?: string;
|
|
197
207
|
/** Native: repository-relative instruction scopes, independent of the file-tool root. */
|
|
198
208
|
instructionDirectories?: readonly string[];
|
|
199
209
|
/** Native: instruction Markdown basename; defaults to AGENTS.md and its legacy fallback chain. */
|