@nathapp/nax-agent 0.1.0 → 0.3.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 +43 -0
- package/README.md +56 -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 +21 -3
- package/dist/index.js +13 -3
- package/dist/infra/stderr-tail.d.ts +8 -0
- package/dist/infra/stderr-tail.js +48 -0
- 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 +54 -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/permissions/ask-chain.d.ts +1 -1
- package/dist/permissions/ask.d.ts +6 -0
- package/dist/permissions/ask.js +6 -0
- package/dist/permissions/index.d.ts +1 -1
- package/dist/permissions/index.js +1 -1
- 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 +12 -0
- package/dist/session/agent-session-options.js +96 -0
- package/dist/session/agent-session-resume.d.ts +14 -0
- package/dist/session/agent-session-resume.js +49 -0
- package/dist/session/agent-session-turn.d.ts +41 -0
- package/dist/session/agent-session-turn.js +177 -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 +184 -0
- package/dist/session/native-backend-options.d.ts +24 -0
- package/dist/session/native-backend-options.js +81 -0
- package/dist/session/native-backend.d.ts +4 -0
- package/dist/session/native-backend.js +106 -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 +33 -0
- package/dist/session/session-ask-link.js +53 -0
- package/dist/session/session-ask-port.d.ts +12 -0
- package/dist/session/session-ask-port.js +64 -0
- package/dist/session/session-backend.d.ts +79 -0
- package/dist/session/session-backend.js +1 -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 +161 -0
- package/dist/session/session-tool-support.d.ts +48 -0
- package/dist/session/session-tool-support.js +104 -0
- package/dist/session/session-types.d.ts +33 -2
- package/dist/session/turn-event.d.ts +66 -0
- package/dist/session/turn-event.js +1 -0
- package/dist/tools/ask-request.js +3 -1
- 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
|
@@ -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.
|
package/dist/tools/policy.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { relative, resolve, sep } from "node:path";
|
|
17
17
|
import { realOrRaw } from "#src/internal/realpath";
|
|
18
18
|
import { validateArgv } from "./exec-guard.js";
|
|
19
|
-
import {
|
|
19
|
+
import { EMPTY_OWNED_PATHS_POLICY } from "./owned-paths.js";
|
|
20
20
|
import { commandBranch } from "./policy-command-branch.js";
|
|
21
21
|
import { compileArgvPattern, compileRuleMap, globToRegExp, matchedArgvSource, matchedGlobSource, matchedRulePatterns, matchesAny, matchesArgvGrant, } from "./policy-match.js";
|
|
22
22
|
import { pathsBranch, resolveWithin } from "./policy-paths-branch.js";
|
|
@@ -35,13 +35,14 @@ function isFieldlessScope(scope) {
|
|
|
35
35
|
}
|
|
36
36
|
export function compileToolPolicy(grants, root, options) {
|
|
37
37
|
const resolvedRoot = realOrRaw(root);
|
|
38
|
+
const owned = options?.ownedPaths ?? EMPTY_OWNED_PATHS_POLICY;
|
|
38
39
|
// nax#2115: the SAME transform `relativeTo` applies to every checked path, so
|
|
39
40
|
// the guard compares like with like. A path outside the root yields a
|
|
40
41
|
// ".."-prefixed rel that can never equal a checked path's, exempting nothing.
|
|
41
42
|
const ownedWriteExemption = options?.ownedWriteExemption === undefined
|
|
42
43
|
? undefined
|
|
43
44
|
: relative(resolvedRoot, realOrRaw(options.ownedWriteExemption)).split(sep).join("/");
|
|
44
|
-
const naxOptIns =
|
|
45
|
+
const naxOptIns = owned.writeOptIns(root, options?.naxAllowWrite ?? []);
|
|
45
46
|
const denyBy = compileRuleMap(options?.denyRules);
|
|
46
47
|
const askBy = compileRuleMap(options?.askRules);
|
|
47
48
|
const bashApproval = options?.bashApproval ?? "gated";
|
|
@@ -87,9 +88,9 @@ export function compileToolPolicy(grants, root, options) {
|
|
|
87
88
|
* denied and an ask on an ungranted call never becomes an approval prompt.
|
|
88
89
|
*/
|
|
89
90
|
function applyPathRules(tool, rel, state) {
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
return deny(`${tool} may not modify ${
|
|
91
|
+
const ownedRefusal = owned.writeRefusal(tool, rel, { exemptRel: ownedWriteExemption, optIns: naxOptIns });
|
|
92
|
+
if (ownedRefusal !== undefined)
|
|
93
|
+
return deny(`${tool} may not modify ${ownedRefusal}`);
|
|
93
94
|
const denyEntry = denyBy.get(tool);
|
|
94
95
|
const askEntry = askBy.get(tool);
|
|
95
96
|
if (denyEntry !== undefined && (denyEntry.unconditional || matchesAny(denyEntry.matchers, rel))) {
|
|
@@ -187,7 +188,7 @@ export function compileToolPolicy(grants, root, options) {
|
|
|
187
188
|
* the nested helpers below stay authoritative, and the sibling reads them at
|
|
188
189
|
* call time — `pathsBranch` itself lives in `./policy-paths-branch`.
|
|
189
190
|
*/
|
|
190
|
-
const pathsBranchContext = { resolvedRoot, deny, askVerdict, applyPathRules };
|
|
191
|
+
const pathsBranchContext = { resolvedRoot, deny, askVerdict, applyPathRules, ownedPaths: owned };
|
|
191
192
|
return {
|
|
192
193
|
root: resolvedRoot,
|
|
193
194
|
grantedTools() {
|
|
@@ -217,10 +218,11 @@ export function compileToolPolicy(grants, root, options) {
|
|
|
217
218
|
bashApproval,
|
|
218
219
|
rawBashRefusal: options?.rawBashRefusal,
|
|
219
220
|
sandboxWrapped: options?.sandboxWrapped,
|
|
221
|
+
ownedPaths: owned,
|
|
220
222
|
resolvedRoot,
|
|
221
223
|
denyBy,
|
|
222
224
|
askBy,
|
|
223
|
-
resolvePath: (candidate, cwd) => resolveWithin(resolvedRoot, resolve(cwd, candidate)),
|
|
225
|
+
resolvePath: (candidate, cwd) => resolveWithin(resolvedRoot, resolve(cwd, candidate), owned),
|
|
224
226
|
deny,
|
|
225
227
|
askVerdict,
|
|
226
228
|
}) ??
|
|
@@ -9,12 +9,12 @@ export interface ProtectedPathsPolicy {
|
|
|
9
9
|
readonly gitExcludePathspecs: readonly string[];
|
|
10
10
|
/** Gitignore patterns GitCommit refuses to stage. */
|
|
11
11
|
readonly gitIgnorePatterns: readonly string[];
|
|
12
|
-
/** Project-relative directory whose top-level entries the sandbox policy lists. */
|
|
13
|
-
readonly projectStateDir
|
|
14
|
-
/** Directory whose `credentials*` files the sandbox denies. */
|
|
15
|
-
readonly credentialDir
|
|
16
|
-
/** File the sandbox denies writes to: the trust store deciding whether repository code runs. */
|
|
17
|
-
readonly trustStoreFile
|
|
12
|
+
/** Project-relative directory whose top-level entries the sandbox policy lists. Absent: the sandbox skips it (S3 spec 6.2). */
|
|
13
|
+
readonly projectStateDir?: string;
|
|
14
|
+
/** Directory whose `credentials*` files the sandbox denies. Absent: the sandbox skips it (S3 spec 6.2). */
|
|
15
|
+
readonly credentialDir?: string;
|
|
16
|
+
/** File the sandbox denies writes to: the trust store deciding whether repository code runs. Absent: the sandbox skips it (S3 spec 6.2). */
|
|
17
|
+
readonly trustStoreFile?: string;
|
|
18
18
|
}
|
|
19
19
|
/** The Git tool's default excludes; none when the session carries no policy. */
|
|
20
20
|
export declare function gitExcludePathspecsOf(ctx: {
|
package/dist/tools/read.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* both.
|
|
23
23
|
*/
|
|
24
24
|
import { readPrefix } from "#src/internal/bounded-io";
|
|
25
|
+
import { credentialReadRefusal } from "./credential-read-deny.js";
|
|
25
26
|
import { applyCapCut, limitStopFooter, shouldAppendLimitStopFooter } from "./read-continuation.js";
|
|
26
27
|
import { MODEL_MAX_BYTES, MODEL_MAX_LINES, READ_CEILING, splitModelLines } from "./truncate.js";
|
|
27
28
|
/** Range arguments models invent instead of offset/limit -- rejected by name, never silently dropped. */
|
|
@@ -41,6 +42,34 @@ function countLines(prefix) {
|
|
|
41
42
|
const trimmed = prefix.endsWith("\n") ? prefix.slice(0, -1) : prefix;
|
|
42
43
|
return trimmed.split("\n").length;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The range half of Read's input: the unsupported-alias rejection (#1923) plus
|
|
47
|
+
* offset/limit parsing. Extracted from `run` so the credential read-deny adds
|
|
48
|
+
* no complexity there.
|
|
49
|
+
*/
|
|
50
|
+
function parseRange(input) {
|
|
51
|
+
const usedAlias = UNSUPPORTED_RANGE_ALIASES.find((alias) => input[alias] !== undefined);
|
|
52
|
+
if (usedAlias !== undefined) {
|
|
53
|
+
return {
|
|
54
|
+
error: `"${usedAlias}" is not a supported Read argument -- use offset (1-based line number) and/or limit (line count) instead`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
let offset = 1;
|
|
58
|
+
if (input.offset !== undefined) {
|
|
59
|
+
const parsed = parsePositiveInt(input.offset, "offset");
|
|
60
|
+
if (typeof parsed === "string")
|
|
61
|
+
return { error: parsed };
|
|
62
|
+
offset = parsed;
|
|
63
|
+
}
|
|
64
|
+
let limit;
|
|
65
|
+
if (input.limit !== undefined) {
|
|
66
|
+
const parsed = parsePositiveInt(input.limit, "limit");
|
|
67
|
+
if (typeof parsed === "string")
|
|
68
|
+
return { error: parsed };
|
|
69
|
+
limit = parsed;
|
|
70
|
+
}
|
|
71
|
+
return { offset, limit };
|
|
72
|
+
}
|
|
44
73
|
export const readTool = {
|
|
45
74
|
name: "Read",
|
|
46
75
|
description: "Read a UTF-8 text file from the repository. Paths are relative to the repository root. " +
|
|
@@ -64,15 +93,18 @@ export const readTool = {
|
|
|
64
93
|
const [target] = ctx.resolvedPaths;
|
|
65
94
|
if (target === undefined)
|
|
66
95
|
return { content: "no path supplied", isError: true };
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
96
|
+
// S3 spec 6.3: a workdir may resolve onto the host's credential directory
|
|
97
|
+
// or trust store; the read tools refuse those regardless of grant.
|
|
98
|
+
const credential = credentialReadRefusal(ctx.protectedPaths, target);
|
|
99
|
+
if (credential !== undefined) {
|
|
100
|
+
return { content: `path "${String(input.path)}" ${credential}`, isError: true };
|
|
73
101
|
}
|
|
74
102
|
const hasOffset = input.offset !== undefined;
|
|
75
103
|
const hasLimit = input.limit !== undefined;
|
|
104
|
+
const range = parseRange(input);
|
|
105
|
+
if ("error" in range)
|
|
106
|
+
return { content: range.error, isError: true };
|
|
107
|
+
const { offset, limit } = range;
|
|
76
108
|
try {
|
|
77
109
|
if (!hasOffset && !hasLimit) {
|
|
78
110
|
// Leading [N lines] header so an agent has a way to count the lines it
|
|
@@ -106,20 +138,6 @@ export const readTool = {
|
|
|
106
138
|
});
|
|
107
139
|
return { content: result.content };
|
|
108
140
|
}
|
|
109
|
-
let offset = 1;
|
|
110
|
-
if (hasOffset) {
|
|
111
|
-
const parsed = parsePositiveInt(input.offset, "offset");
|
|
112
|
-
if (typeof parsed === "string")
|
|
113
|
-
return { content: parsed, isError: true };
|
|
114
|
-
offset = parsed;
|
|
115
|
-
}
|
|
116
|
-
let limit;
|
|
117
|
-
if (hasLimit) {
|
|
118
|
-
const parsed = parsePositiveInt(input.limit, "limit");
|
|
119
|
-
if (typeof parsed === "string")
|
|
120
|
-
return { content: parsed, isError: true };
|
|
121
|
-
limit = parsed;
|
|
122
|
-
}
|
|
123
141
|
// Read up to maxFileBytes, not maxBytes: the requested range may start
|
|
124
142
|
// past the display ceiling, and we still need to reach it to slice.
|
|
125
143
|
const body = await readPrefix(target, ctx.maxFileBytes);
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { CommandInterceptor } from "#src/command-interceptor/index";
|
|
13
13
|
import type { JSONSchema } from "#src/session/tool-descriptor";
|
|
14
14
|
import type { SandboxRecord } from "../sandbox/index.ts";
|
|
15
|
+
import type { OwnedPathsPolicy } from "./owned-paths.ts";
|
|
15
16
|
import type { ProtectedPathsPolicy } from "./protected-paths.ts";
|
|
16
17
|
import type { CodingToolName, ToolScope } from "./types.ts";
|
|
17
18
|
export interface ToolResult {
|
|
@@ -96,6 +97,8 @@ export interface ToolRunContext {
|
|
|
96
97
|
readonly interceptor?: CommandInterceptor;
|
|
97
98
|
/** Host-owned paths (S1 spec port 6). Absent: the Git tools exclude and skip nothing extra. */
|
|
98
99
|
readonly protectedPaths?: ProtectedPathsPolicy;
|
|
100
|
+
/** Host-owned path rules (S3-2 port). Absent: no owned-path refusals. */
|
|
101
|
+
readonly ownedPaths?: OwnedPathsPolicy;
|
|
99
102
|
}
|
|
100
103
|
export interface CodingTool {
|
|
101
104
|
readonly name: string;
|
|
@@ -112,6 +112,7 @@ export async function runExecBranch(input, ctx, opts) {
|
|
|
112
112
|
packageWorkdir: opts.exec.packageWorkdir,
|
|
113
113
|
packageRelPath,
|
|
114
114
|
allowScripts: opts.exec.allowScripts,
|
|
115
|
+
ownedPaths: ctx.ownedPaths,
|
|
115
116
|
...(opts.exec.packageName !== undefined ? { packageName: opts.exec.packageName } : {}),
|
|
116
117
|
});
|
|
117
118
|
if ("error" in normalized)
|