Expand description
Shared wrapper-command set + peel logic (R155-T9).
“Wrappers” are commands like timeout, env, nohup that take a command
as their final positional argument. The approval gate and the FE both need
to look past them when reasoning about a user’s intent — granting
BashCmdPattern { cmd: "git", … } should match timeout 30 git push.
This module owns the canonical wrapper set and the peel routine in Rust.
The TS-side renderer (packages/yah/ui/src/components/forms/peelWrappers.ts)
keeps a parallel list — keep them in sync.
@yah:ticket(R155-T9, “bash_ast::wrappers shared static set; agent-tools + FE consume the same list to keep peel semantics in sync”)
@yah:at(2026-05-14T17:22:09Z)
@yah:assignee(agent:claude)
@yah:status(review)
@yah:phase(P2)
@yah:parent(R155)
@yah:handoff(“Promoted the wrapper-name list to a JSON sidecar at crates/yah/bash-ast/data/wrappers.json — single source of truth shared with the FE. Rust keeps the WRAPPERS &&str const for ergonomic use (and runtime no-alloc); exposed WRAPPERS_JSON via include_str! for downstream re-export. New wrappers_const_matches_json_sidecar test (cargo test -p bash-ast 37/37 green) parses the JSON and asserts equality with the const, so drift fails CI. FE peelWrappers.ts now builds WRAPPER_COMMANDS from the same JSON via a relative import (../../../../../../crates/yah/bash-ast/data/wrappers.json) — Bun’s tsc + builder both resolve it across the workspace boundary (resolveJsonModule:true). agent-tools already consumes bash_ast::wrappers::WRAPPERS and peel_command through R155-F6, so no change there. typecheck clean, bun test peelWrappers 12/12 green, bun run build:js succeeds, cargo check –workspace clean.”)
@yah:verify(“Edit crates/yah/bash-ast/data/wrappers.json (e.g. add \"perfwrap\"); cargo test -p bash-ast wrappers_const_matches_json_sidecar — fails until WRAPPERS in src/wrappers.rs is updated to match. Revert the JSON edit. FE: peelWrappers test still sees all 8 wrappers; sudo still rejected.”)
@arch:see(.yah/docs/working/W113-yah-bash-snapshot-and-permissions.md)
@yah:ticket(R295-F1, “ToolInvocation extraction + ToolRegistry skeleton (bash-ast + agent-tools)”)
@yah:assignee(agent:claude)
@yah:at(2026-05-23T00:01:16Z)
@yah:status(review)
@yah:parent(R295)
@arch:see(.yah/docs/working/W086-git-subcommand-gate.md)
@yah:next(“Generic helper exposing tool_invocation_of(&PeeledCommand, &ToolRegistry) -> Option<ToolInvocation> per design doc section 1. Lives in crates/yah/bash-ast/src/wrappers.rs (or sibling tool_invocation.rs if it grows). Walks the peeled command’s argv: matches primary basename against registry, peels each tool’s global flags per its ToolSchema, consumes subcommand chain up to ToolSchema.max_depth.”)
@yah:next(“Define the ToolSchema struct + ToolRegistry container in agent-tools (the consumer crate). ToolSchema fields: tool name, global_flags (bare / attached-value / =-value / eats-N), max_depth, policy table keyed on SubcommandPath, default_tier, leaf_flag_escalation rules. ApprovalTier enum has AutoAllow / StandardPrompt / TypedPrompt / HardDeny.”)
@yah:next(“Empty default registry at this stage — schemas land in F3. Tests use a mock schema for an imaginary tool to exercise the generic extractor end-to-end without committing to any policy table yet.”)
@yah:next(“Return ToolInvocation { tool, subcommand: Vec<&str>, rest, global_flags, leaf_flags }. The leaf_flags field lets F2 implement flag-based escalation (e.g. git reset --hard vs git reset).”)
@yah:verify(“cargo test -p bash-ast tool_invocation::tests # generic extraction (mock schema) covers: bare, dash-flag-with-attached-value, dash-dash-flag-equals, eats-N variant, absolute path, deepest chain the mock schema declares, unknown primary returns None, variable primary returns None.”)
@yah:verify(“cargo test -p agent-tools –lib tool_schema::tests # ToolSchema + ToolRegistry serde round-trip; loading TOML + matching default_tier on unknown subcommand.”)
@yah:handoff(“Shipped. (1) bash-ast/src/tool_invocation.rs (new): GlobalFlagSpec {Bare,TakesValue}, ToolSchemaInfo, ToolLookup trait, ToolInvocation<’a>, tool_invocation_of<’a>. Private helpers: tool_basename (strips path prefix), find_global_flag (exact + flag= prefix matching). 11 tests in bash-ast::tool_invocation::tests cover all verify items: bare invocation, dash-flag-with-attached-value, dash-dash-flag-equals, short flag TakesValue, bare global flag, deepest chain (max_depth=2), leaf flags after subcommand, absolute path, unknown primary returns None, variable primary returns None (via peel_command None), global flag stops at unknown flag. (2) agent-tools/src/tool_schema.rs (new): ApprovalTier {AutoAllow,StandardPrompt,TypedPrompt,HardDeny} with serde + max_with helper; LeafFlagRule; PolicyEntry; ToolSchema {tier_for (longest-prefix), apply_leaf_flags}; ToolRegistry (empty default, register/get/len/is_empty) + impl ToolLookup (case-insensitive lookup). 11 tests: json_round_trip, toml_round_trip, default_tier, known_subcommand, longest_prefix_matching, leaf_flag_escalation, tier_max_with, registry_lookup_case_insensitive, empty_registry_returns_none, register_and_get. (3) bash-ast/src/lib.rs: pub mod tool_invocation added. (4) agent-tools/src/lib.rs: pub mod tool_schema added. cargo test -p bash-ast: 68/68. cargo test -p agent-tools –lib tool_schema: 11/11. cargo check –workspace clean (pre-existing warnings only).”)
@yah:verify(“cargo test -p bash-ast tool_invocation (11/11)”)
@yah:verify(“cargo test -p agent-tools –lib tool_schema (11/11)”)
@yah:verify(“cargo check -p bash-ast && cargo check -p agent-tools”)
Structs§
- Peeled
Command - Result of peeling wrapper commands off a single simple bash command.
Constants§
- WRAPPERS
- Canonical wrapper command names.
- WRAPPERS_
JSON - Raw bytes of the canonical wrapper-name JSON sidecar (single source of truth shared with the FE). Exposed so consumers in other crates can re-publish it without re-reading the file at runtime.
Functions§
- peel_
command - Peel wrappers from a single simple-command program.
- peel_
simple_ command - Peel wrappers from a single
Commandnode (one pipeline stage / list operand). - peel_
simple_ command_ loose - Looser cousin of
peel_simple_commandthat ignores per-command redirects, leading variable assignments, and embedded subshells. Used bycrate’s downstream consumers (agent-tools::approval) when the containing rule has already opted into those shapes via flags likeallow_redirects— at that layer we still want to extract the primary command name so each pipeline stage can be matched.