Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,481 changes: 1,407 additions & 74 deletions crates/tui/src/command_safety.rs

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion crates/tui/src/tools/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,12 @@ fn enforce_tool_authority(
authority.owner
)));
}
// Per stage, like the execution seam's network guard: a compound
// or glob-bearing gh read reaches the network like a bare one.
let networked_read = input
.get("command")
.and_then(Value::as_str)
.is_some_and(crate::command_safety::is_github_readonly_command);
.is_some_and(crate::command_safety::command_reaches_github_readonly);
if networked_read && authority.network_access != Some(true) {
return Err(ToolError::permission_denied(format!(
"worker '{}' cannot use read-only GitHub CLI access: its machine-readable authority envelope does not grant network access",
Expand Down
3,940 changes: 3,731 additions & 209 deletions crates/tui/src/tools/shell.rs

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion crates/tui/src/tools/shell/guidance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,20 @@ pub(super) fn description() -> &'static str {
\"wait\" blocks for a background task until completion or timeout; \
\"interact\" sends stdin to a background task; \"cancel\" kills a background task. \
Pass wait=false for a nonblocking task snapshot. Foreground mode is for bounded \
commands; use background=true for work expected to take >5 seconds. \
commands; use background=true for work expected to take >5 seconds. Foreground \
commands without `interactive` or `combined_output` run without a \
terminal stdin — interactive programs and a bare `$SHELL` receive EOF and \
exit; `tty` always selects a background terminal task, and a foreground \
command with `combined_output` runs on a terminal lane that nothing \
writes to unless a session member interacts with the task, so a \
stdin-reading command waits until the watchdog ends it. A foreground \
run without `combined_output` may carry one inline stdin payload \
(delivered to the command, then end-of-file); a `combined_output` \
foreground run delivers the payload but its terminal lane stays \
open, so no end-of-file follows it. For multi-write input-driven \
workflows supply stdin on the background lane: \
use background=true plus \
action=\"interact\". \
Output is truncated per stream (~30KB: head/tail kept, middle summarized; \
see metadata flags).",
runtime_command_guidance()
Expand Down
12,870 changes: 9,774 additions & 3,096 deletions crates/tui/src/tools/shell/tests.rs

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions crates/tui/src/tools/subagent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16829,8 +16829,9 @@ impl SubAgentToolRegistry {
if !self.posture_permits_tool(name, Some(&input)) {
if self.allows_bounded_readonly_bash(name) {
return Err(anyhow!(
"[shell.readonly.command] Tool {name} input did not match the bounded read-only shell grammar for Fleet role `{role}`. Admitted: a single read-only command or a pipe of them — git status/log/diff/show/ls-files/blame/grep, git rev-parse (bounded read-only flags), git branch (bare/flag-only, --show-current, --list with patterns, attached values like --sort=-committerdate), git worktree list (flags only), ls, pwd, cat, head, tail, wc, stat, file, du, df, which, grep, rg, fd, find (no -delete/-exec/-execdir/-ok/-okdir/-fprintf/-fls/-fprint), sed -n <range>p, sort, uniq, cut, tr, comm, npm view. Not admitted: cd, &&, ;, redirects, $(...), env prefixes, interpreters (python3/node/...), sleep, test. Do not retry variations of a refused shape; use `read`/`file_search` instead, or an `implement`/`general` role for mutation or arbitrary execution.",
role = self.agent_type.as_str()
"[shell.readonly.command] Tool {name} input was refused by the bounded read-only shell gates — the admission grammar, or the execution seam that judges what the shell would actually resolve — for Fleet role `{role}`. Admitted families: git status/log/diff/show/ls-files/blame/grep (with a literal -C dir / --no-pager), ls, pwd, cat, head, tail, wc, which, stat, file, du, df, grep, rg, fd, read-only od dumps (-b -c -d -o -s -x -h -v, -A d|o|x|n|none, numeric -j/-N, glued `-An`/`-j8`/`-N4k` spellings), sort, uniq, cut, tr, comm, find without mutating primaries, sed -n '<range>p', npm view/show/info, gh issue/pr/release/repo/run/workflow reads, and `cd <one relative path>` (workspace-confined, must be a directory the shell can enter). Value options also admit glued numerics (`head -n5`, `tail -c3`, `git log -5`), and the text filters admit clustered short options (`cut -d, -f1`, `comm -12`, `sort -rn`). These combine into compound probes separated by `;`, `&&`, `||`, or `|` — but every segment of a sequence must individually be admitted (an empty segment refuses; at most {max_segments} segments). Refused shapes: a bare `&`, redirects (`<`, `>`), `$` or backtick expansions, subshells, interpreters (`python3 -c ...`), mutation commands (`rm`, `touch`, `sed -i`, ...), `cd` inside a pipeline, joined by `&&`/`||` as a later segment (it may not run at all, so the gate will not fold onto it), or with a wildcard, `=`-prefixed, or `..` operand; and, refused at the execution seam when the shell would resolve them differently than judged: recursive `**/` globs, a glob whose expansion leaves the workspace, any glob on a `gh` stage, a `-C` value carrying a wildcard, a tilde, or a leading `=` (the shell would expand it while the gate judges the literal spelling), and any `cd` outside the workspace. Do not retry variations of a refused command; pick a different admitted probe, or use an `implement`/`general` role for mutation or arbitrary execution.",
role = self.agent_type.as_str(),
max_segments = crate::command_safety::MAX_READONLY_SEQUENCE_SEGMENTS
));
}
return Err(anyhow!(
Expand Down Expand Up @@ -17306,11 +17307,15 @@ fn carries_network_url(input: &Value) -> bool {
/// written. It fails closed and names the posture, so the refusal reads as a
/// contract rather than a malfunction.
fn reject_network_reaching_input(name: &str, input: &Value) -> Result<()> {
// Judged per stage with the same predicate the execution seam's network
// guard uses, so `pwd; gh repo view` and `gh pr list --search '*'` are
// refused here exactly like a bare `gh repo view`; the whole-command
// parallel classifier would see the compound or the `*` and pass them.
let github_shell_read = matches!(name, "bash" | "Bash" | "exec_shell")
&& input
.get("command")
.and_then(Value::as_str)
.is_some_and(crate::command_safety::is_github_readonly_command);
.is_some_and(crate::command_safety::command_reaches_github_readonly);
if !github_shell_read && !carries_network_url(input) {
return Ok(());
}
Expand Down
Loading
Loading