shellTool function
AgentTool
shellTool(
- ExecutionEnv env, {
- ShellJobRegistry? jobs,
- Duration retryBackoff = _bashRetryBackoff,
- PasswordPromptCallback? onPasswordPrompt,
- Duration passwordQuiet = _bashPasswordQuiet,
- RedactionConfig? redactionConfig,
- Set<
String> approvedSecretLiterals()?,
Creates the bash tool: executes a shell command via ExecutionEnv.exec
and returns stdout followed by stderr, truncated to the last
defaultToolMaxLines lines / defaultToolMaxBytes bytes. A non-zero
exit code, timeout, or abort throws (the loop turns it into an error
tool result, pi semantics).
With jobs (and an environment implementing BackgroundShell) the tool
gains two background behaviors:
background: truestarts the command detached and returns immediately with the job id; completion is reported back as a follow-up message.- Foreground runs still block, but a user steering message mid-run (the loop's soft-yield token, see currentYieldToken) moves the command into a background job WITHOUT killing it: the tool call answers with the job id and a partial-output tail, and the user message is delivered at the next step boundary.
Implementation
AgentTool shellTool(
ExecutionEnv env, {
ShellJobRegistry? jobs,
Duration retryBackoff = _bashRetryBackoff,
PasswordPromptCallback? onPasswordPrompt,
Duration passwordQuiet = _bashPasswordQuiet,
/// The host's resolved redaction config for the shape interceptor
/// (issue #1408 AC3, review 5456649624): the same `redact:` section
/// steers command rewriting and result/job-log masking. Null = the
/// default config (vendor shapes on) for direct tool users; the CLI
/// passes its boot-resolved config (disabled when redaction is off).
RedactionConfig? redactionConfig,
/// Live snapshot of the values the host registered as secrets
/// (`request_secret`, preconfig keys): those literals are EXEMPT from
/// command rewriting so an approved value still materializes while the
/// pipeline masks it in transcripts (review 5456649624).
Set<String> Function()? approvedSecretLiterals,
}) {
return AgentTool(
name: bashToolName,
label: 'bash',
tier: ApprovalTier.exec,
description:
'Execute a bash command in the current working directory. Returns '
'stdout and stderr. Output is truncated to the last '
'$defaultToolMaxLines lines or ${defaultToolMaxBytes ~/ 1024}KB '
'(whichever is hit first). Optionally provide a timeout in seconds. '
'Timeouts you set are retried automatically '
'(${bashToolMaxRetries + 1} attempts total); retries appear as '
'[bash attempt N]. Environment-default timeouts fail once with the '
'partial output. '
'For long-running commands (builds, servers, watchers) pass '
'background: true — the command keeps running as a job, you get its '
'id immediately and are notified when it finishes; check progress '
'with bash_job. A foreground command that is still running when the '
'user sends a message is moved to a background job untouched (never '
'killed) so the user gets an answer right away.',
parameters: const {
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': 'The bash command to execute',
},
'timeout': {
'type': 'number',
'description': 'Timeout in seconds (optional, no default timeout)',
},
'background': {
'type': 'boolean',
'description':
'Run detached and return the job id immediately (optional, '
'default false). Use for long-running commands.',
},
'stdin': {
'type': 'string',
'description':
'Optional text written to the command\'s stdin right after '
'start (a password/passphrase the USER supplied via the ask '
'tool, or "y\\n"). Use when the command prompts for input, '
'e.g. ssh-add or sudo. Never invent secrets — ask first.',
},
},
'required': ['command'],
},
execute: (arguments, cancelToken, onUpdate) async {
cancelToken?.throwIfCancelled();
final command = arguments['command'] as String;
// Issue #1408 AC3: the shape interceptor rewrites recognized secret
// SHAPES (and only those — key-like-but-unmatched text, e.g. a
// filename the agent is creating, stays byte-identical) and the
// result notice names every rewrite so the agent sees what changed
// instead of discovering corruption by I/O error.
final rewrite = redactBashCommandSecretShapes(
command,
config: redactionConfig ?? const RedactionConfig(),
approvedLiterals: approvedSecretLiterals?.call() ?? const {},
);
final effectiveCommand = rewrite?.command ?? command;
final rewriteNotice = rewrite?.notice;
final timeoutArg = arguments['timeout'] as num?;
final timeout = timeoutArg == null ? null : _resolveTimeout(timeoutArg);
final background = arguments['background'] as bool? ?? false;
final stdinData = arguments['stdin'] as String?;
final canJob = jobs != null && jobs.isSupported;
// Issue #1349: a bare long foreground sleep is rejected at call
// validation — it parks the turn; background: true is the fix. An
// explicit `timeout` is a deliberate bail cap (the call unwinds at
// the cap, so steering and cancel reach the agent), not a park.
if (!background && timeout == null) {
final sleepDenial = foregroundSleepDenial(command, canJob: canJob);
if (sleepDenial != null) return ToolExecutionResult.text(sleepDenial);
}
if (background) {
if (!canJob) {
return ToolExecutionResult.text(
'Background execution is not supported in this environment — '
'run the command in the foreground with an explicit timeout.',
);
}
final entry = await jobs.start(
effectiveCommand,
options: ShellExecOptions(
cwd: env.cwd,
timeout: timeout,
// gh-1455: no caller cancel token — a background job outlives
// this call and is stopped through `bash_job stop` only; the
// registry firewalls stray tokens too.
stdinData: stdinData,
),
);
return ToolExecutionResult.text(
'${rewriteNotice == null ? '' : '$rewriteNotice\n'}'
'Started background job ${entry.id}.\n'
'Log: ${entry.logPath}\n'
'You will be notified when it finishes; check progress with '
'bash_job (action: status | output | stop).',
);
}
// Yield-aware foreground run: executed as a job from the start so a
// steering message can move it to the background mid-flight without
// killing the process. Settled before any yield → identical result to
// the classic inline path.
if (canJob && currentYieldToken() != null) {
return _shellViaJob(
env,
jobs,
effectiveCommand,
stdinData: stdinData,
timeout: timeout,
timeoutArg: timeoutArg,
cancelToken: cancelToken,
yieldToken: currentYieldToken()!,
onPasswordPrompt: onPasswordPrompt,
passwordQuiet: passwordQuiet,
rewriteNotice: rewriteNotice,
);
}
return _runForegroundBash(
env,
effectiveCommand,
timeout: timeout,
timeoutArg: timeoutArg,
cancelToken: cancelToken,
stdinData: stdinData,
retryBackoff: retryBackoff,
rewriteNotice: rewriteNotice,
);
},
);
}