/** * Shell-command decomposition for fine-grained permission checks. * * The permission gate authorizes on *tool name* only, so a denied surface * (`WebFetch`, `Read`, an MCP write) is trivially reached through * `bash -c "curl …"`. This module parses a shell command with the * `tree-sitter-bash` grammar (via `web-tree-sitter`) into a **structured * command model** — the shared foundation for the permission work: * * - #76 (this): check each command word as `ShellTool:#use`. * Derived from `ParsedShell.commands.map(c => c.name)`. * - #77 (argument scoping): per-binary extractors read `ShellCommand.args`. * - #78 (combinatory dataflow): edges are built from `ShellCommand.pipeline` * grouping and `ShellCommand.redirects`. * - #79 (env escalation): `ShellCommand.assignments` and redirect targets. * * Building the model once (one parse, one AST walk) keeps every follow-on * additive instead of re-walking the tree. * * **Why a grammar parser, not a regex/tokenizer.** A hand-rolled tokenizer is * vulnerable to *parser-differential attacks* — where the checker reads a * command differently than the shell that will run it. tree-sitter-bash gives * us the shell's own view of the command. * * **Failure postures (two, deliberately opposite):** * - `tooComplex` — parsed but unresolvable (dynamic command name, parse * error, dynamic `sh -c` target). Security signal: enforce denies. * - `parserUnavailable` — the runtime/grammar could not load. Availability * signal: the gate falls open. A broken build must never block shell use. * * **Known static-analysis limits (checks for them are #77/#78/#79):** command * names built at runtime, programs invoked as interpreter arguments * (`python -c "…"`, `find -exec`), and dynamic dataflow. The backstop is that * the decode/exec primitives themselves (`sh`, `bash`, `eval`, `base64`, …) are * ordinary checked words. */ /** A redirection attached to a command (`> file`, `< file`, `2>err`, `>/dev/tcp/…`). */ export interface ShellRedirect { /** Raw operator text (`>`, `>>`, `<`, `2>`, `&>`, …). */ operator: string; /** Literal redirect target (path / fd / device), or `null` when dynamic. */ target: string | null; /** `read` for `<`/`<<`/`<<<`, `write` for `>`/`>>`/`&>`. */ direction: "read" | "write"; } /** A NAME=value assignment (an `env`-style prefix on a command). */ export interface ShellAssignment { name: string; /** Literal value, or `null` when it contains an expansion. */ value: string | null; } /** One effective command invocation extracted from a shell command line. */ export interface ShellCommand { /** * Resolved program/builtin word, normalized to its basename. `null` when the * command name is dynamic/unresolvable (`$X`, `$(…)`), which also flips * {@link ParsedShell.tooComplex}. */ name: string | null; /** Raw text of each argument token, in order (for #77 argument scoping). */ args: string[]; /** `NAME=value` assignment prefixes on this command (for #79 env scoping). */ assignments: ShellAssignment[]; /** Redirects on this command / its enclosing statement (for #78/#79). */ redirects: ShellRedirect[]; /** * Pipeline group id. Commands joined by `|` share an id, so #78 can form the * pipe edges between them. Standalone commands each get a distinct id. */ pipeline: number; } /** Structured result of decomposing one shell command line. */ export interface ParsedShell { /** Every effective command invocation (including wrapper- and `sh -c`-nested). */ commands: ShellCommand[]; /** Something couldn't be resolved statically (dynamic name / parse error). */ tooComplex: boolean; /** The tree-sitter runtime/grammar failed to load — fail OPEN, don't block. */ parserUnavailable?: boolean; } /** Back-compat flat view used by the #76 gate: the deduped command words. */ export interface BashParseResult { words: string[]; tooComplex: boolean; parserUnavailable?: boolean; } /** * Decompose a shell command into a {@link ParsedShell} model. Never throws. * This is the shared foundation; see {@link extractBashCommandWords} for the * flat word list the #76 gate uses. */ export declare function extractShellCommands(command: string): Promise; /** * Flat view of {@link extractShellCommands} for the #76 gate: the deduped set * of command/builtin words the command will execute. Never throws. See * {@link BashParseResult} for the two failure postures. */ export declare function extractBashCommandWords(command: string): Promise;