/** * `!command` body preprocessing. * * Skills may embed shell-command blocks that the SDK runs at body-load * time and splices the output into the rendered body before the model * sees it. The feature is **disabled by default** because the OSS skill * survey (29 real skills, 0 used `!command`) suggests it's a rarely- * needed compatibility shim, not a hot-path tool. * * Two block forms are recognised, mirroring Claude Code's surface: * * - Inline: a single-backticked command line whose body starts with * `!`, e.g. `` `!git log --oneline -5` ``. * - Fenced: a triple-backtick block tagged with `!`, e.g. * ```` ```!\nbash -c 'pwd'\n``` ````. * * Three modes, gated through {@link SkillsPreprocessingConfig.mode}: * * - `'disabled'` (default) — every block is left literal. No execution, * no telemetry. Cheapest path; ships zero attack surface. * - `'allowlisted'` — every block's first shell-tokenised argv must * match one of {@link SkillsPreprocessingConfig.allow}. Non-matches * are replaced with a disable marker + `preprocess_blocked` event. * Matches execute through the consumer's bash (or powershell, gated * by env-var). * - `'full'` — every block executes. Reserved for trusted environments; * third-party skills always bypass this mode regardless of opt-in. * * Tier gate runs *before* the mode gate: third-party skills never * execute, ever, regardless of `mode`. The trust derivation is the same * value `SkillsManager.isTrusted` returns. * * Substitution order (per `skills-trust.md` §4): * * 1. `${QODO_SKILL_DIR}` substituted in the entire body text before * block detection. Authors compose commands like * `` `!cat ${QODO_SKILL_DIR}/references/style.md` `` so the * command's `cwd` (and any path arguments) resolve correctly even * when the skill is loaded from a non-default source. * 2. `!command` blocks executed; output spliced in place. * 3. `$ARGUMENTS` substitution (separate path) runs after * preprocessing — output is treated as inert text, so a command * whose stdout contains `$ARGUMENTS` will see it substituted on * the final pass. * * Threat model: * * - Skill body content is untrusted even from a `trusted` tier — a * skill author may have copied a recipe from the wider OSS * ecosystem without realising it embeds a `!command`. The * allowlist is the only mechanism that can be safely relied on for * blast-radius control; `full` mode means "I trust every skill in * this catalog". * - `!command` blocks run through the consumer's shell, which gives * them pipes / redirects / shell builtins. The allowlist gates the * first command name only; downstream pipe tokens are not screened. * Operators wiring `mode: 'full'` accept that trade-off. The right * answer is to either disallow shell metacharacters in the command * line entirely (rejected) or trust the operator's allowlist * (accepted, with this caveat documented). * - Hard kill via `setTimeout` + `SIGKILL`. Output capped during * buffering — a multi-gigabyte stream is truncated as it arrives, * not after the process completes. * - `cwd` defaults to the skill's directory; we don't enforce * containment because the shell can `cd` freely. Operators wiring * `full` mode accept that. * * The module is async (subprocess spawn) but pure-ish: it takes a body, * returns a transformed body + telemetry events. Caller (typically the * MCP server's `get_skill` handler) forwards the events to the * configured sink. */ import type { ParsedSkill, SkillEvent, SkillsPreprocessingConfig } from './types.js'; /** * Environment-variable gate matching Claude Code's * `CLAUDE_CODE_USE_POWERSHELL_TOOL`. When unset, skills declaring * `shell: powershell` get their `!command` blocks blocked with a * `powershell_disabled` event regardless of mode. */ export declare const POWERSHELL_GATE_ENV_VAR = "QODO_SKILLS_ENABLE_POWERSHELL"; /** * Inputs to `preprocessBody`. Split into "what was parsed" (`skill`) * and "runtime state" (`isTrusted`, `env`, `now`) so tests can inject * deterministic values. */ export interface PreprocessContext { readonly skill: ParsedSkill; /** Whether the skill's source is trusted enough to execute commands. */ readonly isTrusted: boolean; /** * Process environment readable from inside the spawned commands. * Tests inject; production passes `process.env`. */ readonly env: Readonly>; /** * High-resolution timer source for `durationMs`. Defaults to * `Date.now()`; tests inject a stub. */ readonly now?: () => number; /** Host platform. Defaults to `process.platform`; tests inject `'win32'`. */ readonly platform?: NodeJS.Platform; } export interface PreprocessResult { /** Body with `!command` blocks rendered (executed, blocked, or untouched). */ readonly body: string; /** Telemetry events. Caller forwards to the configured sink. */ readonly events: readonly SkillEvent[]; } /** * Preprocess a skill body. Returns the transformed body + telemetry. * * Calling with `mode: 'disabled'` is a no-op fast-path: the body is * returned unchanged and no events are emitted. Operators who never * configure preprocessing pay zero overhead. */ export declare function preprocessBody(rawBody: string, ctx: PreprocessContext, config: SkillsPreprocessingConfig | undefined): Promise; interface CommandBlock { /** Whole match (the slice that gets replaced in the body). */ readonly raw: string; /** Just the command line, no fence/backtick wrapping. */ readonly command: string; /** Byte offset of `raw` within the body, used to apply replacements. */ readonly start: number; readonly end: number; } /** * Find all `!command` blocks in a body. The scanner walks the body once * and emits matches in source order so the replacement step can stitch * the output back together with a single pass. * * The scanner ignores `!`-prefixed commands inside non-`!` fenced * blocks — those are narrative code samples, not commands. Conversely, * a `!`-tagged fenced block hides any inner backticks from the inline * regex. */ export declare function findCommandBlocks(body: string): CommandBlock[]; /** * Replace `${QODO_SKILL_DIR}` (and any future `${QODO_*}` vars) in the * body. The substitution is global — every occurrence is replaced. * * Only `QODO_SKILL_DIR` is supported in this wave; unknown `${QODO_*}` * references pass through literal so a typo doesn't disappear silently. * `skillDir` overrides the substituted value; `preprocessBody` passes a * shell-safe form on Windows. */ export declare function substituteQodoVars(body: string, skill: ParsedSkill, skillDir?: string): string; /** * Extract the first shell-tokenised argv from a command line. Matches * `cmd args` and `cmd "with spaces"`; returns the bare command name * with no quotes. `null` if the line is empty after tokenisation. * * Deliberately incomplete — we only need to know the first command * name for allowlist gating. Pipes, env assignment, here-docs, and * other shell features fall through to the shell itself (`bash -c`). * * `FOO=bar cmd` is peeled — env-assign prefixes don't change the * command identity for allowlist purposes, and an attacker shouldn't * be able to dodge an allowlist that whitelists `cmd` by prepending * `PATH=/evil cmd`. */ export declare function parseFirstToken(line: string): string | null; /** * Check whether `cmd` matches one of the allowlist entries. Match is * left-anchored: * * - Bare names (`git`) match the basename of `cmd` (so `git` matches * both `git` and `/usr/bin/git`). * - Path-bearing entries (`/usr/bin/jq`, `./bin/foo`) must match * verbatim. * * On Windows both forms compare case-insensitively with either separator, * and a bare name ignores a trailing `.exe` — the shell resolves `git` and * `GIT.exe` to the same binary there. */ export declare function isAllowlisted(cmd: string, allow: readonly string[], platform?: NodeJS.Platform): boolean; /** * Resolve the bash executable to invoke. A hardcoded `/bin/bash` would * fail on Windows-host operators and on Nix / NixOS systems where bash * lives under `/run/current-system/sw/bin/` instead of `/bin/`. Resolution * order: * * 1. `QODO_SKILLS_BASH_PATH` env override — explicit operator * control, takes precedence over everything. * 2. `SHELL` env when it's a bash variant — covers the common Unix * case where the operator's preferred shell is already on PATH. * 3. `bash` (rely on PATH lookup) — works on macOS and most Linux * distros. * * Windows reads env names case-insensitively and replaces 2–3 with * {@link findGitBash}; `null` means no usable bash was found. */ export declare function resolveBashPath(env: Readonly>, platform?: NodeJS.Platform): string | null; export {}; //# sourceMappingURL=preprocess.d.ts.map