@nathapp/nax-agent 0.91.7-canary.7 → 0.91.7-canary.8
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 +4 -0
- package/dist/coding-tools/coding-tool-extras.d.ts +8 -0
- package/dist/coding-tools/coding-tool-extras.js +17 -0
- package/dist/coding-tools/coding-tool-support.d.ts +2 -0
- package/dist/coding-tools/coding-tool-support.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/internal/argv-exec.d.ts +4 -0
- package/dist/internal/argv-exec.js +86 -56
- package/dist/sandbox/launcher.js +4 -0
- package/dist/sandbox/types.d.ts +4 -0
- package/dist/session/agent-session-types.d.ts +3 -0
- package/dist/session/native-backend-options.js +3 -0
- package/dist/session/native-backend.js +2 -0
- package/dist/session/session-tool-support.d.ts +2 -0
- package/dist/session/session-tool-support.js +2 -1
- package/dist/tools/bash-background.d.ts +74 -0
- package/dist/tools/bash-background.js +151 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +19 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
|
|
5
5
|
release may change the public API.
|
|
6
6
|
|
|
7
|
+
## [0.91.7-canary.8] - 2026-10-29
|
|
8
|
+
|
|
9
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
10
|
+
|
|
7
11
|
## [0.91.7-canary.7] - 2026-10-29
|
|
8
12
|
|
|
9
13
|
- No changes in this package. Released in lockstep with the other nax packages.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { BashApprovalMode } from "#src/config/bash-approval";
|
|
10
10
|
import type { QualityCommandSpec } from "#src/internal/command-spec/index";
|
|
11
11
|
import type { CommandLauncher } from "#src/sandbox/index";
|
|
12
|
+
import type { BackgroundCommandPort } from "#src/tools/bash-background";
|
|
12
13
|
import { type CodingTool, type DeclaredCommandRunner, type ToolGrant } from "#src/tools/index";
|
|
13
14
|
export interface DeclaredCommandToolsArgs {
|
|
14
15
|
readonly declaredCommands: ReadonlyMap<string, QualityCommandSpec>;
|
|
@@ -39,6 +40,7 @@ export interface DeclaredCommandToolsArgs {
|
|
|
39
40
|
* flag by name.
|
|
40
41
|
*/
|
|
41
42
|
readonly unsandboxedCommands?: boolean;
|
|
43
|
+
readonly backgroundCommands?: BackgroundCommandPort;
|
|
42
44
|
}
|
|
43
45
|
/**
|
|
44
46
|
* US-003: `unsandboxedCommands` as a spreadable field — present only when the
|
|
@@ -77,3 +79,9 @@ export declare function unsandboxedRuntimeOption(args: {
|
|
|
77
79
|
};
|
|
78
80
|
};
|
|
79
81
|
export declare function buildDeclaredCommandTools(args: DeclaredCommandToolsArgs): CodingTool[];
|
|
82
|
+
/** Preserve absence at every session/support forwarding hop. */
|
|
83
|
+
export declare function backgroundToolOptions(args?: {
|
|
84
|
+
readonly backgroundCommands?: BackgroundCommandPort;
|
|
85
|
+
}): {
|
|
86
|
+
readonly backgroundCommands?: BackgroundCommandPort;
|
|
87
|
+
};
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-local tools an operation declares: RunCommand (its declared
|
|
3
|
+
* commands and, when `Exec` is declared, its argv branch) and Bash.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from coding-tool-support.ts to keep that file under its 600-line
|
|
6
|
+
* source limit ahead of the P4 sandbox wiring. A pure move: every comment that
|
|
7
|
+
* explains a field below travelled with it.
|
|
8
|
+
*/
|
|
1
9
|
import { createBashTool, createRunCommandTool, } from "#src/tools/index";
|
|
2
10
|
/**
|
|
3
11
|
* US-003: `unsandboxedCommands` as a spreadable field — present only when the
|
|
@@ -88,8 +96,17 @@ export function buildDeclaredCommandTools(args) {
|
|
|
88
96
|
...(args.humanApproval === true ? { humanApproval: true } : {}),
|
|
89
97
|
...(args.launcher !== undefined ? { launcher: args.launcher } : {}),
|
|
90
98
|
...unsandboxedToolOptions(args),
|
|
99
|
+
...bashBackgroundOption(args),
|
|
91
100
|
}),
|
|
92
101
|
]
|
|
93
102
|
: []),
|
|
94
103
|
];
|
|
95
104
|
}
|
|
105
|
+
/** Preserve absence at every session/support forwarding hop. */
|
|
106
|
+
export function backgroundToolOptions(args) {
|
|
107
|
+
return args?.backgroundCommands === undefined ? {} : { backgroundCommands: args.backgroundCommands };
|
|
108
|
+
}
|
|
109
|
+
/** Only Bash receives this capability; RunCommand's argv branch stays unchanged. */
|
|
110
|
+
function bashBackgroundOption(args) {
|
|
111
|
+
return args.backgroundCommands === undefined ? {} : { background: args.backgroundCommands };
|
|
112
|
+
}
|
|
@@ -14,6 +14,7 @@ import type { BashApprovalMode } from "#src/config/bash-approval";
|
|
|
14
14
|
import type { QualityCommandSpec } from "#src/internal/command-spec/index";
|
|
15
15
|
import type { AskResolver } from "#src/permissions/index";
|
|
16
16
|
import type { CommandLauncher } from "#src/sandbox/index";
|
|
17
|
+
import type { BackgroundCommandPort } from "#src/tools/bash-background";
|
|
17
18
|
import { type CodingTool, type CodingToolName, type CodingToolRuntime, type DeclaredCommandRunner, type ProtectedPathsPolicy, type ToolAuditSink, type ToolGrant, type ToolPatternNarrowing, type ToolSurfaceDraft, type ToolSurfaceRecord } from "#src/tools/index";
|
|
18
19
|
import type { OwnedPathsPolicy } from "#src/tools/owned-paths";
|
|
19
20
|
export interface CodingToolSupport {
|
|
@@ -148,6 +149,7 @@ export declare function buildCodingToolSupport(args: {
|
|
|
148
149
|
* the runtime's own reading of the field is a later story.
|
|
149
150
|
*/
|
|
150
151
|
unsandboxedCommands?: boolean;
|
|
152
|
+
backgroundCommands?: BackgroundCommandPort;
|
|
151
153
|
askOnUnsandboxed?: boolean;
|
|
152
154
|
/** US-001: forwarded into ToolRunContext so Bash/Exec can SIGKILL on cancel. */
|
|
153
155
|
abortSignal?: AbortSignal;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { getSafeLogger, NaxError } from "#src/infra/index";
|
|
12
12
|
import { advertisedSchemaBytes, BASH_TOOL_NAME, compileToolPolicy, createCodingToolRuntime, createNoOpToolAuditSink, createToolAuditSink, EXEC_TOOL_NAME, } from "#src/tools/index";
|
|
13
13
|
import { resolveBashSupport } from "./coding-tool-bash.js";
|
|
14
|
-
import { buildDeclaredCommandTools, unsandboxedRuntimeOption, unsandboxedToolOptions } from "./coding-tool-extras.js";
|
|
14
|
+
import { backgroundToolOptions, buildDeclaredCommandTools, unsandboxedRuntimeOption, unsandboxedToolOptions, } from "./coding-tool-extras.js";
|
|
15
15
|
import { isTempConfined, rawScreenOptionsFor } from "./coding-tool-sandbox.js";
|
|
16
16
|
export function buildCodingToolSupport(args) {
|
|
17
17
|
if (args.declared.length === 0)
|
|
@@ -120,6 +120,7 @@ export function buildCodingToolSupport(args) {
|
|
|
120
120
|
...(args.shell !== undefined ? { shell: args.shell } : {}),
|
|
121
121
|
...(args.launcher !== undefined ? { launcher: args.launcher } : {}),
|
|
122
122
|
...unsandboxedToolOptions(args),
|
|
123
|
+
...backgroundToolOptions(args),
|
|
123
124
|
}),
|
|
124
125
|
],
|
|
125
126
|
...(args.providerIdByTool !== undefined ? { providerIdByTool: args.providerIdByTool } : {}),
|
package/dist/index.d.ts
CHANGED
|
@@ -46,3 +46,4 @@ export type { TurnEvent, TurnEventSink } from "#src/session/turn-event";
|
|
|
46
46
|
export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_MAX_CHARS_TOTAL, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, renderProviderInstructions, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
|
|
47
47
|
export { compileArgvPattern, matchesArgvPattern, type CompiledPattern } from "#src/tools/policy-match";
|
|
48
48
|
export { EMPTY_OWNED_PATHS_POLICY, type OwnedBashCandidate, type OwnedPathsPolicy, } from "#src/tools/owned-paths";
|
|
49
|
+
export type { BackgroundCommandPort, BackgroundCommandHandle, BackgroundCommandSink, BackgroundCommandEnd, ForegroundCommand, } from "#src/tools/bash-background";
|
|
@@ -18,6 +18,10 @@ export interface RunArgvOptions {
|
|
|
18
18
|
* and the result resolves with `aborted: true`. US-001.
|
|
19
19
|
*/
|
|
20
20
|
readonly signal?: AbortSignal;
|
|
21
|
+
/** Abort to remove only the deadline, without cancelling the command. */
|
|
22
|
+
readonly releaseDeadline?: AbortSignal;
|
|
23
|
+
/** Observes every read chunk before capture; observer exceptions are ignored. */
|
|
24
|
+
readonly onOutput?: (stream: "stdout" | "stderr", chunk: Uint8Array) => void;
|
|
21
25
|
}
|
|
22
26
|
export interface ArgvExecResult {
|
|
23
27
|
readonly exitCode: number;
|
|
@@ -84,7 +84,7 @@ function buildEnv(options) {
|
|
|
84
84
|
* after a process-group kill: a child that ignores the kill cannot pin
|
|
85
85
|
* runArgv forever on its still-open pipe.
|
|
86
86
|
*/
|
|
87
|
-
async function drainToEof(stream, signal) {
|
|
87
|
+
async function drainToEof(stream, signal, onChunk) {
|
|
88
88
|
const reader = stream.getReader();
|
|
89
89
|
const capture = new BoundedCapture(_argvExecDeps.streamCaptureBytes);
|
|
90
90
|
const onAbort = () => {
|
|
@@ -100,8 +100,16 @@ async function drainToEof(stream, signal) {
|
|
|
100
100
|
const { done, value } = await reader.read();
|
|
101
101
|
if (done === true)
|
|
102
102
|
break;
|
|
103
|
-
if (value !== undefined)
|
|
103
|
+
if (value !== undefined) {
|
|
104
|
+
try {
|
|
105
|
+
// Copy even Buffer-backed chunks: observers must not mutate captured bytes.
|
|
106
|
+
onChunk?.(Uint8Array.from(value));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Observers are best-effort: never let one truncate capture or stop draining.
|
|
110
|
+
}
|
|
104
111
|
capture.push(value);
|
|
112
|
+
}
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
catch {
|
|
@@ -175,6 +183,24 @@ class BoundedCapture {
|
|
|
175
183
|
return head + marker + new TextDecoder().decode(tail.subarray(start));
|
|
176
184
|
}
|
|
177
185
|
}
|
|
186
|
+
/** The deadline is releasable independently of command cancellation and drain grace. */
|
|
187
|
+
function armDeadline(options, onTimeout) {
|
|
188
|
+
let timerId;
|
|
189
|
+
const release = () => {
|
|
190
|
+
if (timerId !== undefined)
|
|
191
|
+
_argvExecDeps.clearTimeout(timerId);
|
|
192
|
+
timerId = undefined;
|
|
193
|
+
};
|
|
194
|
+
if (options.releaseDeadline?.aborted !== true) {
|
|
195
|
+
// A cancellable timer: releaseDeadline and settlement both clear it mid-flight.
|
|
196
|
+
timerId = _argvExecDeps.setTimeout(onTimeout, options.timeoutMs);
|
|
197
|
+
options.releaseDeadline?.addEventListener("abort", release, { once: true });
|
|
198
|
+
}
|
|
199
|
+
return () => {
|
|
200
|
+
release();
|
|
201
|
+
options.releaseDeadline?.removeEventListener("abort", release);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
178
204
|
export async function runArgv(options) {
|
|
179
205
|
const signal = options.signal;
|
|
180
206
|
// Already-aborted signal: never spawn. The caller gets `aborted: true` with
|
|
@@ -210,14 +236,14 @@ export async function runArgv(options) {
|
|
|
210
236
|
// BUG-13: unlike every git call (routed through gitWithTimeout), a spawn
|
|
211
237
|
// with no deadline can block its caller forever on a hung install
|
|
212
238
|
// (registry/NFS stall).
|
|
213
|
-
const
|
|
239
|
+
const clearDeadline = armDeadline(options, () => {
|
|
214
240
|
timedOut = true;
|
|
215
241
|
// MEM-4: proc.kill() reaches only the direct child, orphaning postinstall
|
|
216
242
|
// grandchildren. killProcessGroup(pid, "SIGKILL") kills the whole group
|
|
217
243
|
// (negative pid), falling back to the single process on ESRCH.
|
|
218
244
|
killGroup();
|
|
219
245
|
stopReaders();
|
|
220
|
-
}
|
|
246
|
+
});
|
|
221
247
|
// US-001: an abort races the timeout. One listener, removed on every settle
|
|
222
248
|
// path so a never-aborted signal never leaks a registration (AC10).
|
|
223
249
|
const onAbort = () => {
|
|
@@ -233,57 +259,61 @@ export async function runArgv(options) {
|
|
|
233
259
|
// to settle (via reader.cancel()) after we kill the process group. US-001
|
|
234
260
|
// requires that settlement stay bounded even when a background process
|
|
235
261
|
// inherits the pipe and ignores the SIGKILL.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
try {
|
|
263
|
+
const stdoutPromise = drainToEof(proc.stdout, stdoutController.signal, options.onOutput?.bind(undefined, "stdout"));
|
|
264
|
+
const stderrPromise = drainToEof(proc.stderr, stderrController.signal, options.onOutput?.bind(undefined, "stderr"));
|
|
265
|
+
const exitCode = await proc.exited;
|
|
266
|
+
// Post-exit drain grace. After exit, the streams MUST close — they were
|
|
267
|
+
// owned by the shell, which is now dead. Background processes that
|
|
268
|
+
// inherited the pipe (the `&` case) keep it open: that's the orphan.
|
|
269
|
+
const graceMs = _argvExecDeps.drainGraceMs;
|
|
270
|
+
// Cleared on every settle path: a successful command whose readers close
|
|
271
|
+
// before the grace elapses would otherwise leave a pending timer whose
|
|
272
|
+
// callback fires 500ms later and resolves an orphaned promise. Under load
|
|
273
|
+
// the leaked registrations accumulate in the timer wheel for nothing.
|
|
274
|
+
let graceTimerId;
|
|
275
|
+
const gracePromise = new Promise((resolve) => {
|
|
276
|
+
graceTimerId = _argvExecDeps.setTimeout(() => resolve("expired"), graceMs);
|
|
277
|
+
});
|
|
278
|
+
const stdoutSettled = await Promise.race([
|
|
279
|
+
stdoutPromise,
|
|
280
|
+
gracePromise.then(() => "expired"),
|
|
281
|
+
]);
|
|
282
|
+
const stderrSettled = await Promise.race([
|
|
283
|
+
stderrPromise,
|
|
284
|
+
gracePromise.then(() => "expired"),
|
|
285
|
+
]);
|
|
286
|
+
if (graceTimerId !== undefined)
|
|
287
|
+
_argvExecDeps.clearTimeout(graceTimerId);
|
|
288
|
+
const stdoutClosed = stdoutSettled !== "expired";
|
|
289
|
+
const stderrClosed = stderrSettled !== "expired";
|
|
290
|
+
if (!stdoutClosed || !stderrClosed) {
|
|
291
|
+
orphansKilled = true;
|
|
292
|
+
killGroup();
|
|
293
|
+
// The OS should close the pipes once the group dies; race that against
|
|
294
|
+
// a small post-kill budget. If a holdout somehow ignores SIGKILL or is
|
|
295
|
+
// not yet reaped, abort the matching reader controller — that drives
|
|
296
|
+
// `reader.cancel()` in drainToEof, which forces the pending `read()` to
|
|
297
|
+
// resolve with `done: true` and bounds settlement.
|
|
298
|
+
if (!stdoutClosed)
|
|
299
|
+
stdoutController.abort();
|
|
300
|
+
if (!stderrClosed)
|
|
301
|
+
stderrController.abort();
|
|
302
|
+
}
|
|
303
|
+
// Always await the readers to harvest whatever they captured.
|
|
304
|
+
const stdoutFinal = stdoutClosed ? stdoutSettled : await stdoutPromise;
|
|
305
|
+
const stderrFinal = stderrClosed ? stderrSettled : await stderrPromise;
|
|
306
|
+
return {
|
|
307
|
+
exitCode,
|
|
308
|
+
stdout: stdoutFinal.text,
|
|
309
|
+
stderr: stderrFinal.text,
|
|
310
|
+
timedOut,
|
|
311
|
+
aborted,
|
|
312
|
+
orphansKilled,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
clearDeadline();
|
|
317
|
+
signal?.removeEventListener("abort", onAbort);
|
|
275
318
|
}
|
|
276
|
-
// Always await the readers to harvest whatever they captured.
|
|
277
|
-
const stdoutFinal = stdoutClosed ? stdoutSettled : await stdoutPromise;
|
|
278
|
-
const stderrFinal = stderrClosed ? stderrSettled : await stderrPromise;
|
|
279
|
-
_argvExecDeps.clearTimeout(timerId);
|
|
280
|
-
signal?.removeEventListener("abort", onAbort);
|
|
281
|
-
return {
|
|
282
|
-
exitCode,
|
|
283
|
-
stdout: stdoutFinal.text,
|
|
284
|
-
stderr: stderrFinal.text,
|
|
285
|
-
timedOut,
|
|
286
|
-
aborted,
|
|
287
|
-
orphansKilled,
|
|
288
|
-
};
|
|
289
319
|
}
|
package/dist/sandbox/launcher.js
CHANGED
|
@@ -66,6 +66,8 @@ async function runUnwrapped(req, sandbox, env) {
|
|
|
66
66
|
stripEnvVars: [...req.stripEnvVars],
|
|
67
67
|
...(overlay !== undefined ? { env: overlay } : {}),
|
|
68
68
|
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
69
|
+
...(req.releaseDeadline !== undefined ? { releaseDeadline: req.releaseDeadline } : {}),
|
|
70
|
+
...(req.onOutput !== undefined ? { onOutput: req.onOutput } : {}),
|
|
69
71
|
});
|
|
70
72
|
return { ...result, executed: argv, sandbox };
|
|
71
73
|
}
|
|
@@ -102,6 +104,8 @@ async function runWrapped(req, backend, policy, env) {
|
|
|
102
104
|
stripEnvVars: [...req.stripEnvVars],
|
|
103
105
|
...(req.env !== undefined ? { env: req.env } : {}),
|
|
104
106
|
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
107
|
+
...(req.releaseDeadline !== undefined ? { releaseDeadline: req.releaseDeadline } : {}),
|
|
108
|
+
...(req.onOutput !== undefined ? { onOutput: req.onOutput } : {}),
|
|
105
109
|
});
|
|
106
110
|
const denied = result.exitCode !== 0 && LIKELY_SANDBOX_DENIAL.test(result.stderr);
|
|
107
111
|
const violations = backend.annotate(commandId, result.stderr);
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -116,6 +116,10 @@ export interface LaunchRequest {
|
|
|
116
116
|
readonly unwrapped?: true;
|
|
117
117
|
/** Aborts the run: forwarded to runArgv, which SIGKILLs the process group. US-001. */
|
|
118
118
|
readonly signal?: AbortSignal;
|
|
119
|
+
/** Releases the deadline without cancelling the run. Forwarded to runArgv. */
|
|
120
|
+
readonly releaseDeadline?: AbortSignal;
|
|
121
|
+
/** Best-effort per-chunk observer, forwarded before bounded output capture. */
|
|
122
|
+
readonly onOutput?: (stream: "stdout" | "stderr", chunk: Uint8Array) => void;
|
|
119
123
|
}
|
|
120
124
|
export interface LaunchResult extends ArgvExecResult {
|
|
121
125
|
/** The logical argv (what the tool asked to run), not the sandbox wrapper argv. */
|
|
@@ -7,6 +7,7 @@ import type { CommandInterceptor } from "#src/command-interceptor/index";
|
|
|
7
7
|
import type { CommandShadow } from "#src/command-safety/types";
|
|
8
8
|
import type { TokenUsage } from "#src/cost/standard-types";
|
|
9
9
|
import type { TranscriptStore } from "#src/native/session/transcript-types";
|
|
10
|
+
import type { BackgroundCommandPort } from "#src/tools/bash-background";
|
|
10
11
|
import type { ProtectedPathsPolicy } from "#src/tools/protected-paths";
|
|
11
12
|
import type { BackendInfo, SessionBackend } from "./session-backend.ts";
|
|
12
13
|
import type { JSONSchema } from "./tool-descriptor.ts";
|
|
@@ -45,6 +46,8 @@ export interface AgentSessionHostPorts {
|
|
|
45
46
|
readonly protectedPaths?: ProtectedPathsPolicy;
|
|
46
47
|
readonly commandInterceptor?: CommandInterceptor;
|
|
47
48
|
readonly commandShadow?: CommandShadow;
|
|
49
|
+
/** Opt in to host-owned background Bash commands. */
|
|
50
|
+
readonly backgroundCommands?: BackgroundCommandPort;
|
|
48
51
|
}
|
|
49
52
|
export interface CreateAgentSessionOptions {
|
|
50
53
|
/** The driver: `nativeBackend({ model })`, or an ACP backend. Native-only options live on the backend. */
|
|
@@ -45,6 +45,9 @@ const OptionsSchema = z.strictObject({
|
|
|
45
45
|
commandShadow: z
|
|
46
46
|
.custom(hasMethods(["observe", "settle", "drain"]), "must implement observe, settle and drain")
|
|
47
47
|
.optional(),
|
|
48
|
+
backgroundCommands: z
|
|
49
|
+
.custom(hasMethods(["adopt", "foreground"]), "must implement adopt and foreground")
|
|
50
|
+
.optional(),
|
|
48
51
|
commandInterceptor: z.custom(hasMethods(["intercept"]), "must implement intercept").optional(),
|
|
49
52
|
})
|
|
50
53
|
.optional(),
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* with built-in and embedder tools, the NativeSessionAdapter, loop handlers
|
|
5
5
|
* and the resume model check.
|
|
6
6
|
*/
|
|
7
|
+
import { backgroundToolOptions } from "#src/coding-tools/coding-tool-extras";
|
|
7
8
|
import { getLogger } from "#src/infra/agent-logger";
|
|
8
9
|
import { errorMessage } from "#src/infra/errors";
|
|
9
10
|
import { DEFAULT_SPIN_BREAKER_SETTINGS } from "#src/infra/spin-breaker/index";
|
|
@@ -85,6 +86,7 @@ async function openNative(resolved, ctx) {
|
|
|
85
86
|
askOnUnsandboxed: posture.askOnUnsandboxed,
|
|
86
87
|
askDiscoveredTools: posture.askDiscoveredTools,
|
|
87
88
|
commandShadow: raw.hostPorts?.commandShadow,
|
|
89
|
+
...backgroundToolOptions(raw.hostPorts),
|
|
88
90
|
// US-003: the profile's validated opt-in, so an interactive "full" session
|
|
89
91
|
// can offer Bash's per-call escape without turning the sandbox off.
|
|
90
92
|
...(allowUnsandboxedCommands ? { unsandboxedCommands: true } : {}),
|
|
@@ -5,6 +5,7 @@ import type { BashApprovalMode } from "#src/config/bash-approval";
|
|
|
5
5
|
import { type SandboxConfig } from "#src/config/schemas-sandbox";
|
|
6
6
|
import type { AskResolver } from "#src/permissions/index";
|
|
7
7
|
import type { CommandLauncher, SandboxState } from "#src/sandbox/index";
|
|
8
|
+
import type { BackgroundCommandPort } from "#src/tools/bash-background";
|
|
8
9
|
import type { ProtectedPathsPolicy } from "#src/tools/protected-paths";
|
|
9
10
|
import type { ResolvedProviderTools } from "#src/tools/provider-advertise";
|
|
10
11
|
import type { CodingToolName, ToolGrant } from "#src/tools/types";
|
|
@@ -72,6 +73,7 @@ export interface SessionToolSupportArgs {
|
|
|
72
73
|
* defaulting to off, so every existing caller keeps today's tool surface.
|
|
73
74
|
*/
|
|
74
75
|
readonly unsandboxedCommands?: boolean;
|
|
76
|
+
readonly backgroundCommands?: BackgroundCommandPort;
|
|
75
77
|
readonly providerTools?: ResolvedProviderTools;
|
|
76
78
|
/**
|
|
77
79
|
* Narrow the profile's built-in tools to these names; the universal scratchpad
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* buildCodingToolSupport takes, picks the embedder default protected-paths
|
|
5
5
|
* policy, and enforces the sandbox floor for "full".
|
|
6
6
|
*/
|
|
7
|
-
import { unsandboxedToolOptions } from "#src/coding-tools/coding-tool-extras";
|
|
7
|
+
import { backgroundToolOptions, unsandboxedToolOptions } from "#src/coding-tools/coding-tool-extras";
|
|
8
8
|
import { resolveSessionSandbox } from "#src/coding-tools/coding-tool-sandbox";
|
|
9
9
|
import { buildCodingToolSupport } from "#src/coding-tools/coding-tool-support";
|
|
10
10
|
import { UNIVERSAL_CODING_TOOLS } from "#src/coding-tools/universal-coding-tools";
|
|
@@ -148,6 +148,7 @@ export function buildSessionToolSupport(args) {
|
|
|
148
148
|
...(args.interceptor !== undefined ? { interceptor: args.interceptor } : {}),
|
|
149
149
|
...(args.launcher !== undefined ? { launcher: args.launcher } : {}),
|
|
150
150
|
...unsandboxedToolOptions(args),
|
|
151
|
+
...backgroundToolOptions(args),
|
|
151
152
|
...postureToolOptions(args),
|
|
152
153
|
});
|
|
153
154
|
// Unreachable while the scratchpad trio is always declared and granted; kept for the type.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { RunArgvOptions } from "#src/internal/argv-exec";
|
|
2
|
+
export type BackgroundStream = "stdout" | "stderr";
|
|
3
|
+
export interface BackgroundCommandEnd {
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
readonly timedOut: boolean;
|
|
6
|
+
readonly aborted: boolean;
|
|
7
|
+
/** Present when the launch itself failed. */
|
|
8
|
+
readonly launchError?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface BackgroundCommandHandle {
|
|
11
|
+
readonly command: string;
|
|
12
|
+
readonly description?: string;
|
|
13
|
+
readonly origin: "agent" | "moved";
|
|
14
|
+
/** Always resolves, including on launch failure. */
|
|
15
|
+
readonly ended: Promise<BackgroundCommandEnd>;
|
|
16
|
+
/** Immediately aborts the launch, SIGKILLing its process group. */
|
|
17
|
+
stop(): void;
|
|
18
|
+
}
|
|
19
|
+
export interface BackgroundCommandSink {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
append(stream: BackgroundStream, chunk: Uint8Array): void;
|
|
22
|
+
}
|
|
23
|
+
export interface ForegroundCommand {
|
|
24
|
+
readonly command: string;
|
|
25
|
+
moveToBackground(): void;
|
|
26
|
+
}
|
|
27
|
+
export interface BackgroundCommandPort {
|
|
28
|
+
adopt(handle: BackgroundCommandHandle): BackgroundCommandSink;
|
|
29
|
+
/** Register a foreground call; the returned function unregisters it. */
|
|
30
|
+
foreground(call: ForegroundCommand): () => void;
|
|
31
|
+
}
|
|
32
|
+
export type BackgroundLaunchOptions = Pick<RunArgvOptions, "signal" | "releaseDeadline" | "onOutput">;
|
|
33
|
+
/** The newest raw output retained while a foreground call can still be moved. */
|
|
34
|
+
export declare const FOREGROUND_REPLAY_BYTES: number;
|
|
35
|
+
type BackgroundResult = {
|
|
36
|
+
content: string;
|
|
37
|
+
isError: false;
|
|
38
|
+
};
|
|
39
|
+
interface LaunchResult {
|
|
40
|
+
readonly launched: {
|
|
41
|
+
readonly exitCode: number;
|
|
42
|
+
readonly timedOut: boolean;
|
|
43
|
+
readonly aborted?: boolean;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
interface BashExecutionRequest<T extends LaunchResult> {
|
|
47
|
+
readonly port?: BackgroundCommandPort;
|
|
48
|
+
readonly runInBackground: unknown;
|
|
49
|
+
readonly command: string;
|
|
50
|
+
readonly description: unknown;
|
|
51
|
+
readonly signal?: AbortSignal;
|
|
52
|
+
readonly launch: (options?: BackgroundLaunchOptions, isForeground?: () => boolean) => Promise<T>;
|
|
53
|
+
}
|
|
54
|
+
/** Keep framing in Bash, but keep all ownership and early-return logic here. */
|
|
55
|
+
export declare function runBashWithBackground<T extends LaunchResult>(request: BashExecutionRequest<T>): Promise<T | BackgroundResult>;
|
|
56
|
+
export declare function withBackgroundSentence(description: string, port?: BackgroundCommandPort): string;
|
|
57
|
+
/** Adopt before launching so even the first output chunk has a sink. */
|
|
58
|
+
export declare function startBashBackground(request: {
|
|
59
|
+
readonly port?: BackgroundCommandPort;
|
|
60
|
+
readonly runInBackground: unknown;
|
|
61
|
+
readonly command: string;
|
|
62
|
+
readonly description: unknown;
|
|
63
|
+
readonly launch: (options: BackgroundLaunchOptions) => Promise<{
|
|
64
|
+
readonly launched: {
|
|
65
|
+
readonly exitCode: number;
|
|
66
|
+
readonly timedOut: boolean;
|
|
67
|
+
readonly aborted?: boolean;
|
|
68
|
+
};
|
|
69
|
+
}>;
|
|
70
|
+
}): {
|
|
71
|
+
content: string;
|
|
72
|
+
isError: false;
|
|
73
|
+
} | undefined;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { errorMessage } from "#src/infra/errors";
|
|
2
|
+
/** The newest raw output retained while a foreground call can still be moved. */
|
|
3
|
+
export const FOREGROUND_REPLAY_BYTES = 1024 * 1024;
|
|
4
|
+
/** Keep framing in Bash, but keep all ownership and early-return logic here. */
|
|
5
|
+
export async function runBashWithBackground(request) {
|
|
6
|
+
const background = startBashBackground(request);
|
|
7
|
+
if (background !== undefined)
|
|
8
|
+
return background;
|
|
9
|
+
if (request.port === undefined)
|
|
10
|
+
return request.launch();
|
|
11
|
+
return runMovableForeground({ ...request, port: request.port });
|
|
12
|
+
}
|
|
13
|
+
function outputReplay() {
|
|
14
|
+
let chunks = [];
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
let sink;
|
|
17
|
+
let replaying = false;
|
|
18
|
+
return {
|
|
19
|
+
append(stream, chunk) {
|
|
20
|
+
if (sink !== undefined)
|
|
21
|
+
return sink.append(stream, chunk);
|
|
22
|
+
if (chunk.byteLength === 0)
|
|
23
|
+
return;
|
|
24
|
+
const tail = replaying ? chunk : chunk.subarray(-FOREGROUND_REPLAY_BYTES);
|
|
25
|
+
// Copy even Buffer inputs: Buffer.slice retains the original backing allocation.
|
|
26
|
+
chunks.push({ stream, chunk: Uint8Array.from(tail) });
|
|
27
|
+
bytes += tail.byteLength;
|
|
28
|
+
while (!replaying && bytes > FOREGROUND_REPLAY_BYTES) {
|
|
29
|
+
const first = chunks[0];
|
|
30
|
+
if (first === undefined)
|
|
31
|
+
break;
|
|
32
|
+
const dropped = Math.min(first.chunk.byteLength, bytes - FOREGROUND_REPLAY_BYTES);
|
|
33
|
+
bytes -= dropped;
|
|
34
|
+
if (dropped === first.chunk.byteLength)
|
|
35
|
+
chunks.shift();
|
|
36
|
+
else
|
|
37
|
+
first.chunk = first.chunk.slice(dropped);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
replay(target) {
|
|
41
|
+
// Drain before switching to live forwarding, including any reentrant appends.
|
|
42
|
+
replaying = true;
|
|
43
|
+
for (const item of chunks)
|
|
44
|
+
target.append(item.stream, item.chunk);
|
|
45
|
+
chunks = [];
|
|
46
|
+
bytes = 0;
|
|
47
|
+
sink = target;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function commandEnd(result) {
|
|
52
|
+
return {
|
|
53
|
+
exitCode: result.launched.exitCode,
|
|
54
|
+
timedOut: result.launched.timedOut,
|
|
55
|
+
aborted: result.launched.aborted === true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function failedEnd(error) {
|
|
59
|
+
return { exitCode: -1, timedOut: false, aborted: false, launchError: errorMessage(error) };
|
|
60
|
+
}
|
|
61
|
+
function foregroundControl(request) {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const releaseDeadline = new AbortController();
|
|
64
|
+
const released = Promise.withResolvers();
|
|
65
|
+
const ended = Promise.withResolvers();
|
|
66
|
+
const output = outputReplay();
|
|
67
|
+
let settled = false;
|
|
68
|
+
let moved = false;
|
|
69
|
+
const cancel = () => controller.abort();
|
|
70
|
+
const detach = () => request.signal?.removeEventListener("abort", cancel);
|
|
71
|
+
request.signal?.addEventListener("abort", cancel, { once: true });
|
|
72
|
+
if (request.signal?.aborted === true)
|
|
73
|
+
cancel();
|
|
74
|
+
const call = {
|
|
75
|
+
command: request.command,
|
|
76
|
+
moveToBackground() {
|
|
77
|
+
if (settled || moved)
|
|
78
|
+
return;
|
|
79
|
+
moved = true;
|
|
80
|
+
releaseDeadline.abort();
|
|
81
|
+
detach();
|
|
82
|
+
const sink = request.port.adopt({
|
|
83
|
+
command: request.command,
|
|
84
|
+
...(typeof request.description === "string" ? { description: request.description } : {}),
|
|
85
|
+
origin: "moved",
|
|
86
|
+
ended: ended.promise,
|
|
87
|
+
stop: cancel,
|
|
88
|
+
});
|
|
89
|
+
output.replay(sink);
|
|
90
|
+
released.resolve({ content: `moved to the background as ${sink.id}`, isError: false });
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
call,
|
|
95
|
+
released: released.promise,
|
|
96
|
+
options: { signal: controller.signal, releaseDeadline: releaseDeadline.signal, onOutput: output.append },
|
|
97
|
+
isForeground: () => !moved,
|
|
98
|
+
finish(end) {
|
|
99
|
+
settled = true;
|
|
100
|
+
ended.resolve(end);
|
|
101
|
+
},
|
|
102
|
+
detach,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function runMovableForeground(request) {
|
|
106
|
+
const control = foregroundControl(request);
|
|
107
|
+
const unregister = request.port.foreground(control.call);
|
|
108
|
+
const launch = Promise.resolve().then(() => request.launch(control.options, control.isForeground));
|
|
109
|
+
// Observe both outcomes even after the race releases the foreground tool result.
|
|
110
|
+
void launch.then((result) => control.finish(commandEnd(result)), (error) => control.finish(failedEnd(error)));
|
|
111
|
+
try {
|
|
112
|
+
return await Promise.race([launch, control.released]);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
control.detach();
|
|
116
|
+
unregister();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function withBackgroundSentence(description, port) {
|
|
120
|
+
if (port === undefined)
|
|
121
|
+
return description;
|
|
122
|
+
const separator = description.endsWith(" ") ? "" : " ";
|
|
123
|
+
return `${description}${separator}Set "runInBackground": true to return at once with a task id; a notice arrives when the command ends.`;
|
|
124
|
+
}
|
|
125
|
+
/** Adopt before launching so even the first output chunk has a sink. */
|
|
126
|
+
export function startBashBackground(request) {
|
|
127
|
+
if (request.port === undefined || request.runInBackground !== true)
|
|
128
|
+
return undefined;
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const completion = Promise.withResolvers();
|
|
131
|
+
const sink = request.port.adopt({
|
|
132
|
+
command: request.command,
|
|
133
|
+
...(typeof request.description === "string" ? { description: request.description } : {}),
|
|
134
|
+
origin: "agent",
|
|
135
|
+
ended: completion.promise,
|
|
136
|
+
stop: () => controller.abort(),
|
|
137
|
+
});
|
|
138
|
+
// The promise boundary also settles synchronous launch failures without rejection.
|
|
139
|
+
void Promise.resolve()
|
|
140
|
+
.then(() => request.launch({
|
|
141
|
+
signal: controller.signal,
|
|
142
|
+
releaseDeadline: AbortSignal.abort(),
|
|
143
|
+
onOutput: (stream, chunk) => sink.append(stream, chunk),
|
|
144
|
+
}))
|
|
145
|
+
.then(({ launched }) => completion.resolve({
|
|
146
|
+
exitCode: launched.exitCode,
|
|
147
|
+
timedOut: launched.timedOut,
|
|
148
|
+
aborted: launched.aborted === true,
|
|
149
|
+
}), (error) => completion.resolve({ exitCode: -1, timedOut: false, aborted: false, launchError: errorMessage(error) }));
|
|
150
|
+
return { content: `started in the background as ${sink.id}`, isError: false };
|
|
151
|
+
}
|
package/dist/tools/bash.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { runArgv } from "#src/internal/argv-exec";
|
|
23
23
|
import type { BashApprovalMode } from "../config/bash-approval.ts";
|
|
24
24
|
import { type CommandLauncher } from "../sandbox/index.ts";
|
|
25
|
+
import { type BackgroundCommandPort } from "./bash-background.ts";
|
|
25
26
|
import type { CodingTool } from "./registry.ts";
|
|
26
27
|
/**
|
|
27
28
|
* Deadline for a Bash spawn — the same ceiling the Exec branch uses
|
|
@@ -32,6 +33,7 @@ import type { CodingTool } from "./registry.ts";
|
|
|
32
33
|
export declare const BASH_TIMEOUT_MS = 300000;
|
|
33
34
|
export declare const DEFAULT_BASH_SHELL = "/bin/sh";
|
|
34
35
|
export interface BashToolOptions {
|
|
36
|
+
readonly background?: BackgroundCommandPort;
|
|
35
37
|
/** `quality.shell`. */
|
|
36
38
|
readonly shell?: string;
|
|
37
39
|
/** `quality.stripEnvVars` — secrets removed before the spawn. */
|
package/dist/tools/bash.js
CHANGED
|
@@ -24,6 +24,7 @@ import { agentOutputOverlay } from "#src/internal/agent-output-env";
|
|
|
24
24
|
import { runArgv } from "#src/internal/argv-exec";
|
|
25
25
|
import { formatExecBody } from "#src/internal/exec-framing";
|
|
26
26
|
import { rawBashRefusalReason, sandboxSentence, unsandboxedSentence } from "../sandbox/index.js";
|
|
27
|
+
import { runBashWithBackground, withBackgroundSentence, } from "./bash-background.js";
|
|
27
28
|
import { cutToByteCap, READ_CEILING } from "./truncate.js";
|
|
28
29
|
import { BASH_TOOL_NAME } from "./types.js";
|
|
29
30
|
/**
|
|
@@ -179,7 +180,7 @@ function bashToolDescription(shell, opts) {
|
|
|
179
180
|
* therefore never the string the policy judged, which is exactly why
|
|
180
181
|
* `interceptShell` re-validates the provider's answer rather than trusting it.
|
|
181
182
|
*/
|
|
182
|
-
async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, unwrapped, }) {
|
|
183
|
+
async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, unwrapped, backgroundLaunch, isForeground, }) {
|
|
183
184
|
// The run's interceptor arrives on the context (S1 spec port 7), immutable
|
|
184
185
|
// for this call, so the launch below and `postProcess` see the same object.
|
|
185
186
|
const interceptor = ctx.interceptor;
|
|
@@ -199,6 +200,7 @@ async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, unwrapp
|
|
|
199
200
|
stripEnvVars: [...(opts.stripEnvVars ?? [])],
|
|
200
201
|
...(agentOverlay !== undefined ? { env: agentOverlay } : {}),
|
|
201
202
|
...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
|
|
203
|
+
...backgroundLaunch,
|
|
202
204
|
};
|
|
203
205
|
const launched = opts.launcher !== undefined
|
|
204
206
|
? await opts.launcher.run({
|
|
@@ -210,8 +212,9 @@ async function launchIntercepted({ command, shell, opts, ctx, timeoutMs, unwrapp
|
|
|
210
212
|
...(unwrapped ? { unwrapped: true } : {}),
|
|
211
213
|
})
|
|
212
214
|
: { ...(await _bashToolDeps.runArgv({ argv: executed, ...common })), executed, sandbox: undefined };
|
|
213
|
-
if (!intercepted.rewritten)
|
|
215
|
+
if (!intercepted.rewritten || (backgroundLaunch !== undefined && isForeground?.() !== true)) {
|
|
214
216
|
return { launched, stdout: launched.stdout };
|
|
217
|
+
}
|
|
215
218
|
// The request describes the command the MODEL wrote — the same shape
|
|
216
219
|
// `interceptShell` sent — so post-processing stays keyed to the original,
|
|
217
220
|
// mirroring the Git site, which passes the original argv.
|
|
@@ -264,6 +267,7 @@ function bashToolInputSchema(opts) {
|
|
|
264
267
|
description: `Deadline in milliseconds (default and maximum ${BASH_TIMEOUT_MS}).`,
|
|
265
268
|
},
|
|
266
269
|
description: { type: "string", description: "One short line on what this command is for." },
|
|
270
|
+
...(opts.background !== undefined ? { runInBackground: { type: "boolean" } } : {}),
|
|
267
271
|
...(opts.unsandboxedCommands === true
|
|
268
272
|
? { unsandboxed: { type: "boolean" }, unsandboxedReason: { type: "string" } }
|
|
269
273
|
: {}),
|
|
@@ -279,7 +283,7 @@ export function createBashTool(opts = {}) {
|
|
|
279
283
|
// not a fault worth an operator's attention — the same reason RunCommand
|
|
280
284
|
// sets this.
|
|
281
285
|
routineErrors: true,
|
|
282
|
-
description: withUnsandboxedSentence(bashToolDescription(shell, opts), opts),
|
|
286
|
+
description: withBackgroundSentence(withUnsandboxedSentence(bashToolDescription(shell, opts), opts), opts.background),
|
|
283
287
|
inputSchema: bashToolInputSchema(opts),
|
|
284
288
|
// `commandField` is what routes this call to the Bash branch of the policy
|
|
285
289
|
// (src/tools/policy-bash.ts). No pathFields: the paths are inside the
|
|
@@ -301,7 +305,18 @@ export function createBashTool(opts = {}) {
|
|
|
301
305
|
// here: whether the argument is ADVERTISED is the option's business, and what a
|
|
302
306
|
// given call asked for is the call's.
|
|
303
307
|
const unwrapped = input.unsandboxed === true;
|
|
304
|
-
const
|
|
308
|
+
const request = { command, shell, opts, ctx, timeoutMs, unwrapped };
|
|
309
|
+
const result = await runBashWithBackground({
|
|
310
|
+
port: opts.background,
|
|
311
|
+
runInBackground: input.runInBackground,
|
|
312
|
+
command,
|
|
313
|
+
description: input.description,
|
|
314
|
+
signal: ctx.signal,
|
|
315
|
+
launch: (backgroundLaunch, isForeground) => launchIntercepted({ ...request, backgroundLaunch, isForeground }),
|
|
316
|
+
});
|
|
317
|
+
if ("content" in result)
|
|
318
|
+
return result;
|
|
319
|
+
const { launched, stdout } = result;
|
|
305
320
|
// Same framing ladder as RunCommand (Exec) via the shared
|
|
306
321
|
// `formatExecBody`: an aborted call opens with the cancellation line
|
|
307
322
|
// and surfaces whatever partial output the readers captured before the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax-agent",
|
|
3
|
-
"version": "0.91.7-canary.
|
|
3
|
+
"version": "0.91.7-canary.8",
|
|
4
4
|
"description": "nax's native coding agent: session contract, native loop, tools, permissions, sandbox, command-safety.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "William Khoo",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
50
|
-
"@nathapp/nax-ai": "0.91.7-canary.
|
|
50
|
+
"@nathapp/nax-ai": "0.91.7-canary.8",
|
|
51
51
|
"zod": "^4.3.6"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|