Skip to main content

Module tier

Module tier 

Source
Expand description

Danger-tier classifier over the bash AST (R459-T1).

Sibling to crate::summary: BashShape answers what shape is this command?, this module answers how much does it cost if the user approves it by mistake? The output is a total function from Program to TierClassification — every parsed program resolves to exactly one DangerTier with a structured reason and an optional payload_url for inline content review of the arbitrary_exec tier (the curl|bash family).

Tier escalation is structural, not lexical. Per W184, the classifier asks each command two questions: (1) where does its code come from — agent-authored text the user can read, or fetched-mid-execution bytes piped to an interpreter? (2) what does it touch — read / scoped write / persistent state / irreversible? The bright line between destructive and arbitrary_exec is whether the command’s behavior can be determined from its text alone: a curl URL | sh AST sees bytes, not behavior, so it escalates regardless of what URL it names.

Outputs feed two downstream consumers: the friction-gesture renderer in the answer queue (which maps tier → gesture via camp settings) and the inline content reviewer (which fetches payload_url, hashes the bytes, and gates exec on hash match).

See .yah/docs/working/W184-approval-shapes-by-danger-tier.md.

@yah:ticket(R459-T1, “Tier classifier on bash-ast: BashShape → DangerTier with reason + payload_url”) @yah:assignee(agent:claude) @yah:at(2026-06-05T19:11:49Z) @yah:status(review) @yah:phase(P1) @yah:parent(R459) @arch:see(.yah/docs/working/W184-approval-shapes-by-danger-tier.md) @yah:handoff(“Shipped crates/yah/bash-ast/src/tier.rs (700 LOC + 47 tests). Public surface: DangerTier enum (Cosmetic/Read/ScopedWrite/CrossWrite/External/Destructive/ArbitraryExec) with Ord = severity; TierClassification {tier, reason, payload_url}; classify_tier(&Program) -> TierClassification (total function). Walks pipelines/lists/compounds taking max; Read minimum for bash. Detects: curl|wget piped to bash/sh/zsh/python/ruby/node/etc as ArbitraryExec with payload_url extracted; bash <(curl …) and sh -c "$(curl …)" via nested process/command-sub walk; node/python/ruby/perl/php -e/-c/-r with non-empty body; npx/uvx/pipx run of unpinned packages (pinned = @ or ==/>=/=); sudo/doas/eval. Destructive: rm/mv/cp/chmod/chown of dotfiles (~/.zshrc, ~/.ssh, …) or /etc /usr /bin /var /Library /System; crontab/launchctl/systemctl; apt/yum/dnf/pacman/brew install; rm -rf /. External: git push (force flag stays External — leaf-flag escalation is downstream R459-T2’s job), gh pr/issue/release create/merge/etc, curl -X POST/PUT/DELETE/PATCH, ssh/scp/rsync, kubectl delete/apply/etc, docker push. CrossWrite: git reset –hard, git rebase/merge/cherry-pick/revert, git checkout – path, git clean -f, find -delete, source/. of any file. ScopedWrite: rm of named paths, cargo/npm/git commit, unknown commands (conservative default). Read: ls/grep/rg/cat/echo/find (no -delete) and plain curl (no -X write method, no -O). Wrapper peel works via existing wrappers::peel_simple_command_loose — timeout/env/nohup transparent. Opaque-arg fallback: lenient cmd_name_text_lenient extracts primary even when args contain process-sub / cmd-sub / variable expansion so sudo/eval/interpreter+fetch escalation still fires; otherwise opaque-arg commands fall back to scoped(opaque args to X). Tests cover the full W184 verify matrix: curl|bash, node -e, npx unpinned, sudo, rm -rf, git push, grep, plain cargo build. 47 tier tests + 69 existing bash-ast tests = 116/116 green. cargo check -p bash-ast -p agent-tools clean (only pre-existing kg-daemon warnings). Known gaps deferred to follow-ups: chmod +x of freshly-downloaded paths needs cross-command context; 0.0.0.0 network-bind detection needs per-tool flag tables; protected-branch awareness needs config; npm/python install of arbitrary remote packages not yet escalated.”) @yah:verify(“cargo test -p bash-ast –lib tier # 47/47 green”) @yah:verify(“cargo test -p bash-ast # 116/116 green”) @yah:verify(“cargo check -p bash-ast -p agent-tools # clean (pre-existing kg-daemon warnings only)”) @yah:cleanup(“R459-T2 (static tier annotation per non-bash tool) is the natural downstream — it consumes the same DangerTier enum and adds per-MCP/native-tool tiers.”) @yah:cleanup(“Future leaf-flag escalation table (R459 follow-up): map git push –force, git push to protected branches, chmod +x of downloaded paths, network binds to 0.0.0.0 to higher tiers when policy data is available.”)

@yah:ticket(R479-T1, “sed -i / –in-place leaf-flag escalation to ScopedWrite”) @yah:at(2026-06-08T01:14:33Z) @yah:status(review) @yah:assignee(agent:claude) @yah:parent(R479) @yah:next(“In classify_by_program at tier.rs:591, split the read-allowlist arm: for basename == "sed", inspect args for any flag starting with -i (matches -i, -i.bak, -i’.orig’, –in-place, –in-place=.bak). On match return ScopedWrite with reason "sed -i edits files in place". Mirror the existing find -delete → CrossWrite escalation pattern at tier.rs:599-605.”) @yah:verify(“cargo test -p bash-ast –lib tier:: # 3 new tests: sed_without_i_is_read, sed_dash_i_is_scoped_write, sed_dash_i_with_backup_suffix_is_scoped_write (covers -i.bak and -i ‘.bak’), sed_long_in_place_is_scoped_write (covers –in-place and –in-place=.bak)”) @yah:handoff(“Shipped sed -i / –in-place leaf-flag escalation in crates/yah/bash-ast/src/tier.rs. Added sed_has_in_place() helper (line ~865) detecting -i, -i.bak, -i '.orig', --in-place, --in-place=.bak. Wired into the read-allowlist arm at classify_by_program(); on match returns ScopedWrite with reason sed -i edits files in place, mirroring the find -delete pattern. Added 4 tests: sed_without_i_is_read, sed_dash_i_is_scoped_write, sed_dash_i_with_backup_suffix_is_scoped_write (covers -i.bak attached + BSD -i ‘.orig’ separate), sed_long_in_place_is_scoped_write (covers –in-place and –in-place=.bak). 51/51 tier tests green.”) @yah:verify(“cargo test -p bash-ast –lib tier # 51/51 green”)

@yah:ticket(R479-T2, “awk script-content scan: system() and print > file escalate to ScopedWrite”) @yah:at(2026-06-08T01:14:46Z) @yah:status(review) @yah:assignee(agent:claude) @yah:parent(R479) @yah:next(“Split awk out of the read-allowlist arm at tier.rs:593-597 into its own arm. The script arg is the first non-flag positional (or the arg after -f / –file pointing at a script file — if -f, escalate conservatively to ScopedWrite since we can’t read the file at classify time).”) @yah:next(“For an inline script, substring-scan the literal text for: (a) the token system( (with optional whitespace before (), (b) a > or >> token followed by whitespace and a quoted string (> \"...\" or > '...'). On either match, escalate to ScopedWrite with a reason naming which pattern fired.”) @yah:next(“False-positive bias is intentional — if an awk program contains system( inside a string literal or > is used as numeric comparison followed by a quoted constant, we still escalate. The cost is one click, not a deny.”) @yah:verify(“cargo test -p bash-ast –lib tier:: # awk_print_pattern_is_read (no system / no >file), awk_with_system_call_is_scoped_write, awk_with_print_redirect_is_scoped_write, awk_numeric_comparison_stays_read ('$1 > 10 {print}'), awk_with_dash_f_script_file_escalates (conservative).”) @yah:gotcha(“Awk’s > is overloaded: comparison (if (x > y)) vs file redirection (print x > \"out\"). The whitespace-then-quoted-string heuristic catches the redirection case while letting bare numeric comparisons pass. Don’t try to parse awk — the heuristic is enough.”) @yah:handoff(“Shipped awk script-content scan in crates/yah/bash-ast/src/tier.rs. New classify_awk(args) helper: skips -F/-v flags + their values, detects -f/–file/-fFILE/–file=FILE → ScopedWrite (conservative, can’t read script), then substring-scans the first non-flag positional. awk_script_writes_files: byte-scan for system followed by optional whitespace + ( → ‘awk script calls system()’; and > or >> followed by whitespace + quoted string (\" or ') → ‘awk script redirects output to a file’. Numeric comparisons like $1 > 10 stay Read because no quoted string follows. Wired into the read-allowlist arm alongside sed -i and find -delete. 5 new tests: awk_print_pattern_is_read, awk_with_system_call_is_scoped_write, awk_with_print_redirect_is_scoped_write, awk_numeric_comparison_stays_read, awk_with_dash_f_script_file_escalates. 56/56 tier tests green.”) @yah:verify(“cargo test -p bash-ast –lib tier # 56/56 green”)

Structs§

TierClassification
Classification result. tier is the load-bearing field; reason and payload_url exist so the renderer can show the operator why a command escalated, and so the inline content reviewer can fetch the payload it needs the user to read before exec.

Enums§

DangerTier
Operation danger tier — property of what the agent is about to do, independent of who detected the request or what UX renders the approval.

Functions§

classify_tier
Classify program into a TierClassification. Total function — every parsed program returns a result. Empty programs and bare assignments resolve to Read.