@nathapp/nax-agent 0.91.6 → 0.91.7-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 +8 -0
- package/dist/coding-tools/coding-tool-extras.d.ts +42 -0
- package/dist/coding-tools/coding-tool-extras.js +31 -0
- package/dist/coding-tools/coding-tool-support.d.ts +6 -0
- package/dist/coding-tools/coding-tool-support.js +7 -1
- package/dist/config/catalog-overrides.d.ts +5 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/internal/argv-exec.d.ts +2 -0
- package/dist/internal/argv-exec.js +72 -7
- package/dist/mcp/connect.js +26 -3
- package/dist/mcp/types.d.ts +7 -0
- package/dist/native/models.js +1 -0
- package/dist/native/session/loop-events/types.d.ts +5 -0
- package/dist/native/session-adapter.js +16 -5
- package/dist/permissions/approval-memory.d.ts +18 -0
- package/dist/permissions/approval-memory.js +88 -0
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.js +1 -0
- package/dist/permissions/types.d.ts +6 -0
- package/dist/sandbox/claude-settings.d.ts +2 -1
- package/dist/sandbox/claude-settings.js +7 -3
- package/dist/sandbox/defaults.js +1 -0
- package/dist/sandbox/launcher.js +3 -0
- package/dist/sandbox/types.d.ts +2 -0
- package/dist/session/agent-session-command.d.ts +19 -0
- package/dist/session/agent-session-command.js +38 -0
- package/dist/session/agent-session-types.d.ts +9 -0
- package/dist/session/agent-session.js +49 -1
- package/dist/session/native-backend-options.d.ts +18 -2
- package/dist/session/native-backend-options.js +26 -3
- package/dist/session/native-backend.js +42 -4
- package/dist/session/session-ask-link.d.ts +6 -0
- package/dist/session/session-ask-link.js +5 -0
- package/dist/session/session-ask-port.js +4 -0
- package/dist/session/session-backend.d.ts +21 -1
- package/dist/session/session-tool-support.d.ts +39 -3
- package/dist/session/session-tool-support.js +54 -10
- package/dist/session-store/index.d.ts +3 -0
- package/dist/session-store/index.js +3 -0
- package/dist/session-store/session-lock-guard.d.ts +6 -0
- package/dist/session-store/session-lock-guard.js +85 -0
- package/dist/session-store/session-lock.d.ts +39 -0
- package/dist/session-store/session-lock.js +175 -0
- package/dist/session-store/session-meta.d.ts +27 -0
- package/dist/session-store/session-meta.js +20 -0
- package/dist/session-store/storage.d.ts +39 -0
- package/dist/session-store/storage.js +164 -0
- package/dist/tools/ask-request.d.ts +18 -0
- package/dist/tools/ask-request.js +30 -0
- package/dist/tools/bash.d.ts +9 -0
- package/dist/tools/bash.js +61 -16
- package/dist/tools/git.js +10 -3
- package/dist/tools/policy-match.d.ts +2 -2
- package/dist/tools/policy-match.js +9 -6
- package/dist/tools/policy-paths-branch.js +4 -1
- package/dist/tools/provider-advertise.d.ts +7 -0
- package/dist/tools/provider-advertise.js +13 -1
- package/dist/tools/provider-types.d.ts +6 -0
- package/dist/tools/run-command-exec.js +12 -0
- package/dist/tools/run-command-surface.d.ts +28 -0
- package/dist/tools/run-command-surface.js +109 -0
- package/dist/tools/run-command.d.ts +17 -16
- package/dist/tools/run-command.js +48 -68
- package/dist/tools/runtime-calltool.d.ts +100 -0
- package/dist/tools/runtime-calltool.js +112 -3
- package/dist/tools/runtime.d.ts +11 -0
- package/dist/tools/runtime.js +36 -18
- package/dist/tools/types.d.ts +8 -0
- package/package.json +3 -3
|
@@ -25,6 +25,24 @@ export interface AskSummary {
|
|
|
25
25
|
* what approval needs; the payload is what the audit sink already holds.
|
|
26
26
|
*/
|
|
27
27
|
export declare function askSummary(tool: string, scope: ToolScope, input: Record<string, unknown>): AskSummary;
|
|
28
|
+
/**
|
|
29
|
+
* The reason clause an unsandboxed approval carries (US-004).
|
|
30
|
+
*
|
|
31
|
+
* The reason is model-authored and travels to a human, so it is masked through
|
|
32
|
+
* `maskForPrompt` and cut to `MAX_ASK_SUMMARY_CHARS` — in that order, for the
|
|
33
|
+
* same reason `askSummary` masks before it cuts: cutting first can split a
|
|
34
|
+
* secret so that no pattern matches what is left. A reason whose masked form
|
|
35
|
+
* would hide shell syntax is WITHHELD rather than denying the ask (the command
|
|
36
|
+
* itself is already vetted by `askSummary`); the person still learns the call
|
|
37
|
+
* wants to run outside the sandbox, which is the fact the suffix exists for.
|
|
38
|
+
*
|
|
39
|
+
* A blank or whitespace-only reason takes the SAME fallback as an absent one:
|
|
40
|
+
* the model wrote nothing that could explain anything, and the approver must
|
|
41
|
+
* never be shown the dangling clause the prefix alone renders.
|
|
42
|
+
*/
|
|
43
|
+
export declare function unsandboxedReasonSuffix(input: {
|
|
44
|
+
readonly unsandboxedReason?: unknown;
|
|
45
|
+
}): string;
|
|
28
46
|
/**
|
|
29
47
|
* The reason a `denied:ask` ledger row carries, chosen by WHO refused.
|
|
30
48
|
*
|
|
@@ -37,6 +37,36 @@ export function askSummary(tool, scope, input) {
|
|
|
37
37
|
return { summary: `${tool} [arguments withheld: contains a secret]`, unshowable: true };
|
|
38
38
|
return { summary: masked.masked.slice(0, MAX_ASK_SUMMARY_CHARS), unshowable: false };
|
|
39
39
|
}
|
|
40
|
+
/** The clause `unsandboxedReasonSuffix` renders, before the reason itself. */
|
|
41
|
+
const UNSANDBOXED_PREFIX = " -- runs OUTSIDE the sandbox: ";
|
|
42
|
+
/** What the suffix says when the model gave no usable reason. */
|
|
43
|
+
const NO_REASON = "no reason given";
|
|
44
|
+
/** What the suffix says instead of a reason that cannot be shown safely. */
|
|
45
|
+
const REASON_WITHHELD = "reason withheld";
|
|
46
|
+
/**
|
|
47
|
+
* The reason clause an unsandboxed approval carries (US-004).
|
|
48
|
+
*
|
|
49
|
+
* The reason is model-authored and travels to a human, so it is masked through
|
|
50
|
+
* `maskForPrompt` and cut to `MAX_ASK_SUMMARY_CHARS` — in that order, for the
|
|
51
|
+
* same reason `askSummary` masks before it cuts: cutting first can split a
|
|
52
|
+
* secret so that no pattern matches what is left. A reason whose masked form
|
|
53
|
+
* would hide shell syntax is WITHHELD rather than denying the ask (the command
|
|
54
|
+
* itself is already vetted by `askSummary`); the person still learns the call
|
|
55
|
+
* wants to run outside the sandbox, which is the fact the suffix exists for.
|
|
56
|
+
*
|
|
57
|
+
* A blank or whitespace-only reason takes the SAME fallback as an absent one:
|
|
58
|
+
* the model wrote nothing that could explain anything, and the approver must
|
|
59
|
+
* never be shown the dangling clause the prefix alone renders.
|
|
60
|
+
*/
|
|
61
|
+
export function unsandboxedReasonSuffix(input) {
|
|
62
|
+
const raw = input.unsandboxedReason;
|
|
63
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
64
|
+
return `${UNSANDBOXED_PREFIX}${NO_REASON}`;
|
|
65
|
+
const masked = maskForPrompt(raw);
|
|
66
|
+
if (!masked.ok)
|
|
67
|
+
return `${UNSANDBOXED_PREFIX}${REASON_WITHHELD}`;
|
|
68
|
+
return `${UNSANDBOXED_PREFIX}${masked.masked.slice(0, MAX_ASK_SUMMARY_CHARS)}`;
|
|
69
|
+
}
|
|
40
70
|
/**
|
|
41
71
|
* The reason a `denied:ask` ledger row carries, chosen by WHO refused.
|
|
42
72
|
*
|
package/dist/tools/bash.d.ts
CHANGED
|
@@ -61,6 +61,15 @@ export interface BashToolOptions {
|
|
|
61
61
|
* description, so the agent always knows whether it is sandboxed.
|
|
62
62
|
*/
|
|
63
63
|
readonly launcher?: CommandLauncher;
|
|
64
|
+
/**
|
|
65
|
+
* US-003: whether this session offers the per-call `unsandboxed` argument
|
|
66
|
+
* (`NativeBackendOptions.allowUnsandboxedCommands`, threaded down through
|
|
67
|
+
* `buildSessionToolSupport`). It shapes the ADVERTISEMENT only -- the schema
|
|
68
|
+
* and one description sentence. `run` never reads it: the tool only honours
|
|
69
|
+
* the flag on the call, so a session that did not advertise the argument
|
|
70
|
+
* cannot be steered into running unwrapped by a stray input field.
|
|
71
|
+
*/
|
|
72
|
+
readonly unsandboxedCommands?: boolean;
|
|
64
73
|
}
|
|
65
74
|
/**
|
|
66
75
|
* Injectable spawn seam, mirroring `_argvExecDeps`, plus the platform seam
|
package/dist/tools/bash.js
CHANGED
|
@@ -179,7 +179,7 @@ function bashToolDescription(shell, opts) {
|
|
|
179
179
|
* therefore never the string the policy judged, which is exactly why
|
|
180
180
|
* `interceptShell` re-validates the provider's answer rather than trusting it.
|
|
181
181
|
*/
|
|
182
|
-
async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, }) {
|
|
182
|
+
async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, unwrapped, }) {
|
|
183
183
|
// The run's interceptor arrives on the context (S1 spec port 7), immutable
|
|
184
184
|
// for this call, so the launch below and `postProcess` see the same object.
|
|
185
185
|
const interceptor = ctx.interceptor;
|
|
@@ -205,6 +205,9 @@ async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, }) {
|
|
|
205
205
|
spec: { kind: "shell", shell, command: intercepted.command },
|
|
206
206
|
root: ctx.root,
|
|
207
207
|
...common,
|
|
208
|
+
// US-003: the key is present only for an exact `true` on the call, so
|
|
209
|
+
// every other request stays byte-identical to what US-002 shipped.
|
|
210
|
+
...(unwrapped ? { unwrapped: true } : {}),
|
|
208
211
|
})
|
|
209
212
|
: { ...(await _bashToolDeps.runArgv({ argv: executed, ...common })), executed, sandbox: undefined };
|
|
210
213
|
if (!intercepted.rewritten)
|
|
@@ -223,6 +226,51 @@ async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, }) {
|
|
|
223
226
|
return { launched, stdout: launched.stdout };
|
|
224
227
|
}
|
|
225
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* US-003: the one sentence an opted-in session appends to its description, so
|
|
231
|
+
* the model knows the argument exists and what it is for. Absent from every
|
|
232
|
+
* description when the option is off, which keeps those byte-identical.
|
|
233
|
+
*/
|
|
234
|
+
const UNSANDBOXED_ARGUMENT_SENTENCE = 'Set "unsandboxed": true with an "unsandboxedReason" only when the command needs the network, a local socket or a device the sandbox blocks.';
|
|
235
|
+
/**
|
|
236
|
+
* US-003: the description with the opt-in sentence appended.
|
|
237
|
+
*
|
|
238
|
+
* A separate function rather than a final ternary inside `bashToolDescription`
|
|
239
|
+
* so every existing variant keeps its own wording verbatim, and the separator
|
|
240
|
+
* is chosen here: most variants already end on a space, the sandbox-sentence
|
|
241
|
+
* ones do not, and a description must never end on two spaces or none.
|
|
242
|
+
*/
|
|
243
|
+
function withUnsandboxedSentence(description, opts) {
|
|
244
|
+
if (opts.unsandboxedCommands !== true)
|
|
245
|
+
return description;
|
|
246
|
+
const separator = description.endsWith(" ") ? "" : " ";
|
|
247
|
+
return `${description}${separator}${UNSANDBOXED_ARGUMENT_SENTENCE}`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* US-003: the input schema, plus the two argument fields when the session opted
|
|
251
|
+
* in. Built here rather than inline in `createBashTool` so the schema's shape
|
|
252
|
+
* has one owner, and the off-form is exactly the three properties it always was.
|
|
253
|
+
*
|
|
254
|
+
* Neither added field is `required`: the escape is per call, and a call without
|
|
255
|
+
* it runs exactly as it does today.
|
|
256
|
+
*/
|
|
257
|
+
function bashToolInputSchema(opts) {
|
|
258
|
+
return {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
command: { type: "string", description: 'The shell command to run, e.g. "bun test src/a.test.ts"' },
|
|
262
|
+
timeoutMs: {
|
|
263
|
+
type: "number",
|
|
264
|
+
description: `Deadline in milliseconds (default and maximum ${BASH_TIMEOUT_MS}).`,
|
|
265
|
+
},
|
|
266
|
+
description: { type: "string", description: "One short line on what this command is for." },
|
|
267
|
+
...(opts.unsandboxedCommands === true
|
|
268
|
+
? { unsandboxed: { type: "boolean" }, unsandboxedReason: { type: "string" } }
|
|
269
|
+
: {}),
|
|
270
|
+
},
|
|
271
|
+
required: ["command"],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
226
274
|
export function createBashTool(opts = {}) {
|
|
227
275
|
const shell = opts.shell ?? DEFAULT_BASH_SHELL;
|
|
228
276
|
return {
|
|
@@ -231,23 +279,16 @@ export function createBashTool(opts = {}) {
|
|
|
231
279
|
// not a fault worth an operator's attention — the same reason RunCommand
|
|
232
280
|
// sets this.
|
|
233
281
|
routineErrors: true,
|
|
234
|
-
description: bashToolDescription(shell, opts),
|
|
235
|
-
inputSchema:
|
|
236
|
-
type: "object",
|
|
237
|
-
properties: {
|
|
238
|
-
command: { type: "string", description: 'The shell command to run, e.g. "bun test src/a.test.ts"' },
|
|
239
|
-
timeoutMs: {
|
|
240
|
-
type: "number",
|
|
241
|
-
description: `Deadline in milliseconds (default and maximum ${BASH_TIMEOUT_MS}).`,
|
|
242
|
-
},
|
|
243
|
-
description: { type: "string", description: "One short line on what this command is for." },
|
|
244
|
-
},
|
|
245
|
-
required: ["command"],
|
|
246
|
-
},
|
|
282
|
+
description: withUnsandboxedSentence(bashToolDescription(shell, opts), opts),
|
|
283
|
+
inputSchema: bashToolInputSchema(opts),
|
|
247
284
|
// `commandField` is what routes this call to the Bash branch of the policy
|
|
248
285
|
// (src/tools/policy-bash.ts). No pathFields: the paths are inside the
|
|
249
286
|
// command string, where only that branch can see them.
|
|
250
|
-
|
|
287
|
+
//
|
|
288
|
+
// US-003: `unsandboxedField` is declared unconditionally, so the runtime can
|
|
289
|
+
// see the field whether or not this session advertises it -- the option
|
|
290
|
+
// shapes the schema, never the scope.
|
|
291
|
+
scope: { pathFields: [], commandField: "command", unsandboxedField: "unsandboxed" },
|
|
251
292
|
async run(input, ctx) {
|
|
252
293
|
const command = input.command;
|
|
253
294
|
if (typeof command !== "string" || command.trim() === "") {
|
|
@@ -256,7 +297,11 @@ export function createBashTool(opts = {}) {
|
|
|
256
297
|
const requested = typeof input.timeoutMs === "number" && Number.isFinite(input.timeoutMs) ? input.timeoutMs : BASH_TIMEOUT_MS;
|
|
257
298
|
const timeoutMs = Math.min(Math.max(Math.trunc(requested), MIN_BASH_TIMEOUT_MS), BASH_TIMEOUT_MS);
|
|
258
299
|
try {
|
|
259
|
-
|
|
300
|
+
// US-003: exact `true` only. The tool never consults `opts.unsandboxedCommands`
|
|
301
|
+
// here: whether the argument is ADVERTISED is the option's business, and what a
|
|
302
|
+
// given call asked for is the call's.
|
|
303
|
+
const unwrapped = input.unsandboxed === true;
|
|
304
|
+
const { launched, stdout } = await launchIntercepted({ command, shell, opts, ctx, timeoutMs, unwrapped });
|
|
260
305
|
// Same framing ladder as RunCommand (Exec) via the shared
|
|
261
306
|
// `formatExecBody`: an aborted call opens with the cancellation line
|
|
262
307
|
// and surfaces whatever partial output the readers captured before the
|
package/dist/tools/git.js
CHANGED
|
@@ -171,8 +171,15 @@ function buildGitRootArgv(input) {
|
|
|
171
171
|
}
|
|
172
172
|
return ["rev-parse", "--show-toplevel"];
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* `blame` rejects exclude pathspecs (exit 128). On `log` and `show` an exclude
|
|
176
|
+
* filters COMMITS, not just files: a commit that only touches `.nax/` is
|
|
177
|
+
* dropped, so `log HEAD -1` reported an older commit as HEAD and `show HEAD`
|
|
178
|
+
* printed nothing (#2693). Only `diff` and `status` list files.
|
|
179
|
+
*/
|
|
180
|
+
const GIT_EXCLUDE_EXEMPT_VERBS = ["blame", "log", "show"];
|
|
174
181
|
function defaultGitExcludes(subcommand, excludes) {
|
|
175
|
-
return subcommand
|
|
182
|
+
return GIT_EXCLUDE_EXEMPT_VERBS.includes(subcommand) ? [] : excludes;
|
|
176
183
|
}
|
|
177
184
|
/**
|
|
178
185
|
* `defaultExcludes`: pathspecs appended to an unscoped call (the host's own
|
|
@@ -292,8 +299,8 @@ export function buildGitArgv(input, defaultExcludes = []) {
|
|
|
292
299
|
// on this DEFAULT branch: a caller that names a path under .nax/ gets it --
|
|
293
300
|
// the tool is read-only, and silently returning nothing for an explicitly
|
|
294
301
|
// requested path would be a worse failure than the one being fixed.
|
|
295
|
-
// `blame`
|
|
296
|
-
//
|
|
302
|
+
// `blame`, `log` and `show` are exempt (GIT_EXCLUDE_EXEMPT_VERBS); do not
|
|
303
|
+
// "unify" them back in.
|
|
297
304
|
argv.push(...defaultGitExcludes(subcommand, defaultExcludes));
|
|
298
305
|
return argv;
|
|
299
306
|
}
|
|
@@ -20,7 +20,7 @@ import type { ToolGrant } from "./types.ts";
|
|
|
20
20
|
* -- but it made a *scoped* grant wider than its author wrote, which is the one
|
|
21
21
|
* thing a scoped profile exists to prevent.
|
|
22
22
|
*/
|
|
23
|
-
export declare function globToRegExp(pattern: string): RegExp;
|
|
23
|
+
export declare function globToRegExp(pattern: string, ignoreCase?: boolean): RegExp;
|
|
24
24
|
/** A compiled glob, keeping its source so verb names can be told from paths. */
|
|
25
25
|
export interface CompiledPattern {
|
|
26
26
|
readonly source: string;
|
|
@@ -55,7 +55,7 @@ export declare function matchedGlobSource(patterns: readonly CompiledPattern[],
|
|
|
55
55
|
* is exactly bun, argv[1] starts with add", never "the space-joined argv
|
|
56
56
|
* matches this glob".
|
|
57
57
|
*/
|
|
58
|
-
export declare function compileArgvPattern(pattern: string): readonly CompiledPattern[];
|
|
58
|
+
export declare function compileArgvPattern(pattern: string, ignoreCase?: boolean): readonly CompiledPattern[];
|
|
59
59
|
/** A grant pattern's tokens are a PREFIX of argv — trailing argv tokens
|
|
60
60
|
* (the package name, flags, ...) are the payload a prefix grant like
|
|
61
61
|
* `bun add*` does not itself constrain. */
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* -- but it made a *scoped* grant wider than its author wrote, which is the one
|
|
20
20
|
* thing a scoped profile exists to prevent.
|
|
21
21
|
*/
|
|
22
|
-
export function globToRegExp(pattern) {
|
|
22
|
+
export function globToRegExp(pattern, ignoreCase = false) {
|
|
23
23
|
let out = "";
|
|
24
24
|
for (let i = 0; i < pattern.length; i += 1) {
|
|
25
25
|
const char = pattern[i];
|
|
@@ -46,7 +46,7 @@ export function globToRegExp(pattern) {
|
|
|
46
46
|
}
|
|
47
47
|
out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
48
48
|
}
|
|
49
|
-
return new RegExp(`^${out}
|
|
49
|
+
return new RegExp(`^${out}$`, ignoreCase ? "i" : "");
|
|
50
50
|
}
|
|
51
51
|
export function matchesAny(patterns, value) {
|
|
52
52
|
return patterns.some((p) => p.re.test(value));
|
|
@@ -64,11 +64,11 @@ export function matchedGlobSource(patterns, value) {
|
|
|
64
64
|
* is exactly bun, argv[1] starts with add", never "the space-joined argv
|
|
65
65
|
* matches this glob".
|
|
66
66
|
*/
|
|
67
|
-
export function compileArgvPattern(pattern) {
|
|
67
|
+
export function compileArgvPattern(pattern, ignoreCase = false) {
|
|
68
68
|
return pattern
|
|
69
69
|
.split(/\s+/)
|
|
70
70
|
.filter((token) => token.length > 0)
|
|
71
|
-
.map((token) => ({ source: token, re: globToRegExp(token) }));
|
|
71
|
+
.map((token) => ({ source: token, re: globToRegExp(token, ignoreCase) }));
|
|
72
72
|
}
|
|
73
73
|
/** A grant pattern's tokens are a PREFIX of argv — trailing argv tokens
|
|
74
74
|
* (the package name, flags, ...) are the payload a prefix grant like
|
|
@@ -96,8 +96,11 @@ export function compileRuleMap(rules) {
|
|
|
96
96
|
const nonWildcard = patterns.filter((p) => p !== "*");
|
|
97
97
|
map.set(rule.tool, {
|
|
98
98
|
unconditional: patterns.includes("*"),
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// Case-insensitive, unlike grants: on a case-insensitive filesystem `.ENV` and `.env` are one
|
|
100
|
+
// file, and a deny/ask rule must not be walked past by respelling it (nax#1972, as denyPaths).
|
|
101
|
+
// Over-matching only asks or refuses more, the direction these lists should fail in.
|
|
102
|
+
matchers: nonWildcard.map((source) => ({ source, re: globToRegExp(source, true) })),
|
|
103
|
+
argvPatterns: nonWildcard.map((source) => compileArgvPattern(source, true)),
|
|
101
104
|
raw: patterns,
|
|
102
105
|
rulePatterns: [...(existing?.rulePatterns ?? []), rule.patterns],
|
|
103
106
|
});
|
|
@@ -46,7 +46,10 @@ function entersGitMetadata(root, resolved) {
|
|
|
46
46
|
const rel = relative(realOrRaw(root), resolved);
|
|
47
47
|
if (rel === "" || rel.startsWith(".."))
|
|
48
48
|
return false;
|
|
49
|
-
|
|
49
|
+
// Case-folded: on a case-insensitive filesystem (macOS, Windows defaults) `.GIT/config` IS
|
|
50
|
+
// `.git/config`, and the canonical path keeps the caller's spelling. Over-refusing a genuinely
|
|
51
|
+
// distinct `.GIT` on a case-sensitive filesystem is the direction this ban should fail in.
|
|
52
|
+
return rel.split(sep).some((segment) => segment.toLowerCase() === ".git");
|
|
50
53
|
}
|
|
51
54
|
/**
|
|
52
55
|
* Absolute, symlink-resolved form of `candidate` if it lies inside `root`,
|
|
@@ -53,6 +53,13 @@ export interface ResolvedProviderTools {
|
|
|
53
53
|
providerId: string;
|
|
54
54
|
text: string;
|
|
55
55
|
}[];
|
|
56
|
+
/**
|
|
57
|
+
* Advertised (namespaced) names of tools from a `discovered` provider whose
|
|
58
|
+
* `readOnly` is not `true`, in advertise order: the ones an ask-profile
|
|
59
|
+
* session puts to the person before running. A `static` provider's tools
|
|
60
|
+
* never appear (their schemas are authored in this repo). ABSENT when empty.
|
|
61
|
+
*/
|
|
62
|
+
readonly askTools?: readonly string[];
|
|
56
63
|
}
|
|
57
64
|
export declare function resolveProviderTools(providers: readonly ToolProvider[], stage: string, workdir: string, options?: {
|
|
58
65
|
/**
|
|
@@ -35,6 +35,14 @@ async function instructionsFor(provider, workdir) {
|
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
/** `{ [key]: list }` for a non-empty list, `{}` otherwise: the key is absent, never `[]`. */
|
|
39
|
+
function whenNonEmpty(key, list) {
|
|
40
|
+
return list.length > 0 ? { [key]: list } : {};
|
|
41
|
+
}
|
|
42
|
+
/** A discovered tool its server did not mark read-only; a static provider's tools never ask. */
|
|
43
|
+
function asksBeforeRunning(provider, tool) {
|
|
44
|
+
return provider.kind === "discovered" && tool.readOnly !== true;
|
|
45
|
+
}
|
|
38
46
|
export async function resolveProviderTools(providers, stage, workdir, options) {
|
|
39
47
|
const tools = [];
|
|
40
48
|
const entries = [];
|
|
@@ -43,6 +51,7 @@ export async function resolveProviderTools(providers, stage, workdir, options) {
|
|
|
43
51
|
const withheld = [];
|
|
44
52
|
const providerIdByTool = new Map();
|
|
45
53
|
const instructions = [];
|
|
54
|
+
const askTools = [];
|
|
46
55
|
for (const provider of providers) {
|
|
47
56
|
if (!providerAttachesTo(provider, stage)) {
|
|
48
57
|
skipped.push({ providerId: provider.id, reason: "stage-mismatch", stages: provider.stages });
|
|
@@ -60,6 +69,8 @@ export async function resolveProviderTools(providers, stage, workdir, options) {
|
|
|
60
69
|
const adapted = adaptProviderTool(provider.id, tool);
|
|
61
70
|
tools.push(adapted);
|
|
62
71
|
providerIdByTool.set(adapted.name, provider.id);
|
|
72
|
+
if (asksBeforeRunning(provider, tool))
|
|
73
|
+
askTools.push(adapted.name);
|
|
63
74
|
localNames.push(tool.localName);
|
|
64
75
|
}
|
|
65
76
|
entries.push({ providerId: provider.id, localNames });
|
|
@@ -84,7 +95,8 @@ export async function resolveProviderTools(providers, stage, workdir, options) {
|
|
|
84
95
|
withheld,
|
|
85
96
|
providerIdByTool,
|
|
86
97
|
entries,
|
|
87
|
-
...(instructions
|
|
98
|
+
...whenNonEmpty("instructions", instructions),
|
|
99
|
+
...whenNonEmpty("askTools", askTools),
|
|
88
100
|
};
|
|
89
101
|
}
|
|
90
102
|
/**
|
|
@@ -18,6 +18,12 @@ export interface ProviderTool {
|
|
|
18
18
|
readonly localName: string;
|
|
19
19
|
readonly description: string;
|
|
20
20
|
readonly inputSchema: JSONSchema;
|
|
21
|
+
/**
|
|
22
|
+
* The provider's own claim that the tool does not modify anything. Only a
|
|
23
|
+
* `discovered` provider sets it (an MCP server's `readOnlyHint`); it is
|
|
24
|
+
* trusted as stated and decides whether an ask-profile session asks first.
|
|
25
|
+
*/
|
|
26
|
+
readonly readOnly?: boolean;
|
|
21
27
|
run(input: Record<string, unknown>, ctx: ToolRunContext): Promise<ToolResult>;
|
|
22
28
|
}
|
|
23
29
|
export interface ToolProvider {
|
|
@@ -71,6 +71,10 @@ async function launchExec(req) {
|
|
|
71
71
|
stripEnvVars: req.stripEnvVars,
|
|
72
72
|
...(req.mergedEnv !== undefined ? { env: req.mergedEnv } : {}),
|
|
73
73
|
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
74
|
+
// US-008: the key is present only for an exact `true` on the call, so
|
|
75
|
+
// every other request stays byte-identical to what US-002 shipped -- and
|
|
76
|
+
// the launcher, not this branch, decides what "unwrapped" means.
|
|
77
|
+
...(req.unwrapped ? { unwrapped: true } : {}),
|
|
74
78
|
});
|
|
75
79
|
}
|
|
76
80
|
return {
|
|
@@ -133,6 +137,14 @@ export async function runExecBranch(input, ctx, opts) {
|
|
|
133
137
|
mergedEnv,
|
|
134
138
|
stripEnvVars: opts.stripEnvVars ?? [],
|
|
135
139
|
signal: ctx.signal,
|
|
140
|
+
// US-008: exact `true` only, read off the CALL. Whether the session
|
|
141
|
+
// offers the argument, and whether the request is permitted at all, was
|
|
142
|
+
// settled before `run` was reached (the runtime's `resolveUnsandboxedCall`
|
|
143
|
+
// and its ask), so re-reading `opts.exec.unsandboxedCommands` here would
|
|
144
|
+
// be a second authorization gate in the one place that cannot see the
|
|
145
|
+
// approval. A `"true"` string is not an escape request: it is the ordinary
|
|
146
|
+
// call it reads as.
|
|
147
|
+
unwrapped: input.unsandboxed === true,
|
|
136
148
|
});
|
|
137
149
|
// US-001: match Bash's framing so an aborted Exec is rendered as an
|
|
138
150
|
// error with the cancellation banner (AC14 mirror) and an orphansKilled
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CodingTool } from "./registry.ts";
|
|
2
|
+
import type { RunCommandExecOptions } from "./run-command.ts";
|
|
3
|
+
/**
|
|
4
|
+
* US-006: the platform seam `execSandboxNote` reads to decide whether the
|
|
5
|
+
* description carries the macOS ps note.
|
|
6
|
+
*
|
|
7
|
+
* Kept out of `src/tools/index.ts` on purpose: `src/index.ts` takes named
|
|
8
|
+
* exports from that barrel, so a seam there would reach the package's public
|
|
9
|
+
* `.` entry, where `_` names are forbidden. It DOES reach `./internal`, because
|
|
10
|
+
* `src/internal.ts` star-exports `run-command.ts` — which is exactly where a
|
|
11
|
+
* `_*Deps` seam belongs (nax-only, outside semver), and why adding it needed
|
|
12
|
+
* the one-line api/ snapshot update. Tests reach it through
|
|
13
|
+
* `#src/tools/run-command`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const _runCommandToolDeps: {
|
|
16
|
+
platform: () => NodeJS.Platform;
|
|
17
|
+
};
|
|
18
|
+
export declare function runCommandDescription(commandDescriptions: string, exec: RunCommandExecOptions | undefined): string;
|
|
19
|
+
/**
|
|
20
|
+
* The tool's input schema. Each of the three conditional groups (declared
|
|
21
|
+
* fields, argv fields, the opt-in escape argument) has one owner here, so
|
|
22
|
+
* `createRunCommandTool` carries no schema branch of its own.
|
|
23
|
+
*
|
|
24
|
+
* With exec available, neither field is unconditionally required — `run()`
|
|
25
|
+
* enforces "exactly one of command or argv" at call time, which a static JSON
|
|
26
|
+
* Schema `required` list cannot express as an either/or.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runCommandInputSchema(keys: readonly string[], exec: RunCommandExecOptions | undefined): CodingTool["inputSchema"];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { sandboxSentence, unsandboxedSentence } from "../sandbox/index.js";
|
|
2
|
+
import { describeExecAllowlist } from "./exec-allowlist-text.js";
|
|
3
|
+
/**
|
|
4
|
+
* US-006: the platform seam `execSandboxNote` reads to decide whether the
|
|
5
|
+
* description carries the macOS ps note.
|
|
6
|
+
*
|
|
7
|
+
* Kept out of `src/tools/index.ts` on purpose: `src/index.ts` takes named
|
|
8
|
+
* exports from that barrel, so a seam there would reach the package's public
|
|
9
|
+
* `.` entry, where `_` names are forbidden. It DOES reach `./internal`, because
|
|
10
|
+
* `src/internal.ts` star-exports `run-command.ts` — which is exactly where a
|
|
11
|
+
* `_*Deps` seam belongs (nax-only, outside semver), and why adding it needed
|
|
12
|
+
* the one-line api/ snapshot update. Tests reach it through
|
|
13
|
+
* `#src/tools/run-command`.
|
|
14
|
+
*/
|
|
15
|
+
export const _runCommandToolDeps = {
|
|
16
|
+
platform: () => process.platform,
|
|
17
|
+
};
|
|
18
|
+
function execSandboxNote(launcher) {
|
|
19
|
+
const state = launcher?.state;
|
|
20
|
+
if (state?.kind === "available")
|
|
21
|
+
return ` An "argv" call runs ${sandboxSentence(state.network, state.sharedTmp !== false, _runCommandToolDeps.platform())}`;
|
|
22
|
+
if (state?.kind === "unavailable")
|
|
23
|
+
return ` ${unsandboxedSentence(state.reason)}`;
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* US-008: the one sentence an opted-in session appends to an Exec-capable
|
|
28
|
+
* description. Worded for the argv branch ("the argv", not "the command") so it
|
|
29
|
+
* reads as the argv branch's own argument — the declared branch refuses the
|
|
30
|
+
* flag by name, and a shared sentence would promise a capability half the tool
|
|
31
|
+
* rejects.
|
|
32
|
+
*/
|
|
33
|
+
const UNSANDBOXED_ARGV_SENTENCE = 'Set "unsandboxed": true with an "unsandboxedReason" only when the argv needs the network, a local socket or a device the sandbox blocks.';
|
|
34
|
+
/**
|
|
35
|
+
* US-008: the description with the opt-in sentence appended, or the description
|
|
36
|
+
* unchanged. A separate function for the same reason `withUnsandboxedSentence`
|
|
37
|
+
* is one in bash.ts: the separator is chosen here, so the off-form stays
|
|
38
|
+
* byte-identical to what shipped, and no caller has to end its own sentence
|
|
39
|
+
* with a space to keep the result readable.
|
|
40
|
+
*/
|
|
41
|
+
function withUnsandboxedArgvSentence(description, exec) {
|
|
42
|
+
if (exec.unsandboxedCommands !== true)
|
|
43
|
+
return description;
|
|
44
|
+
const separator = description.endsWith(" ") ? "" : " ";
|
|
45
|
+
return `${description}${separator}${UNSANDBOXED_ARGV_SENTENCE}`;
|
|
46
|
+
}
|
|
47
|
+
function execDescription(commandDescriptions, exec) {
|
|
48
|
+
return withUnsandboxedArgvSentence(`Two ways to run something. (1) Run one of this project's declared commands: ${commandDescriptions} — supply "command" and, optionally, "values" for its placeholders. (2) Run an allowlisted external command via "argv" (an array, e.g. ["bun","add","left-pad"]) — no shell, so no quoting and no shell metacharacters; ${describeExecAllowlist(exec.patterns)}. Supply exactly one of "command" or "argv".${execSandboxNote(exec.launcher)}`, exec);
|
|
49
|
+
}
|
|
50
|
+
export function runCommandDescription(commandDescriptions, exec) {
|
|
51
|
+
if (exec === undefined) {
|
|
52
|
+
return `Run one of this project's declared commands: ${commandDescriptions}. Supply values for its placeholders; you cannot write a command of your own.`;
|
|
53
|
+
}
|
|
54
|
+
return execDescription(commandDescriptions, exec);
|
|
55
|
+
}
|
|
56
|
+
/** The declared branch's two fields, always present. */
|
|
57
|
+
function declaredInputProperties(keys) {
|
|
58
|
+
return {
|
|
59
|
+
command: { type: "string", enum: keys, description: "Which declared command to run" },
|
|
60
|
+
values: {
|
|
61
|
+
type: "object",
|
|
62
|
+
description: 'Values for the command\'s placeholders, e.g. { files: "a.test.ts" }. A path placeholder takes several paths separated by spaces, e.g. { files: "a.test.ts b.test.ts" }, or as an array of paths, e.g. { files: ["test/a.test.ts", "test/b.test.ts"] } -- one call, not one per file.',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The argv branch's two fields, present only when `exec` is available. */
|
|
67
|
+
function argvInputProperties() {
|
|
68
|
+
return {
|
|
69
|
+
argv: {
|
|
70
|
+
type: "array",
|
|
71
|
+
items: { type: "string" },
|
|
72
|
+
description: 'Argv for an allowlisted external command, e.g. ["bun","add","left-pad"]. No shell: each element is one literal argument. Supply exactly one of "command" or "argv".',
|
|
73
|
+
},
|
|
74
|
+
target: {
|
|
75
|
+
type: "string",
|
|
76
|
+
enum: ["repoRoot", "package"],
|
|
77
|
+
description: 'Working directory for "argv": this package (default) or the repo root.',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* US-008: the escape argument's two fields, only when the session opted in.
|
|
83
|
+
* Neither is `required`: the escape is per call, and a call without it runs
|
|
84
|
+
* exactly as it does today.
|
|
85
|
+
*/
|
|
86
|
+
function unsandboxedArgvProperties(exec) {
|
|
87
|
+
if (exec.unsandboxedCommands !== true)
|
|
88
|
+
return {};
|
|
89
|
+
return { unsandboxed: { type: "boolean" }, unsandboxedReason: { type: "string" } };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The tool's input schema. Each of the three conditional groups (declared
|
|
93
|
+
* fields, argv fields, the opt-in escape argument) has one owner here, so
|
|
94
|
+
* `createRunCommandTool` carries no schema branch of its own.
|
|
95
|
+
*
|
|
96
|
+
* With exec available, neither field is unconditionally required — `run()`
|
|
97
|
+
* enforces "exactly one of command or argv" at call time, which a static JSON
|
|
98
|
+
* Schema `required` list cannot express as an either/or.
|
|
99
|
+
*/
|
|
100
|
+
export function runCommandInputSchema(keys, exec) {
|
|
101
|
+
return {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
...declaredInputProperties(keys),
|
|
105
|
+
...(exec === undefined ? {} : { ...argvInputProperties(), ...unsandboxedArgvProperties(exec) }),
|
|
106
|
+
},
|
|
107
|
+
...(exec === undefined ? { required: ["command"] } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { QualityCommandSpec } from "#src/internal/command-spec/index";
|
|
2
|
-
import {
|
|
2
|
+
import type { CommandLauncher } from "../sandbox/index.ts";
|
|
3
3
|
import type { CodingTool } from "./registry.ts";
|
|
4
|
+
export { _runCommandToolDeps } from "./run-command-surface.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Context for RunCommand's allowlisted, model-authored argv branch (`Exec`).
|
|
6
7
|
*
|
|
@@ -27,6 +28,21 @@ export interface RunCommandExecOptions {
|
|
|
27
28
|
readonly patterns: readonly string[];
|
|
28
29
|
/** P4: how the argv runs. Absent = direct runArgv (unit tests). */
|
|
29
30
|
readonly launcher?: CommandLauncher;
|
|
31
|
+
/**
|
|
32
|
+
* US-008: whether this session offers the per-call `unsandboxed` argument on
|
|
33
|
+
* the argv branch (`NativeBackendOptions.allowUnsandboxedCommands`, threaded
|
|
34
|
+
* down through `buildDeclaredCommandTools`). It shapes the ADVERTISEMENT only
|
|
35
|
+
* -- the two schema fields and one description sentence.
|
|
36
|
+
*
|
|
37
|
+
* `runExecBranch` never reads it: an `argv` call carrying `unsandboxed: true`
|
|
38
|
+
* runs unwrapped whenever the RUNTIME let the call through, which is where
|
|
39
|
+
* "is this permitted" is decided (US-005/US-006, `resolveUnsandboxedCall` in
|
|
40
|
+
* `src/tools/runtime-calltool.ts`). Making the option a second gate here
|
|
41
|
+
* would put the same decision in two places, and a session that simply did
|
|
42
|
+
* not advertise the field could then be steered into running unwrapped by a
|
|
43
|
+
* stray input key.
|
|
44
|
+
*/
|
|
45
|
+
readonly unsandboxedCommands?: boolean;
|
|
30
46
|
}
|
|
31
47
|
/** What RunCommand hands the runner for one declared command. */
|
|
32
48
|
export interface DeclaredCommandRequest {
|
|
@@ -104,19 +120,4 @@ export declare function substituteCommand(template: string, values: Record<strin
|
|
|
104
120
|
export declare function substituteCommandSpec(spec: QualityCommandSpec, values: Record<string, string | readonly string[]>): QualityCommandSpec | {
|
|
105
121
|
error: string;
|
|
106
122
|
};
|
|
107
|
-
/**
|
|
108
|
-
* US-006: the platform seam `execSandboxNote` reads to decide whether the
|
|
109
|
-
* description carries the macOS ps note.
|
|
110
|
-
*
|
|
111
|
-
* Kept out of `src/tools/index.ts` on purpose: `src/index.ts` takes named
|
|
112
|
-
* exports from that barrel, so a seam there would reach the package's public
|
|
113
|
-
* `.` entry, where `_` names are forbidden. It DOES reach `./internal`, because
|
|
114
|
-
* `src/internal.ts` star-exports this module — which is exactly where a
|
|
115
|
-
* `_*Deps` seam belongs (nax-only, outside semver), and why adding it here
|
|
116
|
-
* needed the one-line api/ snapshot update. Tests reach it through
|
|
117
|
-
* `#src/tools/run-command`.
|
|
118
|
-
*/
|
|
119
|
-
export declare const _runCommandToolDeps: {
|
|
120
|
-
platform: () => NodeJS.Platform;
|
|
121
|
-
};
|
|
122
123
|
export declare function createRunCommandTool(declared: ReadonlyMap<string, QualityCommandSpec>, opts?: RunCommandToolOptions): CodingTool;
|