/** * EXT-9 Tier-2 ergonomics: classify a shell command into a stable, human-readable * **prefix** (binary + N meaningful subcommands) so an approval the human grants once * (`git checkout main`) can be remembered as a pattern (`git checkout *`) and matched * against later flag-variants (`git checkout -b foo bar`) WITHOUT re-prompting. * * The arity table (which tokens past the binary are "meaningful subcommands" rather * than operands/flags) is a port of a subset of opencode's ~160-command table * (`opencode/packages/opencode/src/permission/arity.ts`). Binaries not in the table * default to arity 0 = just the binary. * * SECURITY — this is the anti-injection core. {@link classifyCommand} returns `null` * (fail-closed) whenever the command contains shell composition that could change the * target of the operation: separators (`;`, `&&`, `||`, `|`, `&`), newlines, command * substitution (`$(...)`, backticks), process substitution (`<(...)`/`>(...)`), or * redirections. Such commands NEVER auto-match an allow-list entry — they always go to * fresh human approval. This is what stops `git checkout x; rm -rf /` from matching an * approved `git checkout *`. * * EXT-55: "newlines" in that list was aspirational until this node. `normalizeCommand` folded * a line break to a SPACE, so the check below never saw one and `ls -la\nrm -rf /` classified * as the single command `ls`. The separator set now lives in ONE place * ({@link @gaunt-sloth/core!core/shell/normalize.COMMAND_SEPARATOR_CLASS | COMMAND_SEPARATOR_CLASS}) and a line break survives * normalization, so a multi-line command is categorically ambiguous — exactly like `;`. * * @module */ /** * Result of classifying a command for allow-list matching. */ export interface CommandClassification { /** * The meaningful command prefix — binary plus subcommands per the arity table, with * flags removed. This is the allow-list KEY: two commands with the same prefix are the * "same operation" for approval purposes (`git checkout main` and `git checkout -b x` * both → `git checkout`). */ prefix: string; /** * Human-facing display pattern, e.g. `git checkout *` (or `ls *`). The trailing `*` * signals "any args/flags". A prefix that already consumed the whole command (no extra * operands) still gets ` *` for a consistent, honest "future args allowed" affordance. */ pattern: string; } /** * Tokenize a shell command into argv, honoring single and double quotes. Quotes group * (and are stripped from) a token; backslash-escaping inside double quotes is collapsed * by the prior normalize step, so this tokenizer treats a residual `\` literally. * * This is a deliberately small tokenizer used ONLY for prefix detection — never for * execution. The original command string is what runs. * * Returns `null` if quoting is unbalanced (an open quote with no close), which is itself * a reason to refuse classification (ambiguous parse → fail-closed). */ export declare function tokenize(command: string): string[] | null; /** * Look up the arity (number of leading meaningful tokens) for an argv, using the * longest-matching-prefix rule against `ARITY`. Flag tokens (starting with `-`) are * dropped when forming the candidate prefixes so boolean flags like `git --no-pager * checkout` still resolve `git checkout`. Unknown binaries default to arity 0 → just the * binary. * * LIMITATION (intentional, fail-closed): we do NOT maintain a per-flag arity table, so an * arg-taking flag leaves its operand in the non-flag stream (e.g. `git -C . checkout` → * `git .`). That simply fails to match an approved `git checkout` and re-prompts — safe, by * design — rather than risking a mis-classification that mis-approves. * * Returns the list of meaningful tokens (binary + subcommands), flag tokens excluded. */ export declare function meaningfulPrefixTokens(argv: string[]): string[]; /** * Classify a command into a stable allow-list prefix + display pattern, or `null` when it * cannot be safely classified for matching (composition/redirection/substitution present, * empty, or unbalanced quotes). * * @param command Raw command string as the model proposed it. * @param normalize Normalizer to apply for the detection form (inject normalizeCommand). */ export declare function classifyCommand(command: string, normalize: (cmd: string) => string): CommandClassification | null;