@nathapp/nax-agent 0.1.0 → 0.2.0
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 +22 -0
- package/README.md +47 -2
- package/dist/coding-tools/coding-tool-sandbox.d.ts +3 -0
- package/dist/coding-tools/coding-tool-sandbox.js +14 -3
- package/dist/coding-tools/coding-tool-support.d.ts +3 -0
- package/dist/coding-tools/coding-tool-support.js +2 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +4 -1
- package/dist/internal/redact.d.ts +23 -0
- package/dist/internal/redact.js +60 -0
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +8 -0
- package/dist/native/adapter-deps.d.ts +3 -3
- package/dist/native/adapter-deps.js +3 -3
- package/dist/native/client.d.ts +7 -1
- package/dist/native/client.js +1 -1
- package/dist/native/credentials/change-guard.d.ts +8 -1
- package/dist/native/credentials/change-guard.js +1 -1
- package/dist/native/credentials/fingerprint.d.ts +4 -1
- package/dist/native/credentials/fingerprint.js +6 -3
- package/dist/native/credentials/session-source.d.ts +12 -0
- package/dist/native/credentials/session-source.js +33 -0
- package/dist/native/index.d.ts +3 -1
- package/dist/native/index.js +2 -1
- package/dist/native/session/loop-events/types.d.ts +1 -1
- package/dist/native/session/loop-handlers.d.ts +3 -0
- package/dist/native/session/loop-handlers.js +1 -1
- package/dist/native/session/memory-transcript-store.d.ts +12 -0
- package/dist/native/session/memory-transcript-store.js +36 -0
- package/dist/native/session/session.d.ts +138 -96
- package/dist/native/session/session.js +107 -127
- package/dist/native/session/transcript-identity.d.ts +39 -0
- package/dist/native/session/transcript-identity.js +81 -0
- package/dist/native/session/transcript-store.d.ts +22 -21
- package/dist/native/session/transcript-store.js +59 -68
- package/dist/native/session/transcript-types.d.ts +38 -0
- package/dist/native/session/transcript-types.js +7 -0
- package/dist/native/session/truncation-handler.d.ts +4 -3
- package/dist/native/session/truncation-handler.js +7 -10
- package/dist/native/session/turn-complete-step.d.ts +4 -1
- package/dist/native/session/turn-complete-step.js +11 -4
- package/dist/native/session/turn-event-emitter.d.ts +48 -0
- package/dist/native/session/turn-event-emitter.js +134 -0
- package/dist/native/session/turn-loop-round-trip.d.ts +5 -2
- package/dist/native/session/turn-loop-round-trip.js +8 -4
- package/dist/native/session/turn-loop.js +27 -13
- package/dist/native/session/turn-result.d.ts +1 -1
- package/dist/native/session/turn-result.js +1 -1
- package/dist/native/session/turn-tool-batch.d.ts +3 -0
- package/dist/native/session/turn-tool-batch.js +14 -6
- package/dist/native/session/turn-types.d.ts +17 -1
- package/dist/native/session-adapter.d.ts +22 -4
- package/dist/native/session-adapter.js +79 -35
- package/dist/native/stream-complete.d.ts +17 -0
- package/dist/native/stream-complete.js +40 -0
- package/dist/permissions/approvals-link.js +5 -4
- package/dist/sandbox/policy-builder.d.ts +6 -1
- package/dist/sandbox/policy-builder.js +18 -14
- package/dist/session/agent-session-deps.d.ts +12 -0
- package/dist/session/agent-session-deps.js +28 -0
- package/dist/session/agent-session-errors.d.ts +11 -0
- package/dist/session/agent-session-errors.js +12 -0
- package/dist/session/agent-session-options.d.ts +16 -0
- package/dist/session/agent-session-options.js +137 -0
- package/dist/session/agent-session-resume.d.ts +12 -0
- package/dist/session/agent-session-resume.js +47 -0
- package/dist/session/agent-session-turn.d.ts +45 -0
- package/dist/session/agent-session-turn.js +179 -0
- package/dist/session/agent-session-types.d.ts +182 -0
- package/dist/session/agent-session-types.js +1 -0
- package/dist/session/agent-session.d.ts +8 -0
- package/dist/session/agent-session.js +223 -0
- package/dist/session/pending-asks.d.ts +24 -0
- package/dist/session/pending-asks.js +77 -0
- package/dist/session/session-ask-link.d.ts +26 -0
- package/dist/session/session-ask-link.js +52 -0
- package/dist/session/session-event-channel.d.ts +17 -0
- package/dist/session/session-event-channel.js +132 -0
- package/dist/session/session-interaction.d.ts +20 -0
- package/dist/session/session-interaction.js +164 -0
- package/dist/session/session-tool-support.d.ts +48 -0
- package/dist/session/session-tool-support.js +101 -0
- package/dist/session/session-types.d.ts +31 -2
- package/dist/session/turn-event.d.ts +61 -0
- package/dist/session/turn-event.js +1 -0
- package/dist/tools/bash-cwd.d.ts +3 -3
- package/dist/tools/credential-read-deny.d.ts +22 -0
- package/dist/tools/credential-read-deny.js +56 -0
- package/dist/tools/glob.js +9 -2
- package/dist/tools/grep.js +55 -26
- package/dist/tools/owned-paths.d.ts +43 -0
- package/dist/tools/owned-paths.js +17 -0
- package/dist/tools/package-managers.d.ts +3 -0
- package/dist/tools/package-managers.js +7 -3
- package/dist/tools/policy-bash-raw.d.ts +3 -0
- package/dist/tools/policy-bash-raw.js +49 -62
- package/dist/tools/policy-command-branch.d.ts +3 -0
- package/dist/tools/policy-command-branch.js +2 -1
- package/dist/tools/policy-paths-branch.d.ts +14 -8
- package/dist/tools/policy-paths-branch.js +20 -19
- package/dist/tools/policy.d.ts +10 -7
- package/dist/tools/policy.js +9 -7
- package/dist/tools/protected-paths.d.ts +6 -6
- package/dist/tools/read.js +38 -20
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/run-command-exec.js +1 -0
- package/dist/tools/runtime.d.ts +3 -0
- package/dist/tools/runtime.js +1 -0
- package/dist/tools/scratchpad.js +2 -1
- package/dist/tools/types.d.ts +2 -2
- package/package.json +2 -2
- package/dist/tools/nax-owned-writes.d.ts +0 -135
- package/dist/tools/nax-owned-writes.js +0 -259
package/dist/tools/grep.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { drainBounded } from "#src/internal/bounded-io";
|
|
14
14
|
import { runtimeSpawn } from "#src/runtime/index";
|
|
15
15
|
import { which } from "#src/runtime/which";
|
|
16
|
+
import { containsCredentialPath, credentialReadRefusal } from "./credential-read-deny.js";
|
|
16
17
|
import { cutToByteCap, READ_CEILING } from "./truncate.js";
|
|
17
18
|
const GREP_TIMEOUT_MS = 15_000;
|
|
18
19
|
/** @internal */
|
|
@@ -30,6 +31,52 @@ export const _grepDeps = {
|
|
|
30
31
|
which: (name) => which(name),
|
|
31
32
|
spawn: runtimeSpawn,
|
|
32
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The two independent caveats a search may carry, either of which means the
|
|
36
|
+
* search the caller asked for is not quite the search that ran:
|
|
37
|
+
*
|
|
38
|
+
* 1. The literal disclosure fires whenever the search was literal AND the
|
|
39
|
+
* pattern contained a metacharacter a regex would have treated specially --
|
|
40
|
+
* regardless of whether it matched. #1876 only covered the zero-match case;
|
|
41
|
+
* a literal match on a pattern like "foo.bar" is the WORSE case (#1922):
|
|
42
|
+
* the caller has positive evidence and no cue that "." was never a
|
|
43
|
+
* wildcard, so a real regex match was never attempted.
|
|
44
|
+
*
|
|
45
|
+
* 2. The dialect note fires when regex mode ran through the grep fallback.
|
|
46
|
+
* ripgrep's Rust regex and POSIX ERE are NOT the same language: `\d`, `\w`
|
|
47
|
+
* and `\b` are unsupported in ERE, and whether they degrade to a literal or
|
|
48
|
+
* to something else differs BY PLATFORM (GNU grep treats `\d` as `d`; the
|
|
49
|
+
* BSD grep on macOS matches a digit). Silently returning a different answer
|
|
50
|
+
* depending on which binary the machine happens to have is exactly the
|
|
51
|
+
* failure this file's header forbids of the fallback, so the divergence is
|
|
52
|
+
* disclosed rather than hidden.
|
|
53
|
+
*/
|
|
54
|
+
function searchCaveat(mode, binary, pattern) {
|
|
55
|
+
const notes = [];
|
|
56
|
+
if (mode === "literal" && containsRegexMetacharacter(pattern)) {
|
|
57
|
+
notes.push("The search was performed literally and regex metacharacters were not interpreted.");
|
|
58
|
+
}
|
|
59
|
+
if (mode === "regex" && binary === "grep") {
|
|
60
|
+
notes.push("ripgrep is not installed here, so the pattern was matched with POSIX ERE via grep: escapes such as \\d, \\w and \\b are NOT supported — use [0-9], [A-Za-z0-9_] and similar instead.");
|
|
61
|
+
}
|
|
62
|
+
return notes.join(" ");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The credential read-deny for a search rooted at `searchRoot` (S3 spec 6.3),
|
|
66
|
+
* or undefined. Two shapes: the search root itself is a credential path, and
|
|
67
|
+
* the subtler one where a subtree of `searchRoot` contains it — a root-level
|
|
68
|
+
* search on a workdir laid over the host's home would read the credential
|
|
69
|
+
* directory's contents via matches.
|
|
70
|
+
*/
|
|
71
|
+
function credentialSearchRefusal(ctx, searchRoot, shown) {
|
|
72
|
+
const inside = credentialReadRefusal(ctx.protectedPaths, searchRoot);
|
|
73
|
+
if (inside !== undefined)
|
|
74
|
+
return `path "${shown}" ${inside}`;
|
|
75
|
+
if (!containsCredentialPath(ctx.protectedPaths, searchRoot))
|
|
76
|
+
return undefined;
|
|
77
|
+
return ("this search would reach the host's credential directory or trust store, which the read tools are refused; " +
|
|
78
|
+
"narrow the search to a subdirectory that does not contain it");
|
|
79
|
+
}
|
|
33
80
|
export function buildGrepArgv(binary, pattern, path, patternType = "literal") {
|
|
34
81
|
const target = path ?? ".";
|
|
35
82
|
// `--` terminates flag parsing on both branches, in both modes: a pattern
|
|
@@ -87,6 +134,11 @@ export const grepTool = {
|
|
|
87
134
|
return { content: "neither ripgrep nor grep is available on this machine", isError: true };
|
|
88
135
|
}
|
|
89
136
|
const [target] = ctx.resolvedPaths;
|
|
137
|
+
// S3 spec 6.3: the search must not reach the host's credential directory
|
|
138
|
+
// or trust store, whatever grant resolved the target.
|
|
139
|
+
const refusal = credentialSearchRefusal(ctx, target ?? ctx.root, typeof input.path === "string" ? input.path : ".");
|
|
140
|
+
if (refusal !== undefined)
|
|
141
|
+
return { content: refusal, isError: true };
|
|
90
142
|
// The I/O ceiling, not the model-facing cap: `maxBytes` shapes what the
|
|
91
143
|
// model is told and belongs to the session's truncation policy, while this
|
|
92
144
|
// bound only keeps a search whose result is enormous from being buffered
|
|
@@ -118,32 +170,9 @@ export const grepTool = {
|
|
|
118
170
|
const exitCode = await proc.exited;
|
|
119
171
|
clearTimeout(timer);
|
|
120
172
|
const stdout = await stdoutText;
|
|
121
|
-
// Two independent caveats
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
// 1. The literal disclosure fires whenever the search was literal AND the
|
|
125
|
-
// pattern contained a metacharacter a regex would have treated
|
|
126
|
-
// specially -- regardless of whether it matched. #1876 only covered the
|
|
127
|
-
// zero-match case; a literal match on a pattern like "foo.bar" is the
|
|
128
|
-
// WORSE case (#1922): the caller has positive evidence and no cue that
|
|
129
|
-
// "." was never a wildcard, so a real regex match was never attempted.
|
|
130
|
-
//
|
|
131
|
-
// 2. The dialect note fires when regex mode ran through the grep fallback.
|
|
132
|
-
// ripgrep's Rust regex and POSIX ERE are NOT the same language: `\d`,
|
|
133
|
-
// `\w` and `\b` are unsupported in ERE, and whether they degrade to a
|
|
134
|
-
// literal or to something else differs BY PLATFORM (GNU grep treats
|
|
135
|
-
// `\d` as `d`; the BSD grep on macOS matches a digit). Silently
|
|
136
|
-
// returning a different answer depending on which binary the machine
|
|
137
|
-
// happens to have is exactly the failure this file's header forbids of
|
|
138
|
-
// the fallback, so the divergence is disclosed rather than hidden.
|
|
139
|
-
const notes = [];
|
|
140
|
-
if (mode === "literal" && containsRegexMetacharacter(pattern)) {
|
|
141
|
-
notes.push("The search was performed literally and regex metacharacters were not interpreted.");
|
|
142
|
-
}
|
|
143
|
-
if (mode === "regex" && binary === "grep") {
|
|
144
|
-
notes.push("ripgrep is not installed here, so the pattern was matched with POSIX ERE via grep: escapes such as \\d, \\w and \\b are NOT supported — use [0-9], [A-Za-z0-9_] and similar instead.");
|
|
145
|
-
}
|
|
146
|
-
const caveat = notes.join(" ");
|
|
173
|
+
// Two independent caveats may apply -- the literal disclosure and the grep
|
|
174
|
+
// fallback's ERE dialect note. Their rules live in `searchCaveat` (above).
|
|
175
|
+
const caveat = searchCaveat(mode, binary, pattern);
|
|
147
176
|
// Both binaries exit 1 for "no matches" — a normal outcome, not a failure.
|
|
148
177
|
if (exitCode === 1 && stdout.trim() === "") {
|
|
149
178
|
const base = `no matches for "${pattern}"`;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owned-paths port (S3 spec 6.5, arc decision D16): which paths the HOST
|
|
3
|
+
* owns the writes to, and how its refusals read. nax-agent holds no such
|
|
4
|
+
* knowledge itself; nax injects its policy through `buildCodingToolSupport`
|
|
5
|
+
* and `resolveSessionSandbox`, and an embedder that injects nothing gets
|
|
6
|
+
* `EMPTY_OWNED_PATHS_POLICY`: containment and the `.git/` refusal still apply,
|
|
7
|
+
* owned-path refusals do not.
|
|
8
|
+
*/
|
|
9
|
+
/** One working-directory frame of a raw Bash token, in frame order. */
|
|
10
|
+
export interface OwnedBashCandidate {
|
|
11
|
+
/** The token resolved lexically against one working directory, symlinks resolved (`realOrRaw`). */
|
|
12
|
+
readonly lexical: string;
|
|
13
|
+
/** The typed-seam resolver's root-relative, `/`-joined path for that frame; null when it refused or the path is outside the root. */
|
|
14
|
+
readonly rel: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface OwnedPathsPolicy {
|
|
17
|
+
/** Refusal for a mutating path tool on a root-relative, `/`-joined path; undefined = not owned. */
|
|
18
|
+
writeRefusal(tool: string, rel: string, ctx: {
|
|
19
|
+
readonly exemptRel?: string;
|
|
20
|
+
readonly optIns: ReadonlySet<string>;
|
|
21
|
+
}): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Refusal for ANY tool, reads included, on a symlink-resolved absolute path;
|
|
24
|
+
* undefined = not refused. Non-undefined makes `resolveWithin` refuse the path,
|
|
25
|
+
* and the text completes `outOfRootReason`'s `path "x" <text>` sentence.
|
|
26
|
+
*/
|
|
27
|
+
configRefusal(root: string, resolved: string): string | undefined;
|
|
28
|
+
/** Raw Bash screen: full refusal for `token` given its per-frame candidates; undefined = allowed. */
|
|
29
|
+
bashRefusal(tool: string, token: string, candidates: readonly OwnedBashCandidate[], ctx: {
|
|
30
|
+
readonly root: string;
|
|
31
|
+
readonly verb: "names" | "redirects into";
|
|
32
|
+
readonly sandboxWrapped: boolean;
|
|
33
|
+
}): string | undefined;
|
|
34
|
+
/** Top-level project-state entries the sandbox denies writes to, even when absent. */
|
|
35
|
+
readonly deniedEntries: readonly string[];
|
|
36
|
+
/** Root-level file names the sandbox denies writes to. */
|
|
37
|
+
readonly rootWriteDenies: readonly string[];
|
|
38
|
+
/** The one project-state entry agents may always write; never sandbox-denied. */
|
|
39
|
+
readonly scratchpadEntry?: string;
|
|
40
|
+
/** Project-state entries opened for writes by `allowWrite`. */
|
|
41
|
+
writeOptIns(root: string, allowWrite: readonly string[]): ReadonlySet<string>;
|
|
42
|
+
}
|
|
43
|
+
export declare const EMPTY_OWNED_PATHS_POLICY: OwnedPathsPolicy;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owned-paths port (S3 spec 6.5, arc decision D16): which paths the HOST
|
|
3
|
+
* owns the writes to, and how its refusals read. nax-agent holds no such
|
|
4
|
+
* knowledge itself; nax injects its policy through `buildCodingToolSupport`
|
|
5
|
+
* and `resolveSessionSandbox`, and an embedder that injects nothing gets
|
|
6
|
+
* `EMPTY_OWNED_PATHS_POLICY`: containment and the `.git/` refusal still apply,
|
|
7
|
+
* owned-path refusals do not.
|
|
8
|
+
*/
|
|
9
|
+
const NO_OPT_INS = new Set();
|
|
10
|
+
export const EMPTY_OWNED_PATHS_POLICY = {
|
|
11
|
+
writeRefusal: () => undefined,
|
|
12
|
+
configRefusal: () => undefined,
|
|
13
|
+
bashRefusal: () => undefined,
|
|
14
|
+
deniedEntries: [],
|
|
15
|
+
rootWriteDenies: [],
|
|
16
|
+
writeOptIns: () => NO_OPT_INS,
|
|
17
|
+
};
|
|
@@ -66,12 +66,15 @@
|
|
|
66
66
|
* a widening `Exec(...)` grant by hand — defense-in-depth, not a live hole
|
|
67
67
|
* under the shipped default.
|
|
68
68
|
*/
|
|
69
|
+
import { type OwnedPathsPolicy } from "./owned-paths.ts";
|
|
69
70
|
import type { NormalizeResult, WorkspaceContext } from "./package-managers-types.ts";
|
|
70
71
|
export type { ExecTarget, NormalizeResult } from "./package-managers-types.ts";
|
|
71
72
|
import type { ExecTarget } from "./package-managers-types.ts";
|
|
72
73
|
export interface NormalizeInput extends WorkspaceContext {
|
|
73
74
|
readonly argv: readonly string[];
|
|
74
75
|
readonly target: ExecTarget;
|
|
76
|
+
/** Host-owned path rules (S3-2 port) handed to resolveWithin; absent: none (the empty policy). */
|
|
77
|
+
readonly ownedPaths?: OwnedPathsPolicy;
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
80
|
* Lowercases and strips a trailing executable suffix so `NPM`, `npm.cmd`
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
*/
|
|
69
69
|
import { resolve } from "node:path";
|
|
70
70
|
import { normalizeFlagToken } from "./exec-guard.js";
|
|
71
|
+
import { EMPTY_OWNED_PATHS_POLICY } from "./owned-paths.js";
|
|
71
72
|
import { MANAGER_TABLE } from "./package-managers-table.js";
|
|
72
73
|
import { resolveWithin } from "./policy.js";
|
|
73
74
|
/** Package runners that execute an arbitrary, model-named package outside
|
|
@@ -295,7 +296,10 @@ function isPathShapedToken(token, manager) {
|
|
|
295
296
|
* absolute, so it is `resolveWithin`'s own `isAbsolute` branch that runs the
|
|
296
297
|
* actual containment check.
|
|
297
298
|
*/
|
|
298
|
-
function findPositionalPathConflict(argv, entry, repoRoot, resolvedCwd) {
|
|
299
|
+
function findPositionalPathConflict(argv, entry, repoRoot, resolvedCwd, ownedPaths) {
|
|
300
|
+
// Defaulted HERE, not at the call site: normalizeExec sits at its complexity
|
|
301
|
+
// limit and one more `??` there would breach the ratchet.
|
|
302
|
+
const owned = ownedPaths ?? EMPTY_OWNED_PATHS_POLICY;
|
|
299
303
|
const manager = normalizeManagerBinary(argv[0]);
|
|
300
304
|
const verbStart = firstNonFlagIndexAfterBinary(argv);
|
|
301
305
|
if (verbStart === -1)
|
|
@@ -309,7 +313,7 @@ function findPositionalPathConflict(argv, entry, repoRoot, resolvedCwd) {
|
|
|
309
313
|
if (!isPathShapedToken(token, manager))
|
|
310
314
|
continue;
|
|
311
315
|
const absolute = token.startsWith("/") ? token : resolve(resolvedCwd, token);
|
|
312
|
-
if (resolveWithin(repoRoot, absolute) === null) {
|
|
316
|
+
if (resolveWithin(repoRoot, absolute, owned) === null) {
|
|
313
317
|
return `argv contains a path-shaped argument "${token}" that resolves outside the permitted root`;
|
|
314
318
|
}
|
|
315
319
|
}
|
|
@@ -381,7 +385,7 @@ export function normalizeExec(input) {
|
|
|
381
385
|
const base = effectiveTarget === "package" ? entry.packageForm(argv, input) : entry.rootForm(argv, input);
|
|
382
386
|
if ("error" in base)
|
|
383
387
|
return base;
|
|
384
|
-
const positionalPathConflict = findPositionalPathConflict(argv, entry, input.repoRoot, base.cwd);
|
|
388
|
+
const positionalPathConflict = findPositionalPathConflict(argv, entry, input.repoRoot, base.cwd, input.ownedPaths);
|
|
385
389
|
if (positionalPathConflict !== undefined) {
|
|
386
390
|
return { error: positionalPathConflict };
|
|
387
391
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OwnedPathsPolicy } from "./owned-paths.ts";
|
|
1
2
|
import type { BashCheck } from "./policy-bash.ts";
|
|
2
3
|
export interface RawScreenArgs {
|
|
3
4
|
readonly tool: string;
|
|
@@ -15,5 +16,7 @@ export interface RawScreenArgs {
|
|
|
15
16
|
* write. Absent or false screens exactly as before.
|
|
16
17
|
*/
|
|
17
18
|
readonly sandboxWrapped?: boolean;
|
|
19
|
+
/** S3-2: host-owned path rules; decides which tokens and redirects are refused and how. */
|
|
20
|
+
readonly ownedPaths: OwnedPathsPolicy;
|
|
18
21
|
}
|
|
19
22
|
export declare function screenRawBashCommand(args: RawScreenArgs): BashCheck;
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* rather than refused. That inversion is the whole point of the mode.
|
|
7
7
|
*
|
|
8
8
|
* The one thing this screen still does is catch a naive mistake: if the lexer
|
|
9
|
-
* CAN parse the command and a segment names or redirects into a path
|
|
10
|
-
* (
|
|
11
|
-
*
|
|
9
|
+
* CAN parse the command and a segment names or redirects into a path the
|
|
10
|
+
* host's owned-paths policy owns (for nax: `.nax/config.json`,
|
|
11
|
+
* `.nax/mono/*\/config.json`, `.nax/features/**\/prd.json`, the root
|
|
12
|
+
* queue-control files), the command is denied. Which paths those are, and how
|
|
13
|
+
* a refusal reads, is the `OwnedPathsPolicy` port's decision (S3-2); this
|
|
14
|
+
* screen only supplies the per-frame candidates.
|
|
12
15
|
*
|
|
13
16
|
* ADVISORY BY CONSTRUCTION. A command using substitution is not parsed and
|
|
14
17
|
* therefore is not screened at all: `sh -c "$(echo rm) .nax/features/f/prd.json"`
|
|
@@ -50,7 +53,6 @@ import { relative, resolve, sep } from "node:path";
|
|
|
50
53
|
import { realOrRaw } from "#src/internal/realpath";
|
|
51
54
|
import { lexBashCommand } from "#src/permissions/index";
|
|
52
55
|
import { cdTargetsFor, nextWorkingDirectories } from "./bash-cwd.js";
|
|
53
|
-
import { isNaxConfigFile, naxOwnedBashRefusal, naxOwnedKind } from "./nax-owned-writes.js";
|
|
54
56
|
/**
|
|
55
57
|
* US-002: the start paths that mean "search the whole filesystem". An exact
|
|
56
58
|
* token comparison, never a prefix one: `~/proj` and `~user` are scoped and
|
|
@@ -99,60 +101,40 @@ function deny(reason) {
|
|
|
99
101
|
return { kind: "deny", reason, breach: false, escalatable: false };
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
102
|
-
*
|
|
104
|
+
* One `OwnedBashCandidate` per live frame, in frame order.
|
|
103
105
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
+
* The two passes are the policy's now (`OwnedPathsPolicy.bashRefusal`); this
|
|
107
|
+
* function only supplies the evidence, one candidate per frame.
|
|
106
108
|
*
|
|
107
|
-
*
|
|
109
|
+
* Built against EVERY frame in `cwd`, mirroring the conservatism of gated
|
|
108
110
|
* mode's own `resolveAll`: a `;`-joined `cd` can leave more than one frame
|
|
109
111
|
* live at once (see `nextWorkingDirectories`), and a candidate that is safe
|
|
110
|
-
* from one frame but hits
|
|
112
|
+
* from one frame but hits an owned path from another must still be seen.
|
|
111
113
|
*
|
|
112
|
-
*
|
|
114
|
+
* Per frame, the candidate carries both halves of today's two-pass order:
|
|
113
115
|
*
|
|
114
|
-
* 1.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* resolver is consulted.
|
|
116
|
+
* 1. `lexical`: the candidate resolved lexically against the frame --
|
|
117
|
+
* `realOrRaw` walks to the nearest existing ancestor, so a not-yet-created
|
|
118
|
+
* file under a symlinked temp root still compares equal to
|
|
119
|
+
* `realOrRaw(root)`. The policy's lexical config check runs
|
|
120
|
+
* on this, BEFORE the resolver is consulted -- the raw screen has no typed
|
|
121
|
+
* seam in front of it, and `resolvePath` (production: `resolveWithin`)
|
|
122
|
+
* returns null for `.nax/config.json` exactly because typed tools are
|
|
123
|
+
* SUPPOSED to refuse those writes.
|
|
123
124
|
*
|
|
124
|
-
* 2.
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* and feature PRD set.
|
|
128
|
-
*
|
|
129
|
-
* The two checks do not overlap: pass 1 covers nax config files; pass 2 covers
|
|
130
|
-
* feature PRDs and the queue run-control files. `isNaxConfigFile` is checked
|
|
131
|
-
* lexically here because the resolver cannot return it, and lexically in
|
|
132
|
-
* `resolveWithin` for the same reason; `isNaxOwnedWritePath` is unchanged.
|
|
125
|
+
* 2. `rel`: the typed-seam resolver's root-relative spelling for the same
|
|
126
|
+
* frame, null when it refused or the path is outside the root. The
|
|
127
|
+
* policy's PRD/queue check runs on this.
|
|
133
128
|
*/
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
// Pass 1: lexical nax-config check. Independent of the resolver on
|
|
137
|
-
// purpose -- see the comment above. `realOrRaw` walks to the nearest
|
|
138
|
-
// existing ancestor so the comparison holds even when the file does not
|
|
139
|
-
// yet exist on disk (which is the common case: the screen catches the
|
|
140
|
-
// write BEFORE the file lands).
|
|
141
|
-
const lexical = realOrRaw(resolve(directory, candidate));
|
|
142
|
-
if (isNaxConfigFile(args.root, lexical))
|
|
143
|
-
return { kind: "config", hit: candidate };
|
|
144
|
-
// Pass 2: typed-seam resolver (unchanged).
|
|
129
|
+
function ownedCandidates(args, candidate, cwd) {
|
|
130
|
+
return cwd.map((directory) => {
|
|
145
131
|
const resolved = args.resolvePath(candidate, directory);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (kind !== undefined)
|
|
153
|
-
return { kind, hit: candidate };
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
132
|
+
const rel = resolved === null ? null : relative(args.root, resolved).split(sep).join("/");
|
|
133
|
+
return {
|
|
134
|
+
lexical: realOrRaw(resolve(directory, candidate)),
|
|
135
|
+
rel: rel === null || rel.startsWith("..") ? null : rel,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
156
138
|
}
|
|
157
139
|
export function screenRawBashCommand(args) {
|
|
158
140
|
const { command, tool } = args;
|
|
@@ -175,26 +157,31 @@ export function screenRawBashCommand(args) {
|
|
|
175
157
|
return deny(`\`find ${findStart}\` searches the whole filesystem and runs into the 300s Bash timeout. ` +
|
|
176
158
|
`Search within the repository root instead: ${args.root}`);
|
|
177
159
|
}
|
|
160
|
+
// US-002: sandbox-wrapped, a token that only NAMES a PRD is allowed -- the
|
|
161
|
+
// policy now applies that exemption (`bashRefusal` receives
|
|
162
|
+
// `sandboxWrapped` and skips the PRD read itself). A redirect WRITES, so
|
|
163
|
+
// the policy refuses it even sandbox-wrapped, with the read/write truth.
|
|
178
164
|
for (const token of segment.tokens) {
|
|
179
165
|
if (token.opaque)
|
|
180
166
|
continue;
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
167
|
+
const reason = args.ownedPaths.bashRefusal(tool, token.text, ownedCandidates(args, token.text, cwd), {
|
|
168
|
+
root: args.root,
|
|
169
|
+
verb: "names",
|
|
170
|
+
sandboxWrapped,
|
|
171
|
+
});
|
|
172
|
+
if (reason !== undefined)
|
|
173
|
+
return deny(reason);
|
|
188
174
|
}
|
|
189
175
|
for (const redirect of segment.redirects) {
|
|
190
176
|
if (redirect.opaque)
|
|
191
177
|
continue;
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
178
|
+
const reason = args.ownedPaths.bashRefusal(tool, redirect.target, ownedCandidates(args, redirect.target, cwd), {
|
|
179
|
+
root: args.root,
|
|
180
|
+
verb: "redirects into",
|
|
181
|
+
sandboxWrapped,
|
|
182
|
+
});
|
|
183
|
+
if (reason !== undefined)
|
|
184
|
+
return deny(reason);
|
|
198
185
|
}
|
|
199
186
|
// See the file header ("THE CRITICAL ASYMMETRY"). An unmodelled `cd` is
|
|
200
187
|
// never itself a denial -- that inversion from gated mode is deliberate --
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* reaching `resolveWithin` back would close a cycle.
|
|
11
11
|
*/
|
|
12
12
|
import type { BashApprovalMode } from "#src/config/bash-approval";
|
|
13
|
+
import type { OwnedPathsPolicy } from "./owned-paths.ts";
|
|
13
14
|
import type { CompiledEntry } from "./policy-match.ts";
|
|
14
15
|
import type { PolicyVerdict, ToolScope } from "./types.ts";
|
|
15
16
|
export interface BashCommandBranchArgs {
|
|
@@ -22,6 +23,8 @@ export interface BashCommandBranchArgs {
|
|
|
22
23
|
readonly rawBashRefusal?: string;
|
|
23
24
|
/** US-002: true when the sandbox wraps the command, so `raw` no longer refuses a PRD it only reads. */
|
|
24
25
|
readonly sandboxWrapped?: boolean;
|
|
26
|
+
/** S3-2: host-owned path rules; the `raw` screen hands it one candidate per live frame. */
|
|
27
|
+
readonly ownedPaths: OwnedPathsPolicy;
|
|
25
28
|
readonly resolvedRoot: string;
|
|
26
29
|
readonly denyBy: ReadonlyMap<string, CompiledEntry>;
|
|
27
30
|
readonly askBy: ReadonlyMap<string, CompiledEntry>;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { checkBashCommand } from "./policy-bash.js";
|
|
13
13
|
import { screenRawBashCommand } from "./policy-bash-raw.js";
|
|
14
14
|
export function commandBranch(args) {
|
|
15
|
-
const { tool, scope, input, grant, bashApproval, rawBashRefusal, sandboxWrapped, resolvedRoot, denyBy, askBy, resolvePath, deny, askVerdict, } = args;
|
|
15
|
+
const { tool, scope, input, grant, bashApproval, rawBashRefusal, sandboxWrapped, ownedPaths, resolvedRoot, denyBy, askBy, resolvePath, deny, askVerdict, } = args;
|
|
16
16
|
if (scope.commandField === undefined)
|
|
17
17
|
return undefined;
|
|
18
18
|
if (bashApproval === "raw") {
|
|
@@ -25,6 +25,7 @@ export function commandBranch(args) {
|
|
|
25
25
|
root: resolvedRoot,
|
|
26
26
|
sandboxWrapped: sandboxWrapped === true,
|
|
27
27
|
resolvePath,
|
|
28
|
+
ownedPaths,
|
|
28
29
|
});
|
|
29
30
|
if (screened.kind === "deny")
|
|
30
31
|
return deny(screened.reason, screened.breach, screened.escalatable);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* from it (the collaborators it shares — deny, askVerdict, applyPathRules —
|
|
22
22
|
* arrive by reference in `PathsBranchContext`, read at call time).
|
|
23
23
|
*/
|
|
24
|
+
import type { OwnedPathsPolicy } from "./owned-paths.ts";
|
|
24
25
|
import { type CompiledEntry } from "./policy-match.ts";
|
|
25
26
|
import type { PolicyVerdict, ToolScope } from "./types.ts";
|
|
26
27
|
/** Mutable scratch shared by `check()`'s branch helpers: first ask rule matched. */
|
|
@@ -38,11 +39,14 @@ export interface PathsBranchContext {
|
|
|
38
39
|
readonly deny: (reason: string, breach?: boolean, escalatable?: boolean) => PolicyVerdict;
|
|
39
40
|
readonly askVerdict: (resolvedPaths: readonly string[], rule: string) => PolicyVerdict;
|
|
40
41
|
readonly applyPathRules: (tool: string, rel: string, state: RuleState) => PolicyVerdict | undefined;
|
|
42
|
+
/** The injected OwnedPathsPolicy (S3-2 port); the owned-config refusal reads through it. */
|
|
43
|
+
readonly ownedPaths: OwnedPathsPolicy;
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
43
46
|
* Absolute, symlink-resolved form of `candidate` if it lies inside `root`,
|
|
44
|
-
* is not itself (and does not lie under) `.git/`, and is not
|
|
45
|
-
*
|
|
47
|
+
* is not itself (and does not lie under) `.git/`, and is not refused by the
|
|
48
|
+
* injected OwnedPathsPolicy's `configRefusal` (S3-2 port) -- for nax, one of
|
|
49
|
+
* nax's own config files.
|
|
46
50
|
*
|
|
47
51
|
* The single containment seam. Multi-root support (a future configurable
|
|
48
52
|
* extension) changes this function and nothing else, which is why every tool
|
|
@@ -54,7 +58,9 @@ export interface PathsBranchContext {
|
|
|
54
58
|
* so containment alone never bars it, and an unconditional ("*") grant --
|
|
55
59
|
* what every non-Exec tool gets under the default `unrestricted` profile --
|
|
56
60
|
* skips glob matching entirely, so nothing downstream of this function would
|
|
57
|
-
* catch it either.
|
|
61
|
+
* catch it either. The owned-config refusal arrives through the port: nax
|
|
62
|
+
* injects `naxOwnedPathsPolicy`, an embedder that injects nothing gets
|
|
63
|
+
* `EMPTY_OWNED_PATHS_POLICY` and only containment plus `.git/` remain.
|
|
58
64
|
*
|
|
59
65
|
* Pre-single-frame, this seam carried one exception: an `execTouchedPaths` set
|
|
60
66
|
* (Task 10) admitted a workspace install's repo-ROOT manifest/lockfile even
|
|
@@ -63,7 +69,7 @@ export interface PathsBranchContext {
|
|
|
63
69
|
* manifest is in-root by construction and that carve-out was retired
|
|
64
70
|
* (PR2/Task 13). There is no exception to this seam now.
|
|
65
71
|
*/
|
|
66
|
-
export declare function resolveWithin(root: string, candidate: string): string | null;
|
|
72
|
+
export declare function resolveWithin(root: string, candidate: string, owned: OwnedPathsPolicy): string | null;
|
|
67
73
|
/**
|
|
68
74
|
* Containment runs before any pattern matching and wins over everything.
|
|
69
75
|
* Each resolved path is then evaluated deny -> ask -> allow. A verb-only
|
|
@@ -74,14 +80,14 @@ export declare function resolveWithin(root: string, candidate: string): string |
|
|
|
74
80
|
* `confineTo` (tool-declared, see `ToolScope`) shifts the root passed to
|
|
75
81
|
* `resolveWithin` from `<root>` to `<root>/<confineTo>`: containment stays
|
|
76
82
|
* the one seam, and only its ROOT changes. `relativeTo` keeps rooting at
|
|
77
|
-
* `resolvedRoot` so grant globs, deny rules and
|
|
78
|
-
* continue to see the canonical repo-root-relative spelling -- authors
|
|
83
|
+
* `resolvedRoot` so grant globs, deny rules and the injected policy's
|
|
84
|
+
* `writeRefusal` continue to see the canonical repo-root-relative spelling -- authors
|
|
79
85
|
* write `.nax/scratchpad/**`, never `**`, regardless of `confineTo`.
|
|
80
86
|
*
|
|
81
87
|
* `confineTo` is bound to stay INSIDE `resolvedRoot`: an authoring typo of
|
|
82
88
|
* `..` or `../shared` would otherwise widen the containment root past the
|
|
83
|
-
* policy boundary and re-scope `resolveWithin`'s `.git/`-metadata and
|
|
84
|
-
* `
|
|
89
|
+
* policy boundary and re-scope `resolveWithin`'s `.git/`-metadata and the
|
|
90
|
+
* port's `configRefusal` protections to a root that no longer aligns with the
|
|
85
91
|
* segments those checks assume -- the repo's own `.nax/config.json` would
|
|
86
92
|
stop being segment-matched against `.nax`. The boundary is the policy
|
|
87
93
|
root's invariant, so an out-of-root confineTo refuses the call outright
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
25
25
|
import { isInside, realOrRaw } from "#src/internal/realpath";
|
|
26
|
-
import { isNaxConfigFile } from "./nax-owned-writes.js";
|
|
27
26
|
import { pathListElements } from "./path-list.js";
|
|
28
27
|
import { pathFieldValue } from "./policy-input.js";
|
|
29
28
|
import { matchesAny } from "./policy-match.js";
|
|
@@ -51,8 +50,9 @@ function entersGitMetadata(root, resolved) {
|
|
|
51
50
|
}
|
|
52
51
|
/**
|
|
53
52
|
* Absolute, symlink-resolved form of `candidate` if it lies inside `root`,
|
|
54
|
-
* is not itself (and does not lie under) `.git/`, and is not
|
|
55
|
-
*
|
|
53
|
+
* is not itself (and does not lie under) `.git/`, and is not refused by the
|
|
54
|
+
* injected OwnedPathsPolicy's `configRefusal` (S3-2 port) -- for nax, one of
|
|
55
|
+
* nax's own config files.
|
|
56
56
|
*
|
|
57
57
|
* The single containment seam. Multi-root support (a future configurable
|
|
58
58
|
* extension) changes this function and nothing else, which is why every tool
|
|
@@ -64,7 +64,9 @@ function entersGitMetadata(root, resolved) {
|
|
|
64
64
|
* so containment alone never bars it, and an unconditional ("*") grant --
|
|
65
65
|
* what every non-Exec tool gets under the default `unrestricted` profile --
|
|
66
66
|
* skips glob matching entirely, so nothing downstream of this function would
|
|
67
|
-
* catch it either.
|
|
67
|
+
* catch it either. The owned-config refusal arrives through the port: nax
|
|
68
|
+
* injects `naxOwnedPathsPolicy`, an embedder that injects nothing gets
|
|
69
|
+
* `EMPTY_OWNED_PATHS_POLICY` and only containment plus `.git/` remain.
|
|
68
70
|
*
|
|
69
71
|
* Pre-single-frame, this seam carried one exception: an `execTouchedPaths` set
|
|
70
72
|
* (Task 10) admitted a workspace install's repo-ROOT manifest/lockfile even
|
|
@@ -73,11 +75,11 @@ function entersGitMetadata(root, resolved) {
|
|
|
73
75
|
* manifest is in-root by construction and that carve-out was retired
|
|
74
76
|
* (PR2/Task 13). There is no exception to this seam now.
|
|
75
77
|
*/
|
|
76
|
-
export function resolveWithin(root, candidate) {
|
|
78
|
+
export function resolveWithin(root, candidate, owned) {
|
|
77
79
|
const absolute = isAbsolute(candidate) ? candidate : resolve(root, candidate);
|
|
78
80
|
if (isInside(root, absolute)) {
|
|
79
81
|
const resolved = realOrRaw(absolute);
|
|
80
|
-
if (entersGitMetadata(root, resolved) ||
|
|
82
|
+
if (entersGitMetadata(root, resolved) || owned.configRefusal(root, resolved) !== undefined)
|
|
81
83
|
return null;
|
|
82
84
|
return resolved;
|
|
83
85
|
}
|
|
@@ -118,14 +120,13 @@ export function resolveWithin(root, candidate) {
|
|
|
118
120
|
* message is where the agent first sees the absolute containment root -- and
|
|
119
121
|
* naming a path the model itself handed in is the right disclosure.
|
|
120
122
|
*/
|
|
121
|
-
function outOfRootReason(root, candidate) {
|
|
123
|
+
function outOfRootReason(root, candidate, owned) {
|
|
122
124
|
const absolute = isAbsolute(candidate) ? candidate : resolve(root, candidate);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
125
|
+
// The owned-config refusal (for nax: nax's own config files) comes from the
|
|
126
|
+
// injected port (S3-2), which supplies the full reason text.
|
|
127
|
+
const configReason = isInside(root, absolute) ? owned.configRefusal(root, realOrRaw(absolute)) : undefined;
|
|
128
|
+
if (configReason !== undefined)
|
|
129
|
+
return configReason;
|
|
129
130
|
if (isInside(root, absolute) && entersGitMetadata(root, realOrRaw(absolute))) {
|
|
130
131
|
return ("targets git metadata under .git/, which every tool is refused regardless of grant -- " +
|
|
131
132
|
"writing there can corrupt the repository beyond git's own recovery, and reading its " +
|
|
@@ -168,9 +169,9 @@ function stripConfinePrefix(confinePrefix, value) {
|
|
|
168
169
|
* fields, `restrictPaths` for array/ref fields — see the module comment).
|
|
169
170
|
*/
|
|
170
171
|
function checkPathSegment(frame, candidate, enforceGlobs) {
|
|
171
|
-
const resolved = resolveWithin(frame.effectiveRoot, stripConfinePrefix(frame.confinePrefix, candidate.path));
|
|
172
|
+
const resolved = resolveWithin(frame.effectiveRoot, stripConfinePrefix(frame.confinePrefix, candidate.path), frame.ctx.ownedPaths);
|
|
172
173
|
if (resolved === null) {
|
|
173
|
-
return frame.ctx.deny(`${candidate.denialSubject} ${outOfRootReason(frame.effectiveRoot, candidate.outOfRootTarget)}`, true);
|
|
174
|
+
return frame.ctx.deny(`${candidate.denialSubject} ${outOfRootReason(frame.effectiveRoot, candidate.outOfRootTarget, frame.ctx.ownedPaths)}`, true);
|
|
174
175
|
}
|
|
175
176
|
const rel = repoRelative(frame.ctx.resolvedRoot, resolved);
|
|
176
177
|
const ruleDenial = frame.ctx.applyPathRules(frame.tool, rel, frame.state);
|
|
@@ -269,14 +270,14 @@ function runRefPathFields(frame) {
|
|
|
269
270
|
* `confineTo` (tool-declared, see `ToolScope`) shifts the root passed to
|
|
270
271
|
* `resolveWithin` from `<root>` to `<root>/<confineTo>`: containment stays
|
|
271
272
|
* the one seam, and only its ROOT changes. `relativeTo` keeps rooting at
|
|
272
|
-
* `resolvedRoot` so grant globs, deny rules and
|
|
273
|
-
* continue to see the canonical repo-root-relative spelling -- authors
|
|
273
|
+
* `resolvedRoot` so grant globs, deny rules and the injected policy's
|
|
274
|
+
* `writeRefusal` continue to see the canonical repo-root-relative spelling -- authors
|
|
274
275
|
* write `.nax/scratchpad/**`, never `**`, regardless of `confineTo`.
|
|
275
276
|
*
|
|
276
277
|
* `confineTo` is bound to stay INSIDE `resolvedRoot`: an authoring typo of
|
|
277
278
|
* `..` or `../shared` would otherwise widen the containment root past the
|
|
278
|
-
* policy boundary and re-scope `resolveWithin`'s `.git/`-metadata and
|
|
279
|
-
* `
|
|
279
|
+
* policy boundary and re-scope `resolveWithin`'s `.git/`-metadata and the
|
|
280
|
+
* port's `configRefusal` protections to a root that no longer aligns with the
|
|
280
281
|
* segments those checks assume -- the repo's own `.nax/config.json` would
|
|
281
282
|
stop being segment-matched against `.nax`. The boundary is the policy
|
|
282
283
|
root's invariant, so an out-of-root confineTo refuses the call outright
|
package/dist/tools/policy.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* exception to the boundary is needed.
|
|
15
15
|
*/
|
|
16
16
|
import type { BashApprovalMode } from "#src/config/bash-approval";
|
|
17
|
+
import { type OwnedPathsPolicy } from "./owned-paths.ts";
|
|
17
18
|
import { resolveWithin } from "./policy-paths-branch.ts";
|
|
18
19
|
import { type ToolGrant, type ToolPolicy } from "./types.ts";
|
|
19
20
|
/** The single containment seam now lives beside the path branch that drives it. */
|
|
@@ -32,16 +33,18 @@ export interface ToolPolicyOptions {
|
|
|
32
33
|
*/
|
|
33
34
|
readonly askRules?: readonly ToolGrant[];
|
|
34
35
|
/**
|
|
35
|
-
* The ONE nax-owned path this session may write despite
|
|
36
|
-
* (nax#2115): the ABSOLUTE `fileOutput` path the dispatching op
|
|
37
|
-
* canonicalised into this policy's own root-relative frame below
|
|
38
|
-
* the caller, so alternate spellings of the same file cannot
|
|
39
|
-
* string the guard compares. Only the feature-PRD refusal
|
|
40
|
-
* CONFIG refusal is deliberately not exempted.
|
|
36
|
+
* The ONE nax-owned path this session may write despite the port's
|
|
37
|
+
* `writeRefusal` (nax#2115): the ABSOLUTE `fileOutput` path the dispatching op
|
|
38
|
+
* declared. It is canonicalised into this policy's own root-relative frame below
|
|
39
|
+
* rather than by the caller, so alternate spellings of the same file cannot
|
|
40
|
+
* diverge from the string the guard compares. Only the feature-PRD refusal
|
|
41
|
+
* honours it; the owned CONFIG refusal is deliberately not exempted.
|
|
41
42
|
*/
|
|
42
43
|
readonly ownedWriteExemption?: string;
|
|
43
|
-
/** nax#2260: `execution.sandbox.filesystem.allowWrite`; a listed top-level `.nax/` entry becomes writable
|
|
44
|
+
/** nax#2260: `execution.sandbox.filesystem.allowWrite`; a listed top-level `.nax/` entry becomes writable through the port's `writeOptIns`. */
|
|
44
45
|
readonly naxAllowWrite?: readonly string[];
|
|
46
|
+
/** S3-2: host-owned path rules (OwnedPathsPolicy port). Absent: none, the embedder default. */
|
|
47
|
+
readonly ownedPaths?: OwnedPathsPolicy;
|
|
45
48
|
/**
|
|
46
49
|
* How a bash command string is adjudicated (ADR-030). Absent means `gated` —
|
|
47
50
|
* today's behaviour — so every caller that does not opt in is unchanged.
|