@warlock.js/ai-workspace 5.31.0 → 5.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/cjs/index.cjs +34 -22
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/backends/local.d.mts +1 -3
  5. package/esm/backends/local.d.mts.map +1 -1
  6. package/esm/backends/local.mjs +1 -1
  7. package/esm/backends/local.mjs.map +1 -1
  8. package/esm/backends/mock.d.mts +5 -6
  9. package/esm/backends/mock.d.mts.map +1 -1
  10. package/esm/contracts/index.d.mts +2 -1
  11. package/esm/contracts/tool-io.type.d.mts +37 -38
  12. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  13. package/esm/contracts/workspace-backend.contract.d.mts +7 -9
  14. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  15. package/esm/contracts/workspace-ops.contract.d.mts +1 -3
  16. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  17. package/esm/contracts/workspace-policy.type.d.mts +10 -11
  18. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  19. package/esm/contracts/workspace.contract.d.mts +2 -4
  20. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  21. package/esm/errors.d.mts +35 -23
  22. package/esm/errors.d.mts.map +1 -1
  23. package/esm/index.d.mts +1 -0
  24. package/esm/ops.d.mts +3 -4
  25. package/esm/ops.d.mts.map +1 -1
  26. package/esm/policy/policy.d.mts +5 -5
  27. package/esm/policy/policy.d.mts.map +1 -1
  28. package/esm/policy/policy.mjs +4 -3
  29. package/esm/policy/policy.mjs.map +1 -1
  30. package/esm/policy/shell-check.d.mts +5 -6
  31. package/esm/policy/shell-check.d.mts.map +1 -1
  32. package/esm/policy/shell-check.mjs +24 -13
  33. package/esm/policy/shell-check.mjs.map +1 -1
  34. package/esm/policy/shell-parser.mjs +2 -2
  35. package/esm/policy/shell-parser.mjs.map +1 -1
  36. package/esm/policy/tokenize-command.d.mts +3 -4
  37. package/esm/policy/tokenize-command.d.mts.map +1 -1
  38. package/esm/policy/tokenize-command.mjs +2 -2
  39. package/esm/policy/tokenize-command.mjs.map +1 -1
  40. package/esm/shell-jobs.d.mts +3 -3
  41. package/esm/shell-jobs.d.mts.map +1 -1
  42. package/esm/shell-jobs.mjs +1 -1
  43. package/esm/todos.d.mts +2 -2
  44. package/esm/todos.d.mts.map +1 -1
  45. package/esm/todos.mjs +1 -1
  46. package/esm/tools/edit-file.d.mts +2 -3
  47. package/esm/tools/edit-file.d.mts.map +1 -1
  48. package/esm/tools/glob.d.mts +3 -4
  49. package/esm/tools/glob.d.mts.map +1 -1
  50. package/esm/tools/grep.d.mts +3 -4
  51. package/esm/tools/grep.d.mts.map +1 -1
  52. package/esm/tools/list-dir.d.mts +2 -3
  53. package/esm/tools/list-dir.d.mts.map +1 -1
  54. package/esm/tools/multi-edit.d.mts +2 -3
  55. package/esm/tools/multi-edit.d.mts.map +1 -1
  56. package/esm/tools/read-file.d.mts +2 -3
  57. package/esm/tools/read-file.d.mts.map +1 -1
  58. package/esm/tools/run-shell.d.mts +5 -6
  59. package/esm/tools/run-shell.d.mts.map +1 -1
  60. package/esm/tools/run-tests.d.mts +3 -4
  61. package/esm/tools/run-tests.d.mts.map +1 -1
  62. package/esm/tools/shell-kill.d.mts +2 -3
  63. package/esm/tools/shell-kill.d.mts.map +1 -1
  64. package/esm/tools/shell-output.d.mts +2 -3
  65. package/esm/tools/shell-output.d.mts.map +1 -1
  66. package/esm/tools/todo.d.mts +3 -4
  67. package/esm/tools/todo.d.mts.map +1 -1
  68. package/esm/tools/write-file.d.mts +2 -3
  69. package/esm/tools/write-file.d.mts.map +1 -1
  70. package/esm/workspace.d.mts +3 -3
  71. package/esm/workspace.d.mts.map +1 -1
  72. package/esm/workspace.mjs +1 -1
  73. package/esm/workspace.mjs.map +1 -1
  74. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"shell-check.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/shell-check.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { WorkspaceShellRefusalReason } from \"../errors\";\nimport type {\n WorkspaceCommandPattern,\n WorkspacePolicy,\n WorkspaceShellInterpreter,\n WorkspaceShellPolicy,\n} from \"../contracts\";\nimport { isInsideJailLexically } from \"./jail\";\nimport {\n parseShellLine,\n type ShellDialect,\n type ShellSegment,\n type ShellSimpleCommand,\n type ShellWord,\n} from \"./shell-parser\";\nimport { analyzeArgvCommand, tokenizeCommand } from \"./tokenize-command\";\n\n/** How a shell-mode line touches a path the jail has to vet. */\nexport type WorkspaceShellPathAccess = \"read\" | \"write\" | \"cwd\";\n\n/** A path a shell-mode line redirects to/from or `cd`s into. */\nexport interface WorkspaceShellPath {\n /** Absolute path, resolved lexically against the workspace root (or an earlier `cd`). */\n path: string;\n /** What the line does with it. */\n access: WorkspaceShellPathAccess;\n /** The target as written in the line. */\n target: string;\n}\n\n/**\n * The shell policy's verdict on one command line — see\n * {@link checkShellCommand}.\n */\nexport interface WorkspaceShellCheck {\n /** Whether the line may run (path targets still get a symlink-aware recheck in `exec`). */\n allowed: boolean;\n /** Why it was refused. */\n reason?: WorkspaceShellRefusalReason;\n /** Model-facing refusal text: the cause, and how to fix the command. */\n message?: string;\n /** The executable the refusal is about, when there is one. */\n executable?: string;\n /** The redirection target or `cd` directory a `\"path-escape\"` refusal is about. */\n path?: string;\n /** The argv of every simple command checked, in order (wrapped commands included). */\n commands: string[][];\n /** Shell mode: every redirection target and `cd` directory, for the jail recheck. */\n paths: WorkspaceShellPath[];\n}\n\n/**\n * Programs and shell builtins that run a command or code the policy cannot\n * see (a script string, a file, a command read from input, another user).\n * With deny rules in force they would hide a denied command, so they are\n * refused unless the policy names them in `allow`.\n */\nconst OPAQUE_RUNNERS = new Set([\n // Shells and interpreters of command strings.\n \"sh\",\n \"bash\",\n \"dash\",\n \"zsh\",\n \"ksh\",\n \"mksh\",\n \"ash\",\n \"yash\",\n \"posh\",\n \"fish\",\n \"csh\",\n \"tcsh\",\n \"busybox\",\n \"cmd\",\n \"powershell\",\n \"pwsh\",\n \"wsl\",\n // Run a command as someone else, elsewhere, or out of the process group.\n \"sudo\",\n \"doas\",\n \"su\",\n \"runuser\",\n \"pkexec\",\n \"runas\",\n \"chroot\",\n \"nsenter\",\n \"unshare\",\n \"setsid\",\n \"systemd-run\",\n \"start-stop-daemon\",\n \"daemonize\",\n // Run commands built from input or on a schedule.\n \"xargs\",\n \"parallel\",\n \"watch\",\n \"script\",\n \"expect\",\n \"at\",\n \"batch\",\n \"screen\",\n \"tmux\",\n \"stdbuf\",\n \"flock\",\n]);\n\n/** POSIX shell builtins that evaluate code or change how later names resolve. */\nconst SHELL_OPAQUE_BUILTINS = new Set([\n \"eval\",\n \".\",\n \"source\",\n \"alias\",\n \"unalias\",\n \"hash\",\n \"enable\",\n \"trap\",\n \"let\",\n \"declare\",\n \"typeset\",\n \"local\",\n \"set\",\n \"shopt\",\n \"fc\",\n \"bind\",\n \"complete\",\n \"compgen\",\n \"compopt\",\n \"mapfile\",\n \"readarray\",\n]);\n\n/** cmd.exe builtins that run commands or change the environment. */\nconst CMD_OPAQUE_BUILTINS = new Set([\"call\", \"start\", \"set\", \"pushd\", \"popd\"]);\n\n/**\n * Shell builtins whose arguments bash may evaluate as arithmetic (array\n * subscripts such as `a[$(cmd)]`): their arguments must be literal and free\n * of `[`.\n */\nconst ARITHMETIC_SENSITIVE_BUILTINS = new Set([\n \"test\",\n \"[\",\n \"printf\",\n \"read\",\n \"unset\",\n \"export\",\n \"readonly\",\n \"getopts\",\n \"wait\",\n \"kill\",\n \"shift\",\n \"return\",\n \"exit\",\n \"break\",\n \"continue\",\n \"ulimit\",\n]);\n\n/**\n * Variables a shell-mode line may not assign: they change how the shell\n * parses, traces or resolves what follows (`CDPATH` would move `cd` out of\n * the jail's sight, `PS4` is expanded — command substitutions included —\n * on every traced line, `IFS` re-splits expansions).\n */\nconst SENSITIVE_VARIABLES = new Set([\n \"CDPATH\",\n \"ENV\",\n \"BASH_ENV\",\n \"SHELLOPTS\",\n \"BASHOPTS\",\n \"IFS\",\n \"PS1\",\n \"PS2\",\n \"PS3\",\n \"PS4\",\n \"PROMPT_COMMAND\",\n \"BASH_XTRACEFD\",\n \"GLOBIGNORE\",\n \"EXECIGNORE\",\n \"BASH_LOADABLES_PATH\",\n]);\n\n/** Redirection targets that are always fine even though they sit outside the jail. */\nconst DEVICE_TARGETS = new Set([\"/dev/null\", \"/dev/stdout\", \"/dev/stderr\"]);\n\n/** cmd.exe's null device. */\nconst CMD_DEVICE_TARGETS = new Set([\"nul\"]);\n\n/** Deepest chain of wrappers (`env time nice …`) the policy unwraps. */\nconst MAX_WRAPPER_DEPTH = 8;\n\n/** Most distinct directories a line may `cd` between. */\nconst MAX_CD_CANDIDATES = 32;\n\n/** Longest command shown verbatim in a refusal before it is abbreviated. */\nconst MAX_SHOWN_COMMAND = 48;\n\n/** The dialect shell mode parses on this platform. */\nexport function shellDialect(): ShellDialect {\n return process.platform === \"win32\" ? \"cmd\" : \"posix\";\n}\n\n/**\n * Reduce an argv's first element to the basename the allow/deny policy is\n * keyed on. `\"npm\"` → `\"npm\"`; `\"/usr/bin/node\"` → `\"node\"`; `\"node.exe\"`\n * → `\"node\"` (the `.exe`/`.cmd`/`.bat`/`.com` Windows extension is\n * stripped). The cmd dialect splits Windows paths too.\n */\nexport function executableBasename(firstToken: string, dialect: ShellDialect = \"posix\"): string {\n const base = dialect === \"cmd\" ? path.win32.basename(firstToken) : path.basename(firstToken);\n\n return base.replace(/\\.(exe|cmd|bat|com)$/i, \"\");\n}\n\n/** Show a command in a refusal, abbreviated at a word boundary when long. */\nfunction show(source: string): string {\n const flat = source.replace(/\\s+/g, \" \").trim();\n\n if (flat.length <= MAX_SHOWN_COMMAND) {\n return `\\`${flat}\\``;\n }\n\n const cut = flat.lastIndexOf(\" \", MAX_SHOWN_COMMAND);\n\n return `\\`${flat.slice(0, cut > 0 ? cut : MAX_SHOWN_COMMAND)} …\\``;\n}\n\n/** Compile `denyPatterns` once per check. */\ninterface CompiledPattern {\n source: WorkspaceCommandPattern;\n test(argv: string[], dialect: ShellDialect): boolean;\n}\n\nfunction compilePattern(pattern: WorkspaceCommandPattern): CompiledPattern {\n if (pattern instanceof RegExp) {\n const regex = new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, \"\"));\n\n return {\n source: pattern,\n test: (argv, dialect) =>\n regex.test([executableBasename(argv[0] ?? \"\", dialect), ...argv.slice(1)].join(\" \")),\n };\n }\n\n const tokens = tokenizeCommand(pattern);\n\n if (tokens === null) {\n // Not representable as an argv: match it as text inside the joined argv.\n return {\n source: pattern,\n test: (argv, dialect) =>\n [executableBasename(argv[0] ?? \"\", dialect), ...argv.slice(1)].join(\" \").includes(pattern),\n };\n }\n\n return {\n source: pattern,\n test: (argv, dialect) => {\n if (argv.length < tokens.length) {\n return false;\n }\n\n return tokens.every((token, index) => {\n const actual = argv[index] ?? \"\";\n\n return index === 0\n ? executableBasename(actual, dialect).toLowerCase() ===\n executableBasename(token, dialect).toLowerCase()\n : actual === token;\n });\n },\n };\n}\n\n/** Thrown inside the checker to stop at the first refusal. */\nclass Refusal extends Error {\n public constructor(\n public readonly reason: WorkspaceShellRefusalReason,\n message: string,\n public readonly executable?: string,\n public readonly refusedPath?: string,\n ) {\n super(message);\n }\n}\n\n/** Per-line checker state. */\nclass LineChecker {\n public readonly commands: string[][] = [];\n\n public readonly paths: WorkspaceShellPath[] = [];\n\n private readonly deny: Set<string>;\n\n private readonly allow: string[];\n\n private readonly patterns: CompiledPattern[];\n\n private readonly hasCommandRules: boolean;\n\n private readonly interpreter: WorkspaceShellInterpreter;\n\n public constructor(\n private readonly policy: WorkspacePolicy,\n private readonly shell: WorkspaceShellPolicy,\n private readonly line: string,\n private readonly dialect: ShellDialect,\n ) {\n this.deny = new Set((shell.deny ?? []).map((name) => name.toLowerCase()));\n this.allow = shell.allow ?? [];\n this.patterns = (shell.denyPatterns ?? []).map(compilePattern);\n this.interpreter = shell.interpreter ?? \"none\";\n this.hasCommandRules =\n this.deny.size > 0 || this.patterns.length > 0 || shell.allowCommand !== undefined;\n }\n\n /** Whether argument hooks (`denyPatterns` / `allowCommand`) need literal argv. */\n public get hasArgumentHooks(): boolean {\n return this.patterns.length > 0 || this.shell.allowCommand !== undefined;\n }\n\n private get shellMode(): boolean {\n return this.interpreter === \"sh\";\n }\n\n /** Run `denyPatterns` and `allowCommand` on one argv. */\n private checkHooks(argv: string[], nested: boolean): void {\n const shown = show(argv.join(\" \"));\n\n for (const pattern of this.patterns) {\n if (pattern.test(argv, this.dialect)) {\n throw new Refusal(\n \"denied-pattern\",\n `${shown} matches a command pattern the workspace shell policy denies.`,\n executableBasename(argv[0] ?? \"\", this.dialect),\n );\n }\n }\n\n const hook = this.shell.allowCommand;\n\n if (hook === undefined) {\n return;\n }\n\n let verdict: boolean | string;\n\n try {\n verdict = hook([...argv], { command: this.line, interpreter: this.interpreter, nested });\n } catch {\n verdict = false;\n }\n\n if (verdict === true) {\n return;\n }\n\n const explanation =\n typeof verdict === \"string\" && verdict.trim() !== \"\" ? ` ${verdict.trim()}` : \"\";\n\n throw new Refusal(\n \"not-allowed\",\n `${shown} was refused by the workspace shell policy.${explanation}`,\n executableBasename(argv[0] ?? \"\", this.dialect),\n );\n }\n\n /** Whether the policy names `executable` in a non-empty `allow` list. */\n private explicitlyAllowed(executable: string): boolean {\n return this.allow.includes(executable);\n }\n\n /** Refuse a command runner the policy cannot see into, unless explicitly allowed. */\n private checkOpaque(executable: string, argv: string[]): void {\n if (this.explicitlyAllowed(executable) || !this.hasCommandRules) {\n return;\n }\n\n throw new Refusal(\n \"nested-command\",\n `\\`${executable}\\` runs a command the workspace shell policy cannot inspect (in ${show(argv.join(\" \"))}). Run the inner command directly instead.`,\n executable,\n );\n }\n\n /**\n * Check one argv: `deny`, `allow`, `denyPatterns`, `allowCommand`, then\n * follow wrappers and `find -exec` to the command they run. `dynamic[i]`\n * says whether `argv[i]` is decided at run time (shell mode).\n */\n public checkArgv(argv: string[], dynamic: boolean[], depth: number): void {\n const first = argv[0] ?? \"\";\n const executable = executableBasename(first, this.dialect);\n const key = executable.toLowerCase();\n\n if (executable === \"\") {\n throw new Refusal(\"not-allowed\", \"The command has no executable name.\");\n }\n\n if (dynamic[0] === true) {\n throw new Refusal(\n \"unsupported-syntax\",\n `The command name \\`${first}\\` is decided when the line runs, so the workspace shell policy cannot check it. Write the command name literally.`,\n );\n }\n\n if (this.deny.has(key)) {\n throw new Refusal(\n \"denied-executable\",\n `\\`${executable}\\` is denied by the workspace shell policy (in ${show(argv.join(\" \"))}).`,\n executable,\n );\n }\n\n if (this.allow.length > 0 && !this.allow.includes(executable)) {\n const listed = this.allow.slice(0, 20).join(\", \");\n\n throw new Refusal(\n \"not-allowed\",\n `\\`${executable}\\` is not in the workspace shell allowlist. Allowed commands: ${listed}${this.allow.length > 20 ? \", …\" : \"\"}.`,\n executable,\n );\n }\n\n this.checkHooks(argv, depth > 0);\n this.commands.push([...argv]);\n\n if (\n OPAQUE_RUNNERS.has(key) ||\n (this.shellMode && this.dialect === \"posix\" && SHELL_OPAQUE_BUILTINS.has(key)) ||\n (this.shellMode && this.dialect === \"cmd\" && CMD_OPAQUE_BUILTINS.has(key))\n ) {\n this.checkOpaque(executable, argv);\n\n return;\n }\n\n if (this.shellMode && this.dialect === \"posix\" && ARITHMETIC_SENSITIVE_BUILTINS.has(key)) {\n this.checkArithmeticSensitive(executable, argv, dynamic);\n }\n\n const ranges = this.unwrap(key, argv);\n\n if (ranges === undefined) {\n return;\n }\n\n if (ranges === null || depth >= MAX_WRAPPER_DEPTH) {\n throw new Refusal(\n \"nested-command\",\n `\\`${executable}\\` is used with options the workspace shell policy cannot read (in ${show(argv.join(\" \"))}). Run the inner command directly instead.`,\n executable,\n );\n }\n\n for (const [start, end] of ranges) {\n this.checkArgv(argv.slice(start, end), dynamic.slice(start, end), depth + 1);\n }\n }\n\n /**\n * Builtins bash may evaluate arguments of as arithmetic (array subscripts\n * like `a[$(cmd)]` run the substitution): arguments must be literal, free\n * of `[`, and must not name a variable that changes how the shell runs.\n */\n private checkArithmeticSensitive(executable: string, argv: string[], dynamic: boolean[]): void {\n for (let index = 1; index < argv.length; index++) {\n const argument = argv[index] ?? \"\";\n\n if (dynamic[index] === true) {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${executable}\\` arguments must be literal here (\\`${argument}\\` is not): bash may evaluate them as arithmetic.`,\n executable,\n );\n }\n\n if (argument.includes(\"[\") && argument !== \"[\" && argument !== \"]\") {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${executable}\\` arguments with \\`[\\` (array subscripts, evaluated as arithmetic) are not supported.`,\n executable,\n );\n }\n\n const name = argument.split(\"=\")[0] ?? \"\";\n\n if (SENSITIVE_VARIABLES.has(name) || name.startsWith(\"BASH_FUNC_\")) {\n throw new Refusal(\n \"unsupported-syntax\",\n `Changing \\`${name}\\` is not supported: it changes how the shell runs the commands that follow.`,\n executable,\n );\n }\n }\n }\n\n /**\n * The argv ranges a wrapper runs. `undefined` — not a wrapper (or one that\n * runs nothing, like a bare `env`); `null` — a wrapper whose options the\n * policy cannot read (refused).\n */\n private unwrap(key: string, argv: string[]): Array<[number, number]> | null | undefined {\n switch (key) {\n case \"env\":\n return suffixFrom(argv, unwrapEnv(argv));\n case \"nice\":\n return suffixFrom(argv, unwrapNice(argv));\n case \"nohup\":\n return suffixFrom(argv, unwrapSimple(argv, []));\n case \"time\":\n return suffixFrom(argv, unwrapSimple(argv, [\"-p\"]));\n case \"timeout\":\n return suffixFrom(argv, unwrapTimeout(argv));\n case \"command\":\n return argv.slice(1).some((arg) => arg === \"-v\" || arg === \"-V\")\n ? undefined\n : suffixFrom(argv, unwrapSimple(argv, [\"-p\"]));\n case \"builtin\":\n return suffixFrom(argv, unwrapSimple(argv, []));\n case \"exec\":\n return this.dialect === \"posix\" ? suffixFrom(argv, unwrapExec(argv)) : undefined;\n case \"find\":\n return unwrapFind(argv);\n default:\n return undefined;\n }\n }\n\n /** Check a `cd` (shell mode): literal, one operand, inside the jail from every possible cwd. */\n public checkCd(words: ShellWord[], cwds: string[]): string[] {\n const argv = words.map((word) => word.value);\n\n if (this.deny.has(\"cd\")) {\n throw new Refusal(\"denied-executable\", \"`cd` is denied by the workspace shell policy.\", \"cd\");\n }\n\n this.checkHooks(argv, false);\n this.commands.push(argv);\n\n const operands = words.slice(1);\n\n while (\n this.dialect === \"posix\" &&\n operands.length > 0 &&\n [\"-P\", \"-L\", \"--\"].includes(operands[0]?.value ?? \"\")\n ) {\n operands.shift();\n }\n\n const target = operands[0];\n\n if (operands.length !== 1 || target === undefined || target.value === \"-\" || target.dynamic) {\n throw new Refusal(\n \"unsupported-syntax\",\n \"`cd` needs exactly one literal directory inside the workspace (no `-`, `~` or variables).\",\n \"cd\",\n );\n }\n\n const next = new Set(cwds);\n\n for (const cwd of cwds) {\n const resolved = this.resolveTarget(cwd, target.value);\n\n this.assertInJail(resolved, target.value, \"cd\");\n this.paths.push({ path: resolved, access: \"cwd\", target: target.value });\n next.add(resolved);\n }\n\n if (next.size > MAX_CD_CANDIDATES) {\n throw new Refusal(\"unsupported-syntax\", \"The line changes directory too many times.\", \"cd\");\n }\n\n return [...next];\n }\n\n /** Check a redirection target from every possible cwd. */\n public checkRedirectTarget(word: ShellWord, access: \"read\" | \"write\", cwds: string[]): void {\n if (word.dynamic) {\n throw new Refusal(\n \"unsupported-syntax\",\n `The redirection target \\`${word.raw}\\` must be a literal path (no variables, globs or \\`~\\`).`,\n );\n }\n\n const value = word.value;\n\n if (\n (this.dialect === \"posix\" && DEVICE_TARGETS.has(value)) ||\n (this.dialect === \"cmd\" && CMD_DEVICE_TARGETS.has(value.toLowerCase()))\n ) {\n return;\n }\n\n for (const cwd of cwds) {\n const resolved = this.resolveTarget(cwd, value);\n\n this.assertInJail(resolved, value, \"redirect\");\n this.paths.push({ path: resolved, access, target: value });\n }\n }\n\n private resolveTarget(cwd: string, target: string): string {\n return path.resolve(cwd, target);\n }\n\n private assertInJail(resolved: string, target: string, kind: \"cd\" | \"redirect\"): void {\n if (isInsideJailLexically(this.policy, resolved)) {\n return;\n }\n\n throw new Refusal(\n \"path-escape\",\n kind === \"cd\"\n ? `\\`cd ${target}\\` leaves the workspace jail (or enters a denied path); commands must stay inside the workspace.`\n : `The redirection target \\`${target}\\` is outside the workspace jail or on a denied path.`,\n undefined,\n target,\n );\n }\n}\n\n/**\n * Turn a wrapper's \"inner command starts at `start`\" into a range; a\n * wrapper that runs nothing (`start === argv.length`) becomes `undefined`.\n */\nfunction suffixFrom(\n argv: string[],\n start: number | null,\n): Array<[number, number]> | null | undefined {\n if (start === null) {\n return null;\n }\n\n return start < argv.length ? [[start, argv.length]] : undefined;\n}\n\n/** `nohup` / `time` / `command` / `builtin`: skip the listed flags and `--`. */\nfunction unwrapSimple(argv: string[], flags: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (flags.includes(arg)) {\n index++;\n\n continue;\n }\n\n if (arg.startsWith(\"-\") && arg !== \"-\") {\n return null;\n }\n\n break;\n }\n\n return index;\n}\n\n/** `env [-i] [-u NAME] [NAME=value]… command…` */\nfunction unwrapEnv(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-i\" || arg === \"-\" || arg === \"--ignore-environment\") {\n index++;\n\n continue;\n }\n\n if (arg === \"-u\" || arg === \"--unset\") {\n index += 2;\n\n continue;\n }\n\n if (arg.startsWith(\"--unset=\") || (arg.startsWith(\"-u\") && arg.length > 2)) {\n index++;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n while (index < argv.length && /^[^=]+=/.test(argv[index] ?? \"\")) {\n index++;\n }\n\n return index;\n}\n\n/** `nice [-n N | -N | --adjustment=N] command…` */\nfunction unwrapNice(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-n\" || arg === \"--adjustment\") {\n index += 2;\n\n continue;\n }\n\n if (/^-n-?[0-9]+$/.test(arg) || /^--adjustment=-?[0-9]+$/.test(arg) || /^-[0-9]+$/.test(arg)) {\n index++;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n return index;\n}\n\n/** `timeout [options] DURATION command…` */\nfunction unwrapTimeout(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-s\" || arg === \"--signal\" || arg === \"-k\" || arg === \"--kill-after\") {\n index += 2;\n\n continue;\n }\n\n if (\n /^-[sk].+$/.test(arg) ||\n arg.startsWith(\"--signal=\") ||\n arg.startsWith(\"--kill-after=\") ||\n arg === \"--preserve-status\" ||\n arg === \"--foreground\" ||\n arg === \"-v\" ||\n arg === \"--verbose\"\n ) {\n index++;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n // The duration, then the command.\n if (index >= argv.length - 1) {\n return null;\n }\n\n return index + 1;\n}\n\n/** `exec [-cl] [-a name] command…` (bash). */\nfunction unwrapExec(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-c\" || arg === \"-l\" || arg === \"-cl\" || arg === \"-lc\") {\n index++;\n\n continue;\n }\n\n if (arg === \"-a\") {\n index += 2;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n return index;\n}\n\n/** The commands `find -exec / -execdir / -ok / -okdir … ;|+` runs. */\nfunction unwrapFind(argv: string[]): Array<[number, number]> | null | undefined {\n const inner: Array<[number, number]> = [];\n\n for (let index = 1; index < argv.length; index++) {\n const arg = argv[index] ?? \"\";\n\n if (arg !== \"-exec\" && arg !== \"-execdir\" && arg !== \"-ok\" && arg !== \"-okdir\") {\n continue;\n }\n\n const start = index + 1;\n let end = start;\n\n while (end < argv.length && argv[end] !== \";\" && argv[end] !== \"+\") {\n end++;\n }\n\n if (end >= argv.length || end === start) {\n return null;\n }\n\n inner.push([start, end]);\n index = end;\n }\n\n return inner.length > 0 ? inner : undefined;\n}\n\n/** Build the argv-mode refusal for a line that is not a single argv. */\nfunction argvModeRefusal(command: string): Refusal {\n const analysis = analyzeArgvCommand(command);\n\n if (analysis.ok) {\n // Not reached: callers only ask for a refusal of a line that failed.\n return new Refusal(\"metacharacter\", \"The command cannot run as a single argv.\");\n }\n\n if (analysis.type === \"empty\") {\n return new Refusal(\"not-allowed\", \"The command is empty.\");\n }\n\n if (analysis.type === \"unbalanced-quotes\") {\n return new Refusal(\n \"unbalanced-quotes\",\n `The command has an unbalanced \\`${analysis.quote}\\` quote. Close it, or quote the argument with the other kind of quote.`,\n );\n }\n\n const token = analysis.token;\n const suffix = \" (run_shell executes a single command without a shell.)\";\n const parsed = parseShellLine(command, \"posix\");\n const segments: ShellSegment[] = parsed.ok ? parsed.segments : [];\n const listOperator = token === \"&&\" || token === \"||\" || token === \";\" || token === \"\\n\";\n\n if (listOperator && segments.length >= 2) {\n const shownOperator = token === \"\\n\" ? \"A line break\" : `\\`${token}\\``;\n const firstWords = (segments[0]?.source ?? \"\").trim().split(/\\s+/);\n\n if (firstWords[0] === \"cd\") {\n const rest = segments.slice(1).map((segment) => show(segment.source));\n\n return new Refusal(\n \"metacharacter\",\n `${shownOperator} is not supported in argv mode, and \\`cd\\` does not carry over: every command starts in the workspace root. Run ${rest.join(\", then \")} on its own, with paths relative to the root.${suffix}`,\n );\n }\n\n const steps = segments.map((segment) => show(segment.source));\n const joiner = token === \"||\" ? \", and if it fails, \" : \", then \";\n\n return new Refusal(\n \"metacharacter\",\n `${shownOperator} is not supported in argv mode: run ${steps.join(joiner)}, as separate commands.${suffix}`,\n );\n }\n\n if (token === \"&\" && segments.length >= 1) {\n const steps = segments.map((segment) => show(segment.source));\n const run =\n steps.length === 1\n ? `run ${steps[0] ?? \"\"} without it`\n : `run ${steps.join(\", then \")} as separate commands`;\n\n return new Refusal(\n \"metacharacter\",\n `\\`&\\` is not supported in argv mode: ${run}; start a long-running command with background: true.${suffix}`,\n );\n }\n\n if (token === \"|\") {\n const pipeline = command\n .split(\"|\")\n .map((part) => part.trim())\n .filter(Boolean);\n\n return new Refusal(\n \"metacharacter\",\n `\\`|\\` (a pipe) is not supported in argv mode: run ${show(pipeline[0] ?? command)} on its own and read its output yourself.${suffix}`,\n );\n }\n\n if (token === \">\" || token === \">>\" || token === \">&\") {\n return new Refusal(\n \"metacharacter\",\n `\\`${token}\\` (redirection) is not supported in argv mode: run the command without it and save the output with write_file if you need it.${suffix}`,\n );\n }\n\n if (token === \"<\" || token === \"<<\" || token === \"<&\") {\n return new Refusal(\n \"metacharacter\",\n `\\`${token}\\` (input redirection) is not supported in argv mode: pass the file path to the command as an argument.${suffix}`,\n );\n }\n\n if (token === \"$(\" || token === \"`\") {\n return new Refusal(\n \"metacharacter\",\n `Command substitution (${token === \"$(\" ? \"`$(…)`\" : \"backticks\"}) is not supported in argv mode: run the inner command first, then pass its output literally.${suffix}`,\n );\n }\n\n if (token === \"$\" || token === \"${\") {\n return new Refusal(\n \"metacharacter\",\n `\\`$\\` (variable expansion) is not supported in argv mode: write the value literally, or put the \\`$\\` inside quotes to pass it as text.${suffix}`,\n );\n }\n\n if (token === \"(\" || token === \")\") {\n return new Refusal(\n \"metacharacter\",\n `\\`${token}\\` is not supported in argv mode: put it inside quotes to pass it as text.${suffix}`,\n );\n }\n\n return new Refusal(\n \"metacharacter\",\n `\\`${token === \"\\n\" ? \"\\\\n\" : token}\\` is not supported in argv mode: run one command per call, and quote any argument that contains it.${suffix}`,\n );\n}\n\n/** Check a shell-mode (`interpreter: \"sh\"`) line. */\nfunction checkShellLine(\n checker: LineChecker,\n policy: WorkspacePolicy,\n command: string,\n dialect: ShellDialect,\n): void {\n if (/^[-+]/.test(command)) {\n throw new Refusal(\"unsupported-syntax\", \"A command line may not start with `-` or `+`.\");\n }\n\n const parsed = parseShellLine(command, dialect);\n\n if (!parsed.ok) {\n throw new Refusal(\n parsed.reason,\n `${parsed.message} The workspace shell policy only runs lines it can check.`,\n );\n }\n\n if (parsed.background) {\n throw new Refusal(\n \"unsupported-syntax\",\n \"`&` (run in the background) is not supported in a command line: start a long-running command with background: true instead.\",\n );\n }\n\n let cwds = [path.resolve(policy.cwd)];\n\n for (const simple of parsed.commands) {\n cwds = checkSimpleCommand(checker, simple, cwds, dialect);\n }\n}\n\n/** Check one parsed simple command; returns the possible working directories after it. */\nfunction checkSimpleCommand(\n checker: LineChecker,\n simple: ShellSimpleCommand,\n cwds: string[],\n dialect: ShellDialect,\n): string[] {\n for (const assignment of simple.assignments) {\n const name = assignment.assignmentName ?? \"\";\n\n if (SENSITIVE_VARIABLES.has(name) || name.startsWith(\"BASH_FUNC_\")) {\n throw new Refusal(\n \"unsupported-syntax\",\n `Assigning \\`${name}\\` is not supported: it changes how the shell runs the commands that follow.`,\n );\n }\n }\n\n for (const redirect of simple.redirects) {\n if (redirect.type === \"read\" || redirect.type === \"write\") {\n checker.checkRedirectTarget(redirect.target, redirect.type, cwds);\n }\n }\n\n const first = simple.words[0];\n\n if (first === undefined) {\n return cwds;\n }\n\n if (!first.dynamic && first.value.includes(\"=\")) {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${first.raw}\\` is not a plain command name or \\`NAME=value\\` assignment.`,\n );\n }\n\n if (/^[-+]/.test(first.value)) {\n throw new Refusal(\"unsupported-syntax\", `\\`${first.raw}\\` is not a command name.`);\n }\n\n if (checker.hasArgumentHooks) {\n const dynamic = simple.words.find((word) => word.dynamic);\n\n if (dynamic !== undefined) {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${dynamic.raw}\\` is decided when the line runs, so it cannot be checked against the workspace shell policy's command rules. Write the value literally (quote globs).`,\n );\n }\n }\n\n const executable = first.dynamic ? \"\" : executableBasename(first.value, dialect).toLowerCase();\n\n if (executable === \"cd\") {\n return checker.checkCd(simple.words, cwds);\n }\n\n if (dialect === \"posix\" && (executable === \"pushd\" || executable === \"popd\")) {\n throw new Refusal(\"unsupported-syntax\", `\\`${executable}\\` is not supported; use \\`cd\\`.`);\n }\n\n checker.checkArgv(\n simple.words.map((word) => (word.dynamic ? word.raw : word.value)),\n simple.words.map((word) => word.dynamic),\n 0,\n );\n\n return cwds;\n}\n\n/**\n * The shell policy's verdict on a command line, without touching the\n * filesystem.\n *\n * - **argv mode** (`interpreter: \"none\"`, the default): the line must\n * tokenize into one argv; otherwise it is refused with a\n * `\"metacharacter\"` / `\"unbalanced-quotes\"` reason and a message that\n * names the operator and suggests the split.\n * - **shell mode** (`interpreter: \"sh\"`): the line is parsed and every\n * simple command — inside `;`, `&&`, `||`, pipes, subshells, `$( )`,\n * backticks and here-documents — is checked; redirection targets and `cd`\n * directories must stay in the jail (checked lexically here; `exec`\n * re-checks them with symlinks resolved).\n *\n * Every argv goes through `deny` (case-insensitive, wins), `allow`\n * (exhaustive when non-empty), `denyPatterns` and `allowCommand`.\n * Wrappers (`env`, `time`, `timeout`, `nice`, `nohup`, `command`,\n * `builtin`, `exec`) and `find -exec` are followed to the command they run.\n * A runner the policy cannot see into (`bash -c`, `eval`, `xargs`, `sudo`,\n * …) is refused while the policy has deny rules, unless `allow` names it.\n *\n * @example\n * checkShellCommand({ cwd, shell: { allow: [\"git\"] } }, \"git add -A && git commit -m wip\");\n * // { allowed: false, reason: \"metacharacter\", message: \"`&&` is not supported in argv mode: run `git add -A`, then `git commit -m wip`, …\" }\n */\nexport function checkShellCommand(policy: WorkspacePolicy, command: string): WorkspaceShellCheck {\n return checkShellCommandInDialect(policy, command, shellDialect());\n}\n\n/**\n * {@link checkShellCommand} with the shell-mode dialect chosen explicitly\n * instead of from the platform (used to test the cmd.exe rules anywhere).\n * Argv mode ignores `dialect`.\n */\nexport function checkShellCommandInDialect(\n policy: WorkspacePolicy,\n command: string,\n dialect: ShellDialect,\n): WorkspaceShellCheck {\n const shell = policy.shell;\n\n if (!shell) {\n return {\n allowed: false,\n reason: \"not-allowed\",\n message: \"The workspace has no shell policy, so no command may run.\",\n commands: [],\n paths: [],\n };\n }\n\n const interpreter = shell.interpreter ?? \"none\";\n const checker = new LineChecker(policy, shell, command, interpreter === \"sh\" ? dialect : \"posix\");\n\n try {\n if (command.trim() === \"\") {\n throw new Refusal(\"not-allowed\", \"The command is empty.\");\n }\n\n if (interpreter === \"sh\") {\n checkShellLine(checker, policy, command, dialect);\n } else {\n const argv = tokenizeCommand(command);\n\n if (argv === null) {\n throw argvModeRefusal(command);\n }\n\n checker.checkArgv(\n argv,\n argv.map(() => false),\n 0,\n );\n }\n\n return { allowed: true, commands: checker.commands, paths: checker.paths };\n } catch (error) {\n if (!(error instanceof Refusal)) {\n throw error;\n }\n\n return {\n allowed: false,\n reason: error.reason,\n message: error.message,\n ...(error.executable !== undefined ? { executable: error.executable } : {}),\n ...(error.refusedPath !== undefined ? { path: error.refusedPath } : {}),\n commands: checker.commands,\n paths: checker.paths,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shell-check.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/shell-check.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { WorkspaceShellRefusalReason } from \"../errors\";\nimport type {\n WorkspaceCommandPattern,\n WorkspacePolicy,\n WorkspaceShellInterpreter,\n WorkspaceShellPolicy,\n} from \"../contracts\";\nimport { isInsideJailLexically } from \"./jail\";\nimport {\n parseShellLine,\n type ShellDialect,\n type ShellSegment,\n type ShellSimpleCommand,\n type ShellWord,\n} from \"./shell-parser\";\nimport { analyzeArgvCommand, tokenizeCommand } from \"./tokenize-command\";\n\n/** How a shell-mode line touches a path the jail has to vet. */\nexport type WorkspaceShellPathAccess = \"read\" | \"write\" | \"cwd\";\n\n/** A path a shell-mode line redirects to/from or `cd`s into. */\nexport interface WorkspaceShellPath {\n /** Absolute path, resolved lexically against the workspace root (or an earlier `cd`). */\n path: string;\n /** What the line does with it. */\n access: WorkspaceShellPathAccess;\n /** The target as written in the line. */\n target: string;\n}\n\n/**\n * The shell policy's verdict on one command line — see\n * {@link checkShellCommand}.\n */\nexport interface WorkspaceShellCheck {\n /** Whether the line may run (path targets still get a symlink-aware recheck in `exec`). */\n allowed: boolean;\n /** Why it was refused. */\n reason?: WorkspaceShellRefusalReason;\n /** Model-facing refusal text: the cause, and how to fix the command. */\n message?: string;\n /** The executable the refusal is about, when there is one. */\n executable?: string;\n /** The redirection target or `cd` directory a `\"path-escape\"` refusal is about. */\n path?: string;\n /** The argv of every simple command checked, in order (wrapped commands included). */\n commands: string[][];\n /** Shell mode: every redirection target and `cd` directory, for the jail recheck. */\n paths: WorkspaceShellPath[];\n}\n\n/**\n * Programs and shell builtins that run a command or code the policy cannot\n * see (a script string, a file, a command read from input, another user).\n * With deny rules in force they would hide a denied command, so they are\n * refused unless the policy names them in `allow`.\n */\nconst OPAQUE_RUNNERS = new Set([\n // Shells and interpreters of command strings.\n \"sh\",\n \"bash\",\n \"dash\",\n \"zsh\",\n \"ksh\",\n \"mksh\",\n \"ash\",\n \"yash\",\n \"posh\",\n \"fish\",\n \"csh\",\n \"tcsh\",\n \"busybox\",\n \"cmd\",\n \"powershell\",\n \"pwsh\",\n \"wsl\",\n // Run a command as someone else, elsewhere, or out of the process group.\n \"sudo\",\n \"doas\",\n \"su\",\n \"runuser\",\n \"pkexec\",\n \"runas\",\n \"chroot\",\n \"nsenter\",\n \"unshare\",\n \"setsid\",\n \"systemd-run\",\n \"start-stop-daemon\",\n \"daemonize\",\n // Run commands built from input or on a schedule.\n \"xargs\",\n \"parallel\",\n \"watch\",\n \"script\",\n \"expect\",\n \"at\",\n \"batch\",\n \"screen\",\n \"tmux\",\n \"stdbuf\",\n \"flock\",\n]);\n\n/** POSIX shell builtins that evaluate code or change how later names resolve. */\nconst SHELL_OPAQUE_BUILTINS = new Set([\n \"eval\",\n \".\",\n \"source\",\n \"alias\",\n \"unalias\",\n \"hash\",\n \"enable\",\n \"trap\",\n \"let\",\n \"declare\",\n \"typeset\",\n \"local\",\n \"set\",\n \"shopt\",\n \"fc\",\n \"bind\",\n \"complete\",\n \"compgen\",\n \"compopt\",\n \"mapfile\",\n \"readarray\",\n]);\n\n/** cmd.exe builtins that run commands or change the environment. */\nconst CMD_OPAQUE_BUILTINS = new Set([\"call\", \"start\", \"set\", \"pushd\", \"popd\"]);\n\n/**\n * Shell builtins whose arguments bash may evaluate as arithmetic (array\n * subscripts such as `a[$(cmd)]`): their arguments must be literal and free\n * of `[`.\n */\nconst ARITHMETIC_SENSITIVE_BUILTINS = new Set([\n \"test\",\n \"[\",\n \"printf\",\n \"read\",\n \"unset\",\n \"export\",\n \"readonly\",\n \"getopts\",\n \"wait\",\n \"kill\",\n \"shift\",\n \"return\",\n \"exit\",\n \"break\",\n \"continue\",\n \"ulimit\",\n]);\n\n/**\n * Variables a shell-mode line may not assign: they change how the shell\n * parses, traces or resolves what follows (`CDPATH` would move `cd` out of\n * the jail's sight, `PS4` is expanded — command substitutions included —\n * on every traced line, `IFS` re-splits expansions).\n */\nconst SENSITIVE_VARIABLES = new Set([\n \"CDPATH\",\n \"ENV\",\n \"BASH_ENV\",\n \"SHELLOPTS\",\n \"BASHOPTS\",\n \"IFS\",\n \"PS1\",\n \"PS2\",\n \"PS3\",\n \"PS4\",\n \"PROMPT_COMMAND\",\n \"BASH_XTRACEFD\",\n \"GLOBIGNORE\",\n \"EXECIGNORE\",\n \"BASH_LOADABLES_PATH\",\n]);\n\n/** Redirection targets that are always fine even though they sit outside the jail. */\nconst DEVICE_TARGETS = new Set([\"/dev/null\", \"/dev/stdout\", \"/dev/stderr\"]);\n\n/** cmd.exe's null device. */\nconst CMD_DEVICE_TARGETS = new Set([\"nul\"]);\n\n/** Deepest chain of wrappers (`env time nice …`) the policy unwraps. */\nconst MAX_WRAPPER_DEPTH = 8;\n\n/** Most distinct directories a line may `cd` between. */\nconst MAX_CD_CANDIDATES = 32;\n\n/** Longest command shown verbatim in a refusal before it is abbreviated. */\nconst MAX_SHOWN_COMMAND = 48;\n\n/** The dialect shell mode parses on this platform. */\nexport function shellDialect(): ShellDialect {\n return process.platform === \"win32\" ? \"cmd\" : \"posix\";\n}\n\n/**\n * Reduce an argv's first element to the basename the allow/deny policy is\n * keyed on. `\"npm\"` → `\"npm\"`; `\"/usr/bin/node\"` → `\"node\"`; `\"node.exe\"`\n * → `\"node\"` (the `.exe`/`.cmd`/`.bat`/`.com` Windows extension is\n * stripped). The cmd dialect splits Windows paths too.\n */\nexport function executableBasename(firstToken: string, dialect: ShellDialect = \"posix\"): string {\n const base = dialect === \"cmd\" ? path.win32.basename(firstToken) : path.basename(firstToken);\n\n return base.replace(/\\.(exe|cmd|bat|com)$/i, \"\");\n}\n\n/** Show a command in a refusal, abbreviated at a word boundary when long. */\nfunction show(source: string): string {\n const flat = source.replace(/\\s+/g, \" \").trim();\n\n if (flat.length <= MAX_SHOWN_COMMAND) {\n return `\\`${flat}\\``;\n }\n\n const cut = flat.lastIndexOf(\" \", MAX_SHOWN_COMMAND);\n\n return `\\`${flat.slice(0, cut > 0 ? cut : MAX_SHOWN_COMMAND)} …\\``;\n}\n\n/** Compile `denyPatterns` once per check. */\ninterface CompiledPattern {\n source: WorkspaceCommandPattern;\n test(argv: string[], dialect: ShellDialect): boolean;\n}\n\nfunction compilePattern(pattern: WorkspaceCommandPattern): CompiledPattern {\n if (pattern instanceof RegExp) {\n const regex = new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, \"\"));\n\n return {\n source: pattern,\n test: (argv, dialect) =>\n regex.test([executableBasename(argv[0] ?? \"\", dialect), ...argv.slice(1)].join(\" \")),\n };\n }\n\n const tokens = tokenizeCommand(pattern);\n\n if (tokens === null) {\n // Not representable as an argv: match it as text inside the joined argv.\n return {\n source: pattern,\n test: (argv, dialect) =>\n [executableBasename(argv[0] ?? \"\", dialect), ...argv.slice(1)].join(\" \").includes(pattern),\n };\n }\n\n return {\n source: pattern,\n test: (argv, dialect) => {\n if (argv.length < tokens.length) {\n return false;\n }\n\n return tokens.every((token, index) => {\n const actual = argv[index] ?? \"\";\n\n return index === 0\n ? executableBasename(actual, dialect).toLowerCase() ===\n executableBasename(token, dialect).toLowerCase()\n : actual === token;\n });\n },\n };\n}\n\n/** Thrown inside the checker to stop at the first refusal. */\nclass Refusal extends Error {\n public constructor(\n public readonly reason: WorkspaceShellRefusalReason,\n message: string,\n public readonly executable?: string,\n public readonly refusedPath?: string,\n ) {\n super(message);\n }\n}\n\n/** Per-line checker state. */\nclass LineChecker {\n public readonly commands: string[][] = [];\n\n public readonly paths: WorkspaceShellPath[] = [];\n\n private readonly deny: Set<string>;\n\n private readonly allow: string[];\n\n private readonly patterns: CompiledPattern[];\n\n private readonly hasCommandRules: boolean;\n\n private readonly interpreter: WorkspaceShellInterpreter;\n\n public constructor(\n private readonly policy: WorkspacePolicy,\n private readonly shell: WorkspaceShellPolicy,\n private readonly line: string,\n private readonly dialect: ShellDialect,\n ) {\n this.deny = new Set((shell.deny ?? []).map((name) => name.toLowerCase()));\n this.allow = shell.allow ?? [];\n this.patterns = (shell.denyPatterns ?? []).map(compilePattern);\n this.interpreter = shell.interpreter ?? \"none\";\n this.hasCommandRules =\n this.deny.size > 0 || this.patterns.length > 0 || shell.allowCommand !== undefined;\n }\n\n /** Whether argument hooks (`denyPatterns` / `allowCommand`) need literal argv. */\n public get hasArgumentHooks(): boolean {\n return this.patterns.length > 0 || this.shell.allowCommand !== undefined;\n }\n\n private get shellMode(): boolean {\n return this.interpreter === \"sh\";\n }\n\n /** Run `denyPatterns` and `allowCommand` on one argv. */\n private checkHooks(argv: string[], nested: boolean): void {\n const shown = show(argv.join(\" \"));\n\n for (const pattern of this.patterns) {\n if (pattern.test(argv, this.dialect)) {\n throw new Refusal(\n \"denied-pattern\",\n `${shown} matches a command pattern the workspace shell policy denies.`,\n executableBasename(argv[0] ?? \"\", this.dialect),\n );\n }\n }\n\n const hook = this.shell.allowCommand;\n\n if (hook === undefined) {\n return;\n }\n\n let verdict: boolean | string;\n\n try {\n verdict = hook([...argv], { command: this.line, interpreter: this.interpreter, nested });\n } catch {\n verdict = false;\n }\n\n if (verdict === true) {\n return;\n }\n\n const explanation =\n typeof verdict === \"string\" && verdict.trim() !== \"\" ? ` ${verdict.trim()}` : \"\";\n\n throw new Refusal(\n \"not-allowed\",\n `${shown} was refused by the workspace shell policy.${explanation}`,\n executableBasename(argv[0] ?? \"\", this.dialect),\n );\n }\n\n /** Whether the policy names `executable` in a non-empty `allow` list. */\n private explicitlyAllowed(executable: string): boolean {\n return this.allow.includes(executable);\n }\n\n /** Refuse a command runner the policy cannot see into, unless explicitly allowed. */\n private checkOpaque(executable: string, argv: string[]): void {\n if (this.explicitlyAllowed(executable) || !this.hasCommandRules) {\n return;\n }\n\n throw new Refusal(\n \"nested-command\",\n `\\`${executable}\\` runs a command the workspace shell policy cannot inspect (in ${show(argv.join(\" \"))}). Run the inner command directly instead.`,\n executable,\n );\n }\n\n /**\n * Check one argv: `deny`, `allow`, `denyPatterns`, `allowCommand`, then\n * follow wrappers and `find -exec` to the command they run. `dynamic[i]`\n * says whether `argv[i]` is decided at run time (shell mode).\n */\n public checkArgv(argv: string[], dynamic: boolean[], depth: number): void {\n const first = argv[0] ?? \"\";\n const executable = executableBasename(first, this.dialect);\n const key = executable.toLowerCase();\n\n if (executable === \"\") {\n throw new Refusal(\"not-allowed\", \"The command has no executable name.\");\n }\n\n if (dynamic[0] === true) {\n throw new Refusal(\n \"unsupported-syntax\",\n `The command name \\`${first}\\` is decided when the line runs, so the workspace shell policy cannot check it. Write the command name literally.`,\n );\n }\n\n if (this.deny.has(key)) {\n throw new Refusal(\n \"denied-executable\",\n `\\`${executable}\\` is denied by the workspace shell policy (in ${show(argv.join(\" \"))}).`,\n executable,\n );\n }\n\n if (this.allow.length > 0 && !this.allow.includes(executable)) {\n const listed = this.allow.slice(0, 20).join(\", \");\n\n throw new Refusal(\n \"not-allowed\",\n `\\`${executable}\\` is not in the workspace shell allowlist. Allowed commands: ${listed}${this.allow.length > 20 ? \", …\" : \"\"}.`,\n executable,\n );\n }\n\n this.checkHooks(argv, depth > 0);\n this.commands.push([...argv]);\n\n if (\n OPAQUE_RUNNERS.has(key) ||\n (this.shellMode && this.dialect === \"posix\" && SHELL_OPAQUE_BUILTINS.has(key)) ||\n (this.shellMode && this.dialect === \"cmd\" && CMD_OPAQUE_BUILTINS.has(key))\n ) {\n this.checkOpaque(executable, argv);\n\n return;\n }\n\n if (this.shellMode && this.dialect === \"posix\" && ARITHMETIC_SENSITIVE_BUILTINS.has(key)) {\n this.checkArithmeticSensitive(executable, argv, dynamic);\n }\n\n const ranges = this.unwrap(key, argv);\n\n if (ranges === undefined) {\n return;\n }\n\n if (ranges === null || depth >= MAX_WRAPPER_DEPTH) {\n throw new Refusal(\n \"nested-command\",\n `\\`${executable}\\` is used with options the workspace shell policy cannot read (in ${show(argv.join(\" \"))}). Run the inner command directly instead.`,\n executable,\n );\n }\n\n for (const [start, end] of ranges) {\n this.checkArgv(argv.slice(start, end), dynamic.slice(start, end), depth + 1);\n }\n }\n\n /**\n * Builtins bash may evaluate arguments of as arithmetic (array subscripts\n * like `a[$(cmd)]` run the substitution): arguments must be literal, free\n * of `[`, and must not name a variable that changes how the shell runs.\n */\n private checkArithmeticSensitive(executable: string, argv: string[], dynamic: boolean[]): void {\n for (let index = 1; index < argv.length; index++) {\n const argument = argv[index] ?? \"\";\n\n if (dynamic[index] === true) {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${executable}\\` arguments must be literal here (\\`${argument}\\` is not): bash may evaluate them as arithmetic.`,\n executable,\n );\n }\n\n if (argument.includes(\"[\") && argument !== \"[\" && argument !== \"]\") {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${executable}\\` arguments with \\`[\\` (array subscripts, evaluated as arithmetic) are not supported.`,\n executable,\n );\n }\n\n const name = argument.split(\"=\")[0] ?? \"\";\n\n if (SENSITIVE_VARIABLES.has(name) || name.startsWith(\"BASH_FUNC_\")) {\n throw new Refusal(\n \"unsupported-syntax\",\n `Changing \\`${name}\\` is not supported: it changes how the shell runs the commands that follow.`,\n executable,\n );\n }\n }\n }\n\n /**\n * The argv ranges a wrapper runs. `undefined` — not a wrapper (or one that\n * runs nothing, like a bare `env`); `null` — a wrapper whose options the\n * policy cannot read (refused).\n */\n private unwrap(key: string, argv: string[]): Array<[number, number]> | null | undefined {\n switch (key) {\n case \"env\":\n return suffixFrom(argv, unwrapEnv(argv));\n case \"nice\":\n return suffixFrom(argv, unwrapNice(argv));\n case \"nohup\":\n return suffixFrom(argv, unwrapSimple(argv, []));\n case \"time\":\n return suffixFrom(argv, unwrapSimple(argv, [\"-p\"]));\n case \"timeout\":\n return suffixFrom(argv, unwrapTimeout(argv));\n case \"command\":\n return argv.slice(1).some((arg) => arg === \"-v\" || arg === \"-V\")\n ? undefined\n : suffixFrom(argv, unwrapSimple(argv, [\"-p\"]));\n case \"builtin\":\n return suffixFrom(argv, unwrapSimple(argv, []));\n case \"exec\":\n return this.dialect === \"posix\" ? suffixFrom(argv, unwrapExec(argv)) : undefined;\n case \"find\":\n return unwrapFind(argv);\n default:\n return undefined;\n }\n }\n\n /** Check a `cd` (shell mode): literal, one operand, inside the jail from every possible cwd. */\n public checkCd(words: ShellWord[], cwds: string[]): string[] {\n const argv = words.map((word) => word.value);\n\n if (this.deny.has(\"cd\")) {\n throw new Refusal(\"denied-executable\", \"`cd` is denied by the workspace shell policy.\", \"cd\");\n }\n\n this.checkHooks(argv, false);\n this.commands.push(argv);\n\n const operands = words.slice(1);\n\n while (\n this.dialect === \"posix\" &&\n operands.length > 0 &&\n [\"-P\", \"-L\", \"--\"].includes(operands[0]?.value ?? \"\")\n ) {\n operands.shift();\n }\n\n const target = operands[0];\n\n if (operands.length !== 1 || target === undefined || target.value === \"-\" || target.dynamic) {\n throw new Refusal(\n \"unsupported-syntax\",\n \"`cd` needs exactly one literal directory inside the workspace (no `-`, `~` or variables).\",\n \"cd\",\n );\n }\n\n const next = new Set(cwds);\n\n for (const cwd of cwds) {\n const resolved = this.resolveTarget(cwd, target.value);\n\n this.assertInJail(resolved, target.value, \"cd\");\n this.paths.push({ path: resolved, access: \"cwd\", target: target.value });\n next.add(resolved);\n }\n\n if (next.size > MAX_CD_CANDIDATES) {\n throw new Refusal(\"unsupported-syntax\", \"The line changes directory too many times.\", \"cd\");\n }\n\n return [...next];\n }\n\n /** Check a redirection target from every possible cwd. */\n public checkRedirectTarget(word: ShellWord, access: \"read\" | \"write\", cwds: string[]): void {\n if (word.dynamic) {\n throw new Refusal(\n \"unsupported-syntax\",\n `The redirection target \\`${word.raw}\\` must be a literal path (no variables, globs or \\`~\\`).`,\n );\n }\n\n const value = word.value;\n\n if (\n (this.dialect === \"posix\" && DEVICE_TARGETS.has(value)) ||\n (this.dialect === \"cmd\" && CMD_DEVICE_TARGETS.has(value.toLowerCase()))\n ) {\n return;\n }\n\n for (const cwd of cwds) {\n const resolved = this.resolveTarget(cwd, value);\n\n this.assertInJail(resolved, value, \"redirect\");\n this.paths.push({ path: resolved, access, target: value });\n }\n }\n\n private resolveTarget(cwd: string, target: string): string {\n return path.resolve(cwd, target);\n }\n\n private assertInJail(resolved: string, target: string, kind: \"cd\" | \"redirect\"): void {\n if (isInsideJailLexically(this.policy, resolved)) {\n return;\n }\n\n throw new Refusal(\n \"path-escape\",\n kind === \"cd\"\n ? `\\`cd ${target}\\` leaves the workspace jail (or enters a denied path); commands must stay inside the workspace.`\n : `The redirection target \\`${target}\\` is outside the workspace jail or on a denied path.`,\n undefined,\n target,\n );\n }\n}\n\n/**\n * Turn a wrapper's \"inner command starts at `start`\" into a range; a\n * wrapper that runs nothing (`start === argv.length`) becomes `undefined`.\n */\nfunction suffixFrom(\n argv: string[],\n start: number | null,\n): Array<[number, number]> | null | undefined {\n if (start === null) {\n return null;\n }\n\n return start < argv.length ? [[start, argv.length]] : undefined;\n}\n\n/** `nohup` / `time` / `command` / `builtin`: skip the listed flags and `--`. */\nfunction unwrapSimple(argv: string[], flags: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (flags.includes(arg)) {\n index++;\n\n continue;\n }\n\n if (arg.startsWith(\"-\") && arg !== \"-\") {\n return null;\n }\n\n break;\n }\n\n return index;\n}\n\n/** `env [-i] [-u NAME] [NAME=value]… command…` */\nfunction unwrapEnv(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-i\" || arg === \"-\" || arg === \"--ignore-environment\") {\n index++;\n\n continue;\n }\n\n if (arg === \"-u\" || arg === \"--unset\") {\n index += 2;\n\n continue;\n }\n\n if (arg.startsWith(\"--unset=\") || (arg.startsWith(\"-u\") && arg.length > 2)) {\n index++;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n while (index < argv.length && /^[^=]+=/.test(argv[index] ?? \"\")) {\n index++;\n }\n\n return index;\n}\n\n/** `nice [-n N | -N | --adjustment=N] command…` */\nfunction unwrapNice(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-n\" || arg === \"--adjustment\") {\n index += 2;\n\n continue;\n }\n\n if (/^-n-?[0-9]+$/.test(arg) || /^--adjustment=-?[0-9]+$/.test(arg) || /^-[0-9]+$/.test(arg)) {\n index++;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n return index;\n}\n\n/** `timeout [options] DURATION command…` */\nfunction unwrapTimeout(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-s\" || arg === \"--signal\" || arg === \"-k\" || arg === \"--kill-after\") {\n index += 2;\n\n continue;\n }\n\n if (\n /^-[sk].+$/.test(arg) ||\n arg.startsWith(\"--signal=\") ||\n arg.startsWith(\"--kill-after=\") ||\n arg === \"--preserve-status\" ||\n arg === \"--foreground\" ||\n arg === \"-v\" ||\n arg === \"--verbose\"\n ) {\n index++;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n // The duration, then the command.\n if (index >= argv.length - 1) {\n return null;\n }\n\n return index + 1;\n}\n\n/** `exec [-cl] [-a name] command…` (bash). */\nfunction unwrapExec(argv: string[]): number | null {\n let index = 1;\n\n while (index < argv.length) {\n const arg = argv[index] ?? \"\";\n\n if (arg === \"-c\" || arg === \"-l\" || arg === \"-cl\" || arg === \"-lc\") {\n index++;\n\n continue;\n }\n\n if (arg === \"-a\") {\n index += 2;\n\n continue;\n }\n\n if (arg === \"--\") {\n index++;\n\n break;\n }\n\n if (arg.startsWith(\"-\")) {\n return null;\n }\n\n break;\n }\n\n return index;\n}\n\n/** The commands `find -exec / -execdir / -ok / -okdir … ;|+` runs. */\nfunction unwrapFind(argv: string[]): Array<[number, number]> | null | undefined {\n const inner: Array<[number, number]> = [];\n\n for (let index = 1; index < argv.length; index++) {\n const arg = argv[index] ?? \"\";\n\n if (arg !== \"-exec\" && arg !== \"-execdir\" && arg !== \"-ok\" && arg !== \"-okdir\") {\n continue;\n }\n\n const start = index + 1;\n let end = start;\n\n while (end < argv.length && argv[end] !== \";\" && argv[end] !== \"+\") {\n end++;\n }\n\n if (end >= argv.length || end === start) {\n return null;\n }\n\n inner.push([start, end]);\n index = end;\n }\n\n return inner.length > 0 ? inner : undefined;\n}\n\n/** Build the argv-mode refusal for a line that is not a single argv. */\nfunction argvModeRefusal(command: string): Refusal {\n const analysis = analyzeArgvCommand(command);\n\n if (analysis.ok) {\n // Not reached: callers only ask for a refusal of a line that failed.\n return new Refusal(\"metacharacter\", \"The command cannot run as a single argv.\");\n }\n\n if (analysis.type === \"empty\") {\n return new Refusal(\"not-allowed\", \"The command is empty.\");\n }\n\n if (analysis.type === \"unbalanced-quotes\") {\n return new Refusal(\n \"unbalanced-quotes\",\n `The command has an unbalanced \\`${analysis.quote}\\` quote. Close it, or quote the argument with the other kind of quote.`,\n );\n }\n\n const token = analysis.token;\n const suffix = \" (run_shell executes a single command without a shell.)\";\n const parsed = parseShellLine(command, \"posix\");\n const segments: ShellSegment[] = parsed.ok ? parsed.segments : [];\n const listOperator = token === \"&&\" || token === \"||\" || token === \";\" || token === \"\\n\";\n\n if (listOperator && segments.length >= 2) {\n const shownOperator = token === \"\\n\" ? \"A line break\" : `\\`${token}\\``;\n const firstWords = (segments[0]?.source ?? \"\").trim().split(/\\s+/);\n\n if (firstWords[0] === \"cd\") {\n const rest = segments.slice(1).map((segment) => show(segment.source));\n\n return new Refusal(\n \"metacharacter\",\n `${shownOperator} is not supported in argv mode, and \\`cd\\` does not carry over: every command starts in the workspace root. Run ${rest.join(\", then \")} on its own, with paths relative to the root.${suffix}`,\n );\n }\n\n const steps = segments.map((segment) => show(segment.source));\n const joiner = token === \"||\" ? \", and if it fails, \" : \", then \";\n\n return new Refusal(\n \"metacharacter\",\n `${shownOperator} is not supported in argv mode: run ${steps.join(joiner)}, as separate commands.${suffix}`,\n );\n }\n\n if (token === \"&\" && segments.length >= 1) {\n const steps = segments.map((segment) => show(segment.source));\n const run =\n steps.length === 1\n ? `run ${steps[0] ?? \"\"} without it`\n : `run ${steps.join(\", then \")} as separate commands`;\n\n return new Refusal(\n \"metacharacter\",\n `\\`&\\` is not supported in argv mode: ${run}; start a long-running command with background: true.${suffix}`,\n );\n }\n\n if (token === \"|\") {\n const pipeline = command\n .split(\"|\")\n .map((part) => part.trim())\n .filter(Boolean);\n\n return new Refusal(\n \"metacharacter\",\n `\\`|\\` (a pipe) is not supported in argv mode: run ${show(pipeline[0] ?? command)} on its own and read its output yourself.${suffix}`,\n );\n }\n\n if (token === \">\" || token === \">>\" || token === \">&\") {\n return new Refusal(\n \"metacharacter\",\n `\\`${token}\\` (redirection) is not supported in argv mode: run the command without it and save the output with write_file if you need it.${suffix}`,\n );\n }\n\n if (token === \"<\" || token === \"<<\" || token === \"<&\") {\n return new Refusal(\n \"metacharacter\",\n `\\`${token}\\` (input redirection) is not supported in argv mode: pass the file path to the command as an argument.${suffix}`,\n );\n }\n\n if (token === \"$(\" || token === \"`\") {\n return new Refusal(\n \"metacharacter\",\n `Command substitution (${token === \"$(\" ? \"`$(…)`\" : \"backticks\"}) is not supported in argv mode: run the inner command first, then pass its output literally.${suffix}`,\n );\n }\n\n if (token === \"$\" || token === \"${\") {\n return new Refusal(\n \"metacharacter\",\n `\\`$\\` (variable expansion) is not supported in argv mode: write the value literally, or put the \\`$\\` inside quotes to pass it as text.${suffix}`,\n );\n }\n\n if (token === \"(\" || token === \")\") {\n return new Refusal(\n \"metacharacter\",\n `\\`${token}\\` is not supported in argv mode: put it inside quotes to pass it as text.${suffix}`,\n );\n }\n\n return new Refusal(\n \"metacharacter\",\n `\\`${token === \"\\n\" ? \"\\\\n\" : token}\\` is not supported in argv mode: run one command per call, and quote any argument that contains it.${suffix}`,\n );\n}\n\n/** Check a shell-mode (`interpreter: \"sh\"`) line. */\nfunction checkShellLine(\n checker: LineChecker,\n policy: WorkspacePolicy,\n command: string,\n dialect: ShellDialect,\n): void {\n if (/^[-+]/.test(command)) {\n throw new Refusal(\"unsupported-syntax\", \"A command line may not start with `-` or `+`.\");\n }\n\n const parsed = parseShellLine(command, dialect);\n\n if (!parsed.ok) {\n throw new Refusal(\n parsed.reason,\n `${parsed.message} The workspace shell policy only runs lines it can check.`,\n );\n }\n\n if (parsed.background) {\n throw new Refusal(\n \"unsupported-syntax\",\n \"`&` (run in the background) is not supported in a command line: start a long-running command with background: true instead.\",\n );\n }\n\n let cwds = [path.resolve(policy.cwd)];\n\n for (const simple of parsed.commands) {\n cwds = checkSimpleCommand(checker, simple, cwds, dialect);\n }\n}\n\n/** Check one parsed simple command; returns the possible working directories after it. */\nfunction checkSimpleCommand(\n checker: LineChecker,\n simple: ShellSimpleCommand,\n cwds: string[],\n dialect: ShellDialect,\n): string[] {\n for (const assignment of simple.assignments) {\n const name = assignment.assignmentName ?? \"\";\n\n if (SENSITIVE_VARIABLES.has(name) || name.startsWith(\"BASH_FUNC_\")) {\n throw new Refusal(\n \"unsupported-syntax\",\n `Assigning \\`${name}\\` is not supported: it changes how the shell runs the commands that follow.`,\n );\n }\n }\n\n for (const redirect of simple.redirects) {\n if (redirect.type === \"read\" || redirect.type === \"write\") {\n checker.checkRedirectTarget(redirect.target, redirect.type, cwds);\n }\n }\n\n const first = simple.words[0];\n\n if (first === undefined) {\n return cwds;\n }\n\n if (!first.dynamic && first.value.includes(\"=\")) {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${first.raw}\\` is not a plain command name or \\`NAME=value\\` assignment.`,\n );\n }\n\n if (/^[-+]/.test(first.value)) {\n throw new Refusal(\"unsupported-syntax\", `\\`${first.raw}\\` is not a command name.`);\n }\n\n if (checker.hasArgumentHooks) {\n const dynamic = simple.words.find((word) => word.dynamic);\n\n if (dynamic !== undefined) {\n throw new Refusal(\n \"unsupported-syntax\",\n `\\`${dynamic.raw}\\` is decided when the line runs, so it cannot be checked against the workspace shell policy's command rules. Write the value literally (quote globs).`,\n );\n }\n }\n\n const executable = first.dynamic ? \"\" : executableBasename(first.value, dialect).toLowerCase();\n\n if (executable === \"cd\") {\n return checker.checkCd(simple.words, cwds);\n }\n\n if (dialect === \"posix\" && (executable === \"pushd\" || executable === \"popd\")) {\n throw new Refusal(\"unsupported-syntax\", `\\`${executable}\\` is not supported; use \\`cd\\`.`);\n }\n\n checker.checkArgv(\n simple.words.map((word) => (word.dynamic ? word.raw : word.value)),\n simple.words.map((word) => word.dynamic),\n 0,\n );\n\n return cwds;\n}\n\n/**\n * The shell policy's verdict on a command line, without touching the\n * filesystem.\n *\n * - **argv mode** (`interpreter: \"none\"`, the default): the line must\n * tokenize into one argv; otherwise it is refused with a\n * `\"metacharacter\"` / `\"unbalanced-quotes\"` reason and a message that\n * names the operator and suggests the split.\n * - **shell mode** (`interpreter: \"sh\"`): the line is parsed and every\n * simple command — inside `;`, `&&`, `||`, pipes, subshells, `$( )`,\n * backticks and here-documents — is checked; redirection targets and `cd`\n * directories must stay in the jail (checked lexically here; `exec`\n * re-checks them with symlinks resolved).\n *\n * Every argv goes through `deny` (case-insensitive, wins), `allow`\n * (exhaustive when non-empty), `denyPatterns` and `allowCommand`.\n * Wrappers (`env`, `time`, `timeout`, `nice`, `nohup`, `command`,\n * `builtin`, `exec`) and `find -exec` are followed to the command they run.\n * A runner the policy cannot see into (`bash -c`, `eval`, `xargs`, `sudo`,\n * …) is refused while the policy has deny rules, unless `allow` names it.\n *\n * @example\n * checkShellCommand({ cwd, shell: { allow: [\"git\"] } }, \"git add -A && git commit -m wip\");\n * // { allowed: false, reason: \"metacharacter\", message: \"`&&` is not supported in argv mode: run `git add -A`, then `git commit -m wip`, …\" }\n */\nexport function checkShellCommand(policy: WorkspacePolicy, command: string): WorkspaceShellCheck {\n return checkShellCommandInDialect(policy, command, shellDialect());\n}\n\n/**\n * {@link checkShellCommand} with the shell-mode dialect chosen explicitly\n * instead of from the platform (used to test the cmd.exe rules anywhere).\n * Argv mode ignores `dialect`.\n */\nexport function checkShellCommandInDialect(\n policy: WorkspacePolicy,\n command: string,\n dialect: ShellDialect,\n): WorkspaceShellCheck {\n const shell = policy.shell;\n\n if (!shell) {\n return {\n allowed: false,\n reason: \"not-allowed\",\n message: \"The workspace has no shell policy, so no command may run.\",\n commands: [],\n paths: [],\n };\n }\n\n const interpreter = shell.interpreter ?? \"none\";\n const checker = new LineChecker(policy, shell, command, interpreter === \"sh\" ? dialect : \"posix\");\n\n try {\n if (command.trim() === \"\") {\n throw new Refusal(\"not-allowed\", \"The command is empty.\");\n }\n\n if (interpreter === \"sh\") {\n checkShellLine(checker, policy, command, dialect);\n } else {\n const argv = tokenizeCommand(command);\n\n if (argv === null) {\n throw argvModeRefusal(command);\n }\n\n checker.checkArgv(\n argv,\n argv.map(() => false),\n 0,\n );\n }\n\n return { allowed: true, commands: checker.commands, paths: checker.paths };\n } catch (error) {\n if (!(error instanceof Refusal)) {\n throw error;\n }\n\n return {\n allowed: false,\n reason: error.reason,\n message: error.message,\n ...(error.executable !== undefined ? { executable: error.executable } : {}),\n ...(error.refusedPath !== undefined ? { path: error.refusedPath } : {}),\n commands: checker.commands,\n paths: checker.paths,\n };\n }\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ const MAX_NESTING = 16;
6
6
  * appear unquoted in command position. The parser does not read compound
7
7
  * commands, so a line using one is refused.
8
8
  */
9
- const RESERVED_WORDS = new Set([
9
+ const RESERVED_WORDS = /* @__PURE__ */ new Set([
10
10
  "!",
11
11
  "{",
12
12
  "}",
@@ -30,7 +30,7 @@ const RESERVED_WORDS = new Set([
30
30
  "while"
31
31
  ]);
32
32
  /** cmd.exe keywords the `"cmd"` dialect refuses in command position. */
33
- const CMD_RESERVED_WORDS = new Set([
33
+ const CMD_RESERVED_WORDS = /* @__PURE__ */ new Set([
34
34
  "if",
35
35
  "for",
36
36
  "goto",
@@ -1 +1 @@
1
- {"version":3,"file":"shell-parser.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/shell-parser.ts"],"sourcesContent":["/**\n * A deliberately conservative parser for the command lines `run_shell`\n * hands to a shell when the policy opts in to `interpreter: \"sh\"`.\n *\n * Its only job is to find **every simple command** a line would run — so\n * the policy can check each one — plus every redirection target. It is not\n * a shell: it never expands anything. What it cannot read with confidence\n * it refuses, because a construct the parser misreads is a construct a\n * denied command can hide in. `/bin/sh` is bash on some systems, dash or\n * busybox ash on others, so constructs that those shells read differently\n * (bash-only syntax such as `<<<`, `&>`, `$'…'`, brace expansion, `[[`,\n * arithmetic) are refused as well.\n *\n * Two dialects:\n * - `\"posix\"` — what `/bin/sh -c` reads: `;`, `&&`, `||`, `|`, `&`,\n * newlines, comments, `( … )` subshells, `$( … )` and backtick command\n * substitution (parsed recursively), `$name` / `${name…}` parameter\n * expansion (a restricted set of forms), single and double quotes,\n * backslash escapes, redirections and here-documents.\n * - `\"cmd\"` — what `cmd.exe /d /v:off /s /c` reads, restricted to the\n * subset whose meaning is unambiguous: `&&`, `||`, `|`, `&` (sequential\n * in cmd), double quotes and simple redirections. `%`, `!`, `^`,\n * parentheses, `;` and `,` are refused.\n */\n\n/** Which shell grammar to parse. */\nexport type ShellDialect = \"posix\" | \"cmd\";\n\n/** One word of a simple command, after quote removal. */\nexport interface ShellWord {\n /** The word exactly as written. */\n raw: string;\n /**\n * The word's text after quote removal and escapes. Expansions\n * (`$HOME`, `$(…)`, backticks) are kept as their source text — their real\n * value is only known when the shell runs.\n */\n value: string;\n /**\n * True when the shell decides part of the word at run time: a parameter\n * expansion, a command substitution, an unquoted glob character\n * (`*`, `?`, `[`) or a leading `~`.\n */\n dynamic: boolean;\n /** True when any part of the word was quoted or escaped. */\n quoted: boolean;\n /** For a `NAME=value` assignment word, the variable name. */\n assignmentName?: string;\n}\n\n/** A redirection attached to a simple command (or a subshell). */\nexport interface ShellRedirect {\n /** The operator: `>`, `>>`, `>|`, `<`, `<>`, `>&`, `<&`, `<<` or `<<-`. */\n op: string;\n /** Explicit file descriptor (`2>`), when written. */\n fd?: number;\n /** The target word (file, descriptor number or here-doc delimiter). */\n target: ShellWord;\n /**\n * `\"read\"` / `\"write\"` — a file target; `\"dup\"` — a descriptor copy or\n * close (`2>&1`, `>&-`); `\"heredoc\"` — inline input.\n */\n type: \"read\" | \"write\" | \"dup\" | \"heredoc\";\n}\n\n/** A simple command: assignments, words and redirections. */\nexport interface ShellSimpleCommand {\n /** Leading `NAME=value` words. */\n assignments: ShellWord[];\n /** The command's words — `words[0]` is the command name. Empty for a redirect- or assignment-only command. */\n words: ShellWord[];\n /** Redirections, in order. */\n redirects: ShellRedirect[];\n /** The command's source text (for messages). */\n source: string;\n}\n\n/** A top-level piece of the line and the operator that follows it. */\nexport interface ShellSegment {\n /** Source text of the pipeline. */\n source: string;\n /** The list operator after it (`&&`, `||`, `;`, `&`, a newline), if any. */\n operator?: string;\n}\n\n/** Outcome of {@link parseShellLine}. */\nexport type ShellParseResult =\n | {\n ok: true;\n /** Every simple command, in the order the shell meets them (substitutions first). */\n commands: ShellSimpleCommand[];\n /** The top-level pipelines and the operators joining them. */\n segments: ShellSegment[];\n /** True when the line backgrounds anything with `&` (POSIX dialect). */\n background: boolean;\n }\n | {\n ok: false;\n /** Why the line was refused. */\n reason: \"unbalanced-quotes\" | \"unsupported-syntax\";\n /** What was refused and why, for the model. */\n message: string;\n };\n\n/** Deepest nesting of subshells and substitutions the parser accepts. */\nconst MAX_NESTING = 16;\n\n/**\n * Words that start a compound command (or negate a pipeline) when they\n * appear unquoted in command position. The parser does not read compound\n * commands, so a line using one is refused.\n */\nconst RESERVED_WORDS = new Set([\n \"!\",\n \"{\",\n \"}\",\n \"[[\",\n \"]]\",\n \"case\",\n \"coproc\",\n \"do\",\n \"done\",\n \"elif\",\n \"else\",\n \"esac\",\n \"fi\",\n \"for\",\n \"function\",\n \"if\",\n \"in\",\n \"select\",\n \"then\",\n \"until\",\n \"while\",\n]);\n\n/** cmd.exe keywords the `\"cmd\"` dialect refuses in command position. */\nconst CMD_RESERVED_WORDS = new Set([\"if\", \"for\", \"goto\", \"setlocal\", \"endlocal\"]);\n\n/** A parameter name inside `${…}`: a variable, a positional or a special parameter. */\nconst PARAMETER_NAME = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!-])`;\n\n/**\n * The default/pattern word of `${name:-word}` / `${name%word}`: no quotes,\n * no nested expansion or substitution, no braces or parentheses, no `@`.\n */\nconst PARAMETER_WORD = \"[^`$'\\\"\\\\\\\\{}()@\\\\n]*\";\n\n/**\n * Allowed `${…}` forms: `${name}`, `${#name}`, `${name-word}`,\n * `${name:-word}` (also `=`, `+`, `?`), `${name%word}`, `${name%%word}`,\n * `${name#word}`, `${name##word}`. Everything else — substrings and\n * subscripts (arithmetic in bash), `${!name}`, `${name@op}`, pattern\n * substitution — is refused.\n */\nconst PARAMETER_EXPANSION = new RegExp(\n `^(?:#${PARAMETER_NAME}|${PARAMETER_NAME}(?:(?::?[-=+?]|%%?|##?)${PARAMETER_WORD})?)$`,\n);\n\n/** A refusal raised while parsing; caught by {@link parseShellLine}. */\nclass ShellSyntaxRefusal extends Error {\n public constructor(\n public readonly reason: \"unbalanced-quotes\" | \"unsupported-syntax\",\n message: string,\n ) {\n super(message);\n }\n}\n\n/** Refuse a construct the parser does not read with confidence. */\nfunction unsupported(message: string): ShellSyntaxRefusal {\n return new ShellSyntaxRefusal(\"unsupported-syntax\", message);\n}\n\n/** Refuse an unterminated quote / substitution. */\nfunction unbalanced(message: string): ShellSyntaxRefusal {\n return new ShellSyntaxRefusal(\"unbalanced-quotes\", message);\n}\n\n/** State shared by a parser and the nested parsers it starts (backticks, here-docs). */\ninterface SharedState {\n commands: ShellSimpleCommand[];\n background: boolean;\n nesting: number;\n}\n\n/** A here-document whose body starts after the next newline. */\ninterface PendingHeredoc {\n delimiter: string;\n stripTabs: boolean;\n expand: boolean;\n}\n\n/** Parser for the POSIX `sh` dialect. */\nclass PosixParser {\n private pos = 0;\n\n private pendingHeredocs: PendingHeredoc[] = [];\n\n public constructor(\n private readonly src: string,\n private readonly state: SharedState,\n ) {}\n\n /** Parse the whole line as a command list. */\n public parseProgram(segments: ShellSegment[]): void {\n this.parseList(null, segments);\n this.assertNoPendingHeredocs();\n }\n\n /** Parse the contents of a backtick substitution (a full list until EOF). */\n public parseNestedProgram(): void {\n this.parseList(null, undefined);\n this.assertNoPendingHeredocs();\n }\n\n /**\n * Scan an unquoted-delimiter here-document body: `$…`, `${…}`, `$(…)` and\n * backticks expand there exactly as inside double quotes (but `\"` is an\n * ordinary character), so any command substitution in it runs.\n */\n public parseExpandingText(): void {\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (char === \"\\\\\") {\n this.pos += 2;\n\n continue;\n }\n\n if (char === \"$\") {\n this.parseDollar(\"heredoc\");\n\n continue;\n }\n\n if (char === \"`\") {\n this.parseBackticks(false);\n\n continue;\n }\n\n this.pos++;\n }\n }\n\n private atEnd(): boolean {\n return this.pos >= this.src.length;\n }\n\n private peek(offset = 0): string {\n return this.src.charAt(this.pos + offset);\n }\n\n private assertNoPendingHeredocs(): void {\n if (this.pendingHeredocs.length > 0) {\n throw unsupported(\n `The here-document delimited by \\`${this.pendingHeredocs[0]?.delimiter ?? \"\"}\\` is never terminated.`,\n );\n }\n }\n\n private enterNesting(): void {\n this.state.nesting++;\n\n if (this.state.nesting > MAX_NESTING) {\n throw unsupported(\"The line nests subshells or substitutions too deeply.\");\n }\n }\n\n private exitNesting(): void {\n this.state.nesting--;\n }\n\n /** Skip spaces, tabs and backslash-newline continuations. */\n private skipBlanks(): void {\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (char === \" \" || char === \"\\t\") {\n this.pos++;\n\n continue;\n }\n\n if (char === \"\\\\\" && this.peek(1) === \"\\n\") {\n this.pos += 2;\n\n continue;\n }\n\n break;\n }\n }\n\n /** Skip a `#` comment up to (not including) the newline. */\n private skipComment(): void {\n const newline = this.src.indexOf(\"\\n\", this.pos);\n\n this.pos = newline === -1 ? this.src.length : newline;\n }\n\n /** Consume one newline and read any here-document bodies queued before it. */\n private consumeNewline(): void {\n this.pos++;\n\n if (this.pendingHeredocs.length === 0) {\n return;\n }\n\n if (this.state.nesting > 0) {\n throw unsupported(\"Here-documents inside subshells or substitutions are not supported.\");\n }\n\n const pending = this.pendingHeredocs;\n\n this.pendingHeredocs = [];\n\n for (const heredoc of pending) {\n this.readHeredocBody(heredoc);\n }\n }\n\n /** Skip blanks, comments and newlines (separators between commands). */\n private skipBlanksAndNewlines(): void {\n while (!this.atEnd()) {\n this.skipBlanks();\n\n if (this.peek() === \"#\") {\n this.skipComment();\n\n continue;\n }\n\n if (this.peek() === \"\\n\") {\n this.consumeNewline();\n\n continue;\n }\n\n break;\n }\n }\n\n private readHeredocBody(heredoc: PendingHeredoc): void {\n const bodyLines: string[] = [];\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n if (this.atEnd()) {\n throw unsupported(\n `The here-document delimited by \\`${heredoc.delimiter}\\` is never terminated.`,\n );\n }\n\n const lineEnd = this.src.indexOf(\"\\n\", this.pos);\n const line = lineEnd === -1 ? this.src.slice(this.pos) : this.src.slice(this.pos, lineEnd);\n const compared = heredoc.stripTabs ? line.replace(/^\\t+/, \"\") : line;\n\n this.pos = lineEnd === -1 ? this.src.length : lineEnd + 1;\n\n if (compared === heredoc.delimiter) {\n break;\n }\n\n bodyLines.push(compared);\n }\n\n if (heredoc.expand) {\n new PosixParser(bodyLines.join(\"\\n\"), this.state).parseExpandingText();\n }\n }\n\n /**\n * Parse a command list until `closing` (`\")\"`) or the end of the input.\n * `segments`, when given, collects the top-level pipelines.\n */\n private parseList(closing: string | null, segments: ShellSegment[] | undefined): void {\n let sawCommand = false;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanksAndNewlines();\n\n if (this.atEnd()) {\n if (closing !== null) {\n throw unbalanced(`A \\`(\\` or \\`$(\\` is never closed with \\`${closing}\\`.`);\n }\n\n break;\n }\n\n if (closing !== null && this.peek() === closing) {\n break;\n }\n\n const start = this.pos;\n\n this.parsePipeline(closing);\n\n const end = this.pos;\n\n sawCommand = true;\n this.skipBlanks();\n\n if (this.peek() === \"#\") {\n this.skipComment();\n }\n\n const operator = this.readListOperator();\n\n segments?.push({\n source: this.src.slice(start, end).trim(),\n ...(operator !== null ? { operator } : {}),\n });\n\n if (operator === \"&&\" || operator === \"||\") {\n this.skipBlanksAndNewlines();\n\n if (this.atEnd() || (closing !== null && this.peek() === closing)) {\n throw unsupported(`The line ends with \\`${operator}\\` but no command follows it.`);\n }\n\n continue;\n }\n\n if (operator === \"&\") {\n this.state.background = true;\n }\n\n if (operator === null) {\n if (this.atEnd() || (closing !== null && this.peek() === closing)) {\n break;\n }\n\n throw unsupported(`Unexpected \\`${this.peek()}\\`.`);\n }\n }\n\n if (!sawCommand) {\n throw unsupported(closing === null ? \"The command is empty.\" : \"A `( )` or `$( )` is empty.\");\n }\n }\n\n /** Read `&&`, `||`, `;`, `&` or a newline after a pipeline. */\n private readListOperator(): string | null {\n const char = this.peek();\n const next = this.peek(1);\n\n if (char === \"&\") {\n if (next === \"&\") {\n this.pos += 2;\n\n return \"&&\";\n }\n\n if (next === \">\") {\n throw unsupported(\n \"`&>` (redirect both streams) is not portable; write `> file 2>&1` instead.\",\n );\n }\n\n this.pos++;\n\n return \"&\";\n }\n\n if (char === \"|\" && next === \"|\") {\n this.pos += 2;\n\n return \"||\";\n }\n\n if (char === \";\") {\n if (next === \";\") {\n throw unsupported(\"`;;` (a `case` clause) is not supported.\");\n }\n\n this.pos++;\n\n return \";\";\n }\n\n if (char === \"\\n\") {\n this.consumeNewline();\n\n return \"\\n\";\n }\n\n return null;\n }\n\n private parsePipeline(closing: string | null): void {\n this.parseCommand(closing);\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.peek() !== \"|\" || this.peek(1) === \"|\") {\n return;\n }\n\n if (this.peek(1) === \"&\") {\n throw unsupported(\"`|&` is not portable; write `2>&1 |` instead.\");\n }\n\n this.pos++;\n this.skipBlanksAndNewlines();\n\n if (this.atEnd() || (closing !== null && this.peek() === closing)) {\n throw unsupported(\"The line ends with `|` but no command follows it.\");\n }\n\n this.parseCommand(closing);\n }\n }\n\n private parseCommand(closing: string | null): void {\n this.skipBlanks();\n\n if (this.peek() === \"(\") {\n if (this.peek(1) === \"(\") {\n throw unsupported(\"`((…))` (arithmetic) is not supported.\");\n }\n\n this.parseSubshell();\n\n return;\n }\n\n if (this.peek() === \")\") {\n throw unsupported(closing === null ? \"Unexpected `)`.\" : \"A `( )` or `$( )` is empty.\");\n }\n\n this.parseSimpleCommand();\n }\n\n private parseSubshell(): void {\n const start = this.pos;\n\n this.pos++;\n this.enterNesting();\n this.parseList(\")\", undefined);\n\n if (this.peek() !== \")\") {\n throw unbalanced(\"A `(` is never closed with `)`.\");\n }\n\n this.pos++;\n this.exitNesting();\n\n // Redirections may follow the closing parenthesis; nothing else may.\n const redirects: ShellRedirect[] = [];\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (!this.atRedirectStart()) {\n break;\n }\n\n redirects.push(this.parseRedirect());\n }\n\n if (redirects.length > 0) {\n this.state.commands.push({\n assignments: [],\n words: [],\n redirects,\n source: this.src.slice(start, this.pos).trim(),\n });\n }\n\n if (!this.atEnd() && !this.atOperatorBoundary()) {\n throw unsupported(\"Only an operator or a redirection may follow `)`.\");\n }\n }\n\n /** Whether the next character ends a simple command. */\n private atOperatorBoundary(): boolean {\n const char = this.peek();\n\n return (\n char === \"\\n\" ||\n char === \";\" ||\n char === \"&\" ||\n char === \"|\" ||\n char === \")\" ||\n char === \"#\" ||\n char === \" \" ||\n char === \"\\t\"\n );\n }\n\n /** Whether a redirection starts here (`>`, `<`, or digits glued to one). */\n private atRedirectStart(): boolean {\n let index = this.pos;\n\n while (index < this.src.length && /[0-9]/.test(this.src.charAt(index))) {\n index++;\n }\n\n const char = this.src.charAt(index);\n\n return char === \"<\" || char === \">\";\n }\n\n private parseSimpleCommand(): void {\n const start = this.pos;\n const command: ShellSimpleCommand = { assignments: [], words: [], redirects: [], source: \"\" };\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.atEnd()) {\n break;\n }\n\n const char = this.peek();\n\n if (char === \"\\n\" || char === \";\" || char === \"&\" || char === \"|\" || char === \")\") {\n break;\n }\n\n if (char === \"(\") {\n throw unsupported(\n \"`(` is only supported at the start of a command (a subshell); quote it to pass it as text.\",\n );\n }\n\n if (char === \"#\") {\n this.skipComment();\n\n break;\n }\n\n if (this.atRedirectStart()) {\n command.redirects.push(this.parseRedirect());\n\n continue;\n }\n\n const word = this.parseWord();\n\n if (command.words.length === 0 && word.assignmentName !== undefined) {\n command.assignments.push(word);\n\n continue;\n }\n\n command.words.push(word);\n\n if (command.words.length === 1 && !word.quoted && RESERVED_WORDS.has(word.value)) {\n throw unsupported(\n `\\`${word.value}\\` starts a shell compound command, which is not supported; run the commands one at a time.`,\n );\n }\n }\n\n if (\n command.words.length === 0 &&\n command.assignments.length === 0 &&\n command.redirects.length === 0\n ) {\n throw unsupported(\"The line has an empty command (two operators in a row?).\");\n }\n\n command.source = this.src.slice(start, this.pos).trim();\n this.state.commands.push(command);\n }\n\n private parseRedirect(): ShellRedirect {\n let digits = \"\";\n\n while (/[0-9]/.test(this.peek())) {\n digits += this.peek();\n this.pos++;\n }\n\n const rest = this.src.slice(this.pos, this.pos + 3);\n let op: string;\n\n if (rest.startsWith(\"<<<\")) {\n throw unsupported(\"`<<<` (a here-string) is not portable; use a here-document.\");\n } else if (rest.startsWith(\"<<-\")) {\n op = \"<<-\";\n } else if (rest.startsWith(\"<<\")) {\n op = \"<<\";\n } else if (rest.startsWith(\"<>\")) {\n op = \"<>\";\n } else if (rest.startsWith(\"<&\")) {\n op = \"<&\";\n } else if (rest.startsWith(\"<(\") || rest.startsWith(\">(\")) {\n throw unsupported(\"Process substitution (`<(…)`, `>(…)`) is not supported.\");\n } else if (rest.startsWith(\">>\")) {\n op = \">>\";\n } else if (rest.startsWith(\">|\")) {\n op = \">|\";\n } else if (rest.startsWith(\">&\")) {\n op = \">&\";\n } else {\n op = this.peek();\n }\n\n this.pos += op.length;\n\n const fd = digits === \"\" ? undefined : Number(digits);\n\n this.skipBlanks();\n\n if (this.atEnd() || /[\\n;&|<>()]/.test(this.peek())) {\n throw unsupported(`The redirection \\`${op}\\` has no target.`);\n }\n\n const target = this.parseWord();\n\n if (op === \"<<\" || op === \"<<-\") {\n if (this.state.nesting > 0) {\n throw unsupported(\"Here-documents inside subshells or substitutions are not supported.\");\n }\n\n if (target.dynamic) {\n throw unsupported(\"A here-document delimiter must be literal text.\");\n }\n\n this.pendingHeredocs.push({\n delimiter: target.value,\n stripTabs: op === \"<<-\",\n expand: !target.quoted,\n });\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: \"heredoc\" };\n }\n\n if (op === \">&\" || op === \"<&\") {\n if (target.dynamic || !/^(?:[0-9]+|-)$/.test(target.value)) {\n throw unsupported(\n `\\`${op}${target.raw}\\` is not portable; write \\`> file 2>&1\\` to send both streams to a file.`,\n );\n }\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: \"dup\" };\n }\n\n return {\n op,\n ...(fd !== undefined ? { fd } : {}),\n target,\n type: op === \"<\" ? \"read\" : \"write\",\n };\n }\n\n /** Parse one unquoted-context word. */\n private parseWord(): ShellWord {\n const start = this.pos;\n let value = \"\";\n let dynamic = false;\n let quoted = false;\n let braces = false;\n let nameCandidate = true;\n let assignmentName: string | undefined;\n\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (\n char === \" \" ||\n char === \"\\t\" ||\n char === \"\\n\" ||\n char === \";\" ||\n char === \"&\" ||\n char === \"|\" ||\n char === \"<\" ||\n char === \">\" ||\n char === \"(\" ||\n char === \")\"\n ) {\n break;\n }\n\n if (char === \"\\r\" || char === \"\\0\") {\n throw unsupported(\"Carriage returns and NUL bytes are not supported in a command line.\");\n }\n\n if (char === \"\\\\\") {\n const next = this.peek(1);\n\n if (next === \"\") {\n throw unsupported(\"The line ends with a backslash.\");\n }\n\n this.pos += 2;\n\n if (next !== \"\\n\") {\n value += next;\n quoted = true;\n nameCandidate = false;\n }\n\n continue;\n }\n\n if (char === \"'\") {\n const closing = this.src.indexOf(\"'\", this.pos + 1);\n\n if (closing === -1) {\n throw unbalanced(\"A `'` quote is never closed.\");\n }\n\n value += this.src.slice(this.pos + 1, closing);\n quoted = true;\n nameCandidate = false;\n this.pos = closing + 1;\n\n continue;\n }\n\n if (char === '\"') {\n const part = this.parseDoubleQuoted();\n\n value += part.value;\n dynamic = dynamic || part.dynamic;\n quoted = true;\n nameCandidate = false;\n\n continue;\n }\n\n if (char === \"$\") {\n const part = this.parseDollar(\"unquoted\");\n\n value += part.text;\n dynamic = dynamic || part.dynamic;\n nameCandidate = false;\n\n continue;\n }\n\n if (char === \"`\") {\n value += this.parseBackticks(false);\n dynamic = true;\n nameCandidate = false;\n\n continue;\n }\n\n if (char === \"=\" && nameCandidate && assignmentName === undefined) {\n if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {\n assignmentName = value;\n }\n\n nameCandidate = false;\n } else if (!/[A-Za-z0-9_]/.test(char)) {\n nameCandidate = false;\n }\n\n if (char === \"*\" || char === \"?\" || char === \"[\") {\n dynamic = true;\n }\n\n if (char === \"~\") {\n const previous = this.src.charAt(this.pos - 1);\n\n if (\n this.pos === start ||\n (assignmentName !== undefined && (previous === \"=\" || previous === \":\"))\n ) {\n dynamic = true;\n }\n }\n\n if (char === \"{\" || char === \"}\") {\n braces = true;\n }\n\n value += char;\n this.pos++;\n }\n\n const raw = this.src.slice(start, this.pos);\n\n if (braces && raw !== \"{}\") {\n throw unsupported(\n `Unquoted \\`{\\` or \\`}\\` in \\`${raw}\\` (brace expansion or a command group) is not supported; quote it.`,\n );\n }\n\n // A lone `[` or `]` is never a glob (it is the `test` command or its end).\n if (raw === \"[\" || raw === \"]\") {\n dynamic = false;\n }\n\n return {\n raw,\n value,\n dynamic,\n quoted,\n ...(assignmentName !== undefined ? { assignmentName } : {}),\n };\n }\n\n /** Parse a `\"…\"` span (cursor on the opening quote). */\n private parseDoubleQuoted(): { value: string; dynamic: boolean } {\n this.pos++;\n\n let value = \"\";\n let dynamic = false;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n if (this.atEnd()) {\n throw unbalanced('A `\"` quote is never closed.');\n }\n\n const char = this.peek();\n\n if (char === '\"') {\n this.pos++;\n\n return { value, dynamic };\n }\n\n if (char === \"\\0\") {\n throw unsupported(\"NUL bytes are not supported in a command line.\");\n }\n\n if (char === \"\\\\\") {\n const next = this.peek(1);\n\n if (next === \"\") {\n throw unbalanced('A `\"` quote is never closed.');\n }\n\n if (next === \"$\" || next === \"`\" || next === '\"' || next === \"\\\\\") {\n value += next;\n this.pos += 2;\n\n continue;\n }\n\n if (next === \"\\n\") {\n this.pos += 2;\n\n continue;\n }\n\n value += char;\n this.pos++;\n\n continue;\n }\n\n if (char === \"$\") {\n const part = this.parseDollar(\"dquote\");\n\n value += part.text;\n dynamic = dynamic || part.dynamic;\n\n continue;\n }\n\n if (char === \"`\") {\n value += this.parseBackticks(true);\n dynamic = true;\n\n continue;\n }\n\n value += char;\n this.pos++;\n }\n }\n\n /** Parse a `$…` expansion (cursor on the `$`). */\n private parseDollar(context: \"unquoted\" | \"dquote\" | \"heredoc\"): {\n text: string;\n dynamic: boolean;\n } {\n const start = this.pos;\n const next = this.peek(1);\n\n if (next === \"(\") {\n if (this.peek(2) === \"(\") {\n throw unsupported(\"`$((…))` (arithmetic expansion) is not supported.\");\n }\n\n this.pos += 2;\n this.enterNesting();\n this.parseList(\")\", undefined);\n\n if (this.peek() !== \")\") {\n throw unbalanced(\"A `$(` is never closed with `)`.\");\n }\n\n this.pos++;\n this.exitNesting();\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n if (next === \"{\") {\n const closing = this.src.indexOf(\"}\", this.pos + 2);\n\n if (closing === -1) {\n throw unbalanced(\"A `${` is never closed with `}`.\");\n }\n\n const body = this.src.slice(this.pos + 2, closing);\n\n if (!PARAMETER_EXPANSION.test(body)) {\n throw unsupported(\n `\\`\\${${body}}\\` is not a supported parameter expansion; use \\`$name\\`, \\`\\${name}\\` or \\`\\${name:-default}\\`.`,\n );\n }\n\n this.pos = closing + 1;\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n if (next === \"[\") {\n throw unsupported(\"`$[…]` (arithmetic expansion) is not supported.\");\n }\n\n if (context === \"unquoted\" && (next === \"'\" || next === '\"')) {\n throw unsupported(`\\`$${next}…${next}\\` quoting is not portable; use plain quotes.`);\n }\n\n if (/[A-Za-z_]/.test(next)) {\n this.pos += 2;\n\n while (/[A-Za-z0-9_]/.test(this.peek())) {\n this.pos++;\n }\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n if (/[0-9@*#?$!-]/.test(next) && next !== \"\") {\n this.pos += 2;\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n // A `$` followed by anything else is an ordinary character.\n this.pos++;\n\n return { text: \"$\", dynamic: false };\n }\n\n /** Parse a backtick substitution (cursor on the opening backtick); returns its source. */\n private parseBackticks(insideDoubleQuotes: boolean): string {\n const start = this.pos;\n let content = \"\";\n\n this.pos++;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n if (this.atEnd()) {\n throw unbalanced(\"A backtick substitution is never closed.\");\n }\n\n const char = this.peek();\n\n if (char === \"`\") {\n this.pos++;\n\n break;\n }\n\n if (char === \"\\\\\") {\n const next = this.peek(1);\n\n if (next === \"\") {\n throw unbalanced(\"A backtick substitution is never closed.\");\n }\n\n if (next === \"`\" || next === \"\\\\\" || next === \"$\") {\n content += next;\n this.pos += 2;\n\n continue;\n }\n\n if (next === '\"' && insideDoubleQuotes) {\n throw unsupported(\n 'An escaped `\\\\\"` inside backticks inside double quotes is not portable; use `$(…)`.',\n );\n }\n\n content += char + next;\n this.pos += 2;\n\n continue;\n }\n\n content += char;\n this.pos++;\n }\n\n this.enterNesting();\n new PosixParser(content, this.state).parseNestedProgram();\n this.exitNesting();\n\n return this.src.slice(start, this.pos);\n }\n}\n\n/** Parser for the restricted `cmd.exe` dialect. */\nclass CmdParser {\n private pos = 0;\n\n public constructor(\n private readonly src: string,\n private readonly commands: ShellSimpleCommand[],\n ) {}\n\n public parseProgram(segments: ShellSegment[]): void {\n if (/[\\r\\n\\0]/.test(this.src)) {\n throw unsupported(\"Line breaks and NUL bytes are not supported in a cmd.exe command line.\");\n }\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.atEnd()) {\n throw unsupported(\n segments.length === 0\n ? \"The command is empty.\"\n : \"The line ends with an operator but no command follows it.\",\n );\n }\n\n const start = this.pos;\n\n this.parsePipeline();\n\n const end = this.pos;\n\n this.skipBlanks();\n\n const operator = this.readListOperator();\n\n segments.push({\n source: this.src.slice(start, end).trim(),\n ...(operator !== null ? { operator } : {}),\n });\n\n if (operator === null) {\n if (!this.atEnd()) {\n throw unsupported(`Unexpected \\`${this.peek()}\\`.`);\n }\n\n return;\n }\n }\n }\n\n private atEnd(): boolean {\n return this.pos >= this.src.length;\n }\n\n private peek(offset = 0): string {\n return this.src.charAt(this.pos + offset);\n }\n\n private skipBlanks(): void {\n while (this.peek() === \" \" || this.peek() === \"\\t\") {\n this.pos++;\n }\n }\n\n private readListOperator(): string | null {\n if (this.peek() === \"&\") {\n if (this.peek(1) === \"&\") {\n this.pos += 2;\n\n return \"&&\";\n }\n\n this.pos++;\n\n return \"&\";\n }\n\n if (this.peek() === \"|\" && this.peek(1) === \"|\") {\n this.pos += 2;\n\n return \"||\";\n }\n\n return null;\n }\n\n private parsePipeline(): void {\n this.parseSimpleCommand();\n\n while (this.peek() === \"|\" && this.peek(1) !== \"|\") {\n this.pos++;\n this.skipBlanks();\n\n if (this.atEnd()) {\n throw unsupported(\"The line ends with `|` but no command follows it.\");\n }\n\n this.parseSimpleCommand();\n }\n }\n\n private atRedirectStart(): boolean {\n const char = this.peek();\n\n if (char === \"<\" || char === \">\") {\n return true;\n }\n\n return /[0-9]/.test(char) && (this.peek(1) === \">\" || this.peek(1) === \"<\");\n }\n\n private parseSimpleCommand(): void {\n const start = this.pos;\n const command: ShellSimpleCommand = { assignments: [], words: [], redirects: [], source: \"\" };\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.atEnd() || this.peek() === \"&\" || this.peek() === \"|\") {\n break;\n }\n\n if (this.atRedirectStart()) {\n command.redirects.push(this.parseRedirect());\n\n continue;\n }\n\n const word = this.parseWord();\n\n command.words.push(word);\n\n if (command.words.length === 1) {\n this.checkCommandWord(word);\n }\n }\n\n if (command.words.length === 0 && command.redirects.length === 0) {\n throw unsupported(\"The line has an empty command (two operators in a row?).\");\n }\n\n command.source = this.src.slice(start, this.pos).trim();\n this.commands.push(command);\n }\n\n private checkCommandWord(word: ShellWord): void {\n if (word.raw.startsWith(\"@\") || word.raw.startsWith(\":\")) {\n throw unsupported(`\\`${word.raw}\\` is not supported as a cmd.exe command name.`);\n }\n\n if (/[/=]/.test(word.value)) {\n throw unsupported(\n `A cmd.exe command name may not contain \\`/\\` or \\`=\\` (\\`${word.value}\\`); use \\`\\\\\\` in paths.`,\n );\n }\n\n if (!word.quoted && CMD_RESERVED_WORDS.has(word.value.toLowerCase())) {\n throw unsupported(\n `\\`${word.value}\\` starts a cmd.exe compound command, which is not supported.`,\n );\n }\n }\n\n private parseRedirect(): ShellRedirect {\n let fd: number | undefined;\n\n if (/[0-9]/.test(this.peek())) {\n fd = Number(this.peek());\n this.pos++;\n }\n\n let op: string;\n\n if (this.src.startsWith(\">>\", this.pos)) {\n op = \">>\";\n } else if (this.src.startsWith(\">&\", this.pos)) {\n op = \">&\";\n } else if (this.src.startsWith(\"<&\", this.pos) || this.src.startsWith(\"<<\", this.pos)) {\n throw unsupported(\n `\\`${this.src.slice(this.pos, this.pos + 2)}\\` is not supported by cmd.exe.`,\n );\n } else {\n op = this.peek();\n }\n\n this.pos += op.length;\n this.skipBlanks();\n\n if (this.atEnd() || /[&|<>]/.test(this.peek())) {\n throw unsupported(`The redirection \\`${op}\\` has no target.`);\n }\n\n const target = this.parseWord();\n\n if (op === \">&\") {\n if (!/^[0-9]$/.test(target.value)) {\n throw unsupported(`\\`>&${target.raw}\\` is not supported; write \\`> file 2>&1\\`.`);\n }\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: \"dup\" };\n }\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: op === \"<\" ? \"read\" : \"write\" };\n }\n\n private parseWord(): ShellWord {\n const start = this.pos;\n let value = \"\";\n let dynamic = false;\n let quoted = false;\n\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (\n char === \" \" ||\n char === \"\\t\" ||\n char === \"&\" ||\n char === \"|\" ||\n char === \"<\" ||\n char === \">\"\n ) {\n break;\n }\n\n if (char === '\"') {\n const closing = this.src.indexOf('\"', this.pos + 1);\n\n if (closing === -1) {\n throw unbalanced('A `\"` quote is never closed.');\n }\n\n const inner = this.src.slice(this.pos + 1, closing);\n\n if (/[%!]/.test(inner)) {\n throw unsupported(\n \"`%` and `!` expand even inside quotes in cmd.exe; they are not supported.\",\n );\n }\n\n if (value.endsWith(\"\\\\\") || inner.endsWith(\"\\\\\")) {\n throw unsupported(\n 'A backslash before a `\"` is read differently by cmd.exe and programs.',\n );\n }\n\n value += inner;\n quoted = true;\n this.pos = closing + 1;\n\n continue;\n }\n\n if (\n char === \"%\" ||\n char === \"!\" ||\n char === \"^\" ||\n char === \"(\" ||\n char === \")\" ||\n char === \";\" ||\n char === \",\"\n ) {\n throw unsupported(\n `Unquoted \\`${char}\\` has a special meaning in cmd.exe and is not supported.`,\n );\n }\n\n if (char === \"\\\\\" && this.peek(1) === '\"') {\n throw unsupported('A backslash before a `\"` is read differently by cmd.exe and programs.');\n }\n\n if (char === \"*\" || char === \"?\") {\n dynamic = true;\n }\n\n value += char;\n this.pos++;\n }\n\n return { raw: this.src.slice(start, this.pos), value, dynamic, quoted };\n }\n}\n\n/**\n * Parse a command line for the shell policy. Never throws: a construct the\n * parser does not read with confidence comes back as `{ ok: false }` with a\n * message naming it.\n *\n * @example\n * const parsed = parseShellLine(\"git add -A && git commit -m 'wip'\", \"posix\");\n * // parsed.ok === true; parsed.commands.map((c) => c.words.map((w) => w.value))\n * // → [[\"git\", \"add\", \"-A\"], [\"git\", \"commit\", \"-m\", \"wip\"]]\n */\nexport function parseShellLine(line: string, dialect: ShellDialect): ShellParseResult {\n const segments: ShellSegment[] = [];\n\n try {\n if (dialect === \"cmd\") {\n const commands: ShellSimpleCommand[] = [];\n\n new CmdParser(line, commands).parseProgram(segments);\n\n return { ok: true, commands, segments, background: false };\n }\n\n const state: SharedState = { commands: [], background: false, nesting: 0 };\n\n new PosixParser(line, state).parseProgram(segments);\n\n return { ok: true, commands: state.commands, segments, background: state.background };\n } catch (error) {\n if (error instanceof ShellSyntaxRefusal) {\n return { ok: false, reason: error.reason, message: error.message };\n }\n\n throw error;\n }\n}\n"],"mappings":";;AAyGA,MAAM,cAAc;;;;;;AAOpB,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAM,qBAAqB,IAAI,IAAI;CAAC;CAAM;CAAO;CAAQ;CAAY;AAAU,CAAC;;AAGhF,MAAM,iBAAiB,OAAO,GAAG;;;;;;;;AAejC,MAAM,sBAAsB,IAAI,OAC9B,QAAQ,eAAe,GAAG,eAAe,kDAC3C;;AAGA,IAAM,qBAAN,cAAiC,MAAM;CACrC,AAAO,YACL,AAAgB,QAChB,SACA;EACA,MAAM,OAAO;EAHG;CAIlB;AACF;;AAGA,SAAS,YAAY,SAAqC;CACxD,OAAO,IAAI,mBAAmB,sBAAsB,OAAO;AAC7D;;AAGA,SAAS,WAAW,SAAqC;CACvD,OAAO,IAAI,mBAAmB,qBAAqB,OAAO;AAC5D;;AAiBA,IAAM,cAAN,MAAM,YAAY;CAKhB,AAAO,YACL,AAAiB,KACjB,AAAiB,OACjB;EAFiB;EACA;aANL;yBAE8B,CAAC;CAK1C;;CAGH,AAAO,aAAa,UAAgC;EAClD,KAAK,UAAU,MAAM,QAAQ;EAC7B,KAAK,wBAAwB;CAC/B;;CAGA,AAAO,qBAA2B;EAChC,KAAK,UAAU,MAAM,MAAS;EAC9B,KAAK,wBAAwB;CAC/B;;;;;;CAOA,AAAO,qBAA2B;EAChC,OAAO,CAAC,KAAK,MAAM,GAAG;GACpB,MAAM,OAAO,KAAK,KAAK;GAEvB,IAAI,SAAS,MAAM;IACjB,KAAK,OAAO;IAEZ;GACF;GAEA,IAAI,SAAS,KAAK;IAChB,KAAK,YAAY,SAAS;IAE1B;GACF;GAEA,IAAI,SAAS,KAAK;IAChB,KAAK,eAAe,KAAK;IAEzB;GACF;GAEA,KAAK;EACP;CACF;CAEA,AAAQ,QAAiB;EACvB,OAAO,KAAK,OAAO,KAAK,IAAI;CAC9B;CAEA,AAAQ,KAAK,SAAS,GAAW;EAC/B,OAAO,KAAK,IAAI,OAAO,KAAK,MAAM,MAAM;CAC1C;CAEA,AAAQ,0BAAgC;EACtC,IAAI,KAAK,gBAAgB,SAAS,GAChC,MAAM,YACJ,oCAAoC,KAAK,gBAAgB,EAAE,EAAE,aAAa,GAAG,wBAC/E;CAEJ;CAEA,AAAQ,eAAqB;EAC3B,KAAK,MAAM;EAEX,IAAI,KAAK,MAAM,UAAU,aACvB,MAAM,YAAY,uDAAuD;CAE7E;CAEA,AAAQ,cAAoB;EAC1B,KAAK,MAAM;CACb;;CAGA,AAAQ,aAAmB;EACzB,OAAO,CAAC,KAAK,MAAM,GAAG;GACpB,MAAM,OAAO,KAAK,KAAK;GAEvB,IAAI,SAAS,OAAO,SAAS,KAAM;IACjC,KAAK;IAEL;GACF;GAEA,IAAI,SAAS,QAAQ,KAAK,KAAK,CAAC,MAAM,MAAM;IAC1C,KAAK,OAAO;IAEZ;GACF;GAEA;EACF;CACF;;CAGA,AAAQ,cAAoB;EAC1B,MAAM,UAAU,KAAK,IAAI,QAAQ,MAAM,KAAK,GAAG;EAE/C,KAAK,MAAM,YAAY,KAAK,KAAK,IAAI,SAAS;CAChD;;CAGA,AAAQ,iBAAuB;EAC7B,KAAK;EAEL,IAAI,KAAK,gBAAgB,WAAW,GAClC;EAGF,IAAI,KAAK,MAAM,UAAU,GACvB,MAAM,YAAY,qEAAqE;EAGzF,MAAM,UAAU,KAAK;EAErB,KAAK,kBAAkB,CAAC;EAExB,KAAK,MAAM,WAAW,SACpB,KAAK,gBAAgB,OAAO;CAEhC;;CAGA,AAAQ,wBAA8B;EACpC,OAAO,CAAC,KAAK,MAAM,GAAG;GACpB,KAAK,WAAW;GAEhB,IAAI,KAAK,KAAK,MAAM,KAAK;IACvB,KAAK,YAAY;IAEjB;GACF;GAEA,IAAI,KAAK,KAAK,MAAM,MAAM;IACxB,KAAK,eAAe;IAEpB;GACF;GAEA;EACF;CACF;CAEA,AAAQ,gBAAgB,SAA+B;EACrD,MAAM,YAAsB,CAAC;EAG7B,OAAO,MAAM;GACX,IAAI,KAAK,MAAM,GACb,MAAM,YACJ,oCAAoC,QAAQ,UAAU,wBACxD;GAGF,MAAM,UAAU,KAAK,IAAI,QAAQ,MAAM,KAAK,GAAG;GAC/C,MAAM,OAAO,YAAY,KAAK,KAAK,IAAI,MAAM,KAAK,GAAG,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,OAAO;GACzF,MAAM,WAAW,QAAQ,YAAY,KAAK,QAAQ,QAAQ,EAAE,IAAI;GAEhE,KAAK,MAAM,YAAY,KAAK,KAAK,IAAI,SAAS,UAAU;GAExD,IAAI,aAAa,QAAQ,WACvB;GAGF,UAAU,KAAK,QAAQ;EACzB;EAEA,IAAI,QAAQ,QACV,IAAI,YAAY,UAAU,KAAK,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,mBAAmB;CAEzE;;;;;CAMA,AAAQ,UAAU,SAAwB,UAA4C;EACpF,IAAI,aAAa;EAGjB,OAAO,MAAM;GACX,KAAK,sBAAsB;GAE3B,IAAI,KAAK,MAAM,GAAG;IAChB,IAAI,YAAY,MACd,MAAM,WAAW,4CAA4C,QAAQ,IAAI;IAG3E;GACF;GAEA,IAAI,YAAY,QAAQ,KAAK,KAAK,MAAM,SACtC;GAGF,MAAM,QAAQ,KAAK;GAEnB,KAAK,cAAc,OAAO;GAE1B,MAAM,MAAM,KAAK;GAEjB,aAAa;GACb,KAAK,WAAW;GAEhB,IAAI,KAAK,KAAK,MAAM,KAClB,KAAK,YAAY;GAGnB,MAAM,WAAW,KAAK,iBAAiB;GAEvC,UAAU,KAAK;IACb,QAAQ,KAAK,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;IACxC,GAAI,aAAa,OAAO,EAAE,SAAS,IAAI,CAAC;GAC1C,CAAC;GAED,IAAI,aAAa,QAAQ,aAAa,MAAM;IAC1C,KAAK,sBAAsB;IAE3B,IAAI,KAAK,MAAM,KAAM,YAAY,QAAQ,KAAK,KAAK,MAAM,SACvD,MAAM,YAAY,wBAAwB,SAAS,8BAA8B;IAGnF;GACF;GAEA,IAAI,aAAa,KACf,KAAK,MAAM,aAAa;GAG1B,IAAI,aAAa,MAAM;IACrB,IAAI,KAAK,MAAM,KAAM,YAAY,QAAQ,KAAK,KAAK,MAAM,SACvD;IAGF,MAAM,YAAY,gBAAgB,KAAK,KAAK,EAAE,IAAI;GACpD;EACF;EAEA,IAAI,CAAC,YACH,MAAM,YAAY,YAAY,OAAO,0BAA0B,6BAA6B;CAEhG;;CAGA,AAAQ,mBAAkC;EACxC,MAAM,OAAO,KAAK,KAAK;EACvB,MAAM,OAAO,KAAK,KAAK,CAAC;EAExB,IAAI,SAAS,KAAK;GAChB,IAAI,SAAS,KAAK;IAChB,KAAK,OAAO;IAEZ,OAAO;GACT;GAEA,IAAI,SAAS,KACX,MAAM,YACJ,4EACF;GAGF,KAAK;GAEL,OAAO;EACT;EAEA,IAAI,SAAS,OAAO,SAAS,KAAK;GAChC,KAAK,OAAO;GAEZ,OAAO;EACT;EAEA,IAAI,SAAS,KAAK;GAChB,IAAI,SAAS,KACX,MAAM,YAAY,0CAA0C;GAG9D,KAAK;GAEL,OAAO;EACT;EAEA,IAAI,SAAS,MAAM;GACjB,KAAK,eAAe;GAEpB,OAAO;EACT;EAEA,OAAO;CACT;CAEA,AAAQ,cAAc,SAA8B;EAClD,KAAK,aAAa,OAAO;EAGzB,OAAO,MAAM;GACX,KAAK,WAAW;GAEhB,IAAI,KAAK,KAAK,MAAM,OAAO,KAAK,KAAK,CAAC,MAAM,KAC1C;GAGF,IAAI,KAAK,KAAK,CAAC,MAAM,KACnB,MAAM,YAAY,+CAA+C;GAGnE,KAAK;GACL,KAAK,sBAAsB;GAE3B,IAAI,KAAK,MAAM,KAAM,YAAY,QAAQ,KAAK,KAAK,MAAM,SACvD,MAAM,YAAY,mDAAmD;GAGvE,KAAK,aAAa,OAAO;EAC3B;CACF;CAEA,AAAQ,aAAa,SAA8B;EACjD,KAAK,WAAW;EAEhB,IAAI,KAAK,KAAK,MAAM,KAAK;GACvB,IAAI,KAAK,KAAK,CAAC,MAAM,KACnB,MAAM,YAAY,wCAAwC;GAG5D,KAAK,cAAc;GAEnB;EACF;EAEA,IAAI,KAAK,KAAK,MAAM,KAClB,MAAM,YAAY,YAAY,OAAO,oBAAoB,6BAA6B;EAGxF,KAAK,mBAAmB;CAC1B;CAEA,AAAQ,gBAAsB;EAC5B,MAAM,QAAQ,KAAK;EAEnB,KAAK;EACL,KAAK,aAAa;EAClB,KAAK,UAAU,KAAK,MAAS;EAE7B,IAAI,KAAK,KAAK,MAAM,KAClB,MAAM,WAAW,iCAAiC;EAGpD,KAAK;EACL,KAAK,YAAY;EAGjB,MAAM,YAA6B,CAAC;EAGpC,OAAO,MAAM;GACX,KAAK,WAAW;GAEhB,IAAI,CAAC,KAAK,gBAAgB,GACxB;GAGF,UAAU,KAAK,KAAK,cAAc,CAAC;EACrC;EAEA,IAAI,UAAU,SAAS,GACrB,KAAK,MAAM,SAAS,KAAK;GACvB,aAAa,CAAC;GACd,OAAO,CAAC;GACR;GACA,QAAQ,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK;EAC/C,CAAC;EAGH,IAAI,CAAC,KAAK,MAAM,KAAK,CAAC,KAAK,mBAAmB,GAC5C,MAAM,YAAY,mDAAmD;CAEzE;;CAGA,AAAQ,qBAA8B;EACpC,MAAM,OAAO,KAAK,KAAK;EAEvB,OACE,SAAS,QACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS;CAEb;;CAGA,AAAQ,kBAA2B;EACjC,IAAI,QAAQ,KAAK;EAEjB,OAAO,QAAQ,KAAK,IAAI,UAAU,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,CAAC,GACnE;EAGF,MAAM,OAAO,KAAK,IAAI,OAAO,KAAK;EAElC,OAAO,SAAS,OAAO,SAAS;CAClC;CAEA,AAAQ,qBAA2B;EACjC,MAAM,QAAQ,KAAK;EACnB,MAAM,UAA8B;GAAE,aAAa,CAAC;GAAG,OAAO,CAAC;GAAG,WAAW,CAAC;GAAG,QAAQ;EAAG;EAG5F,OAAO,MAAM;GACX,KAAK,WAAW;GAEhB,IAAI,KAAK,MAAM,GACb;GAGF,MAAM,OAAO,KAAK,KAAK;GAEvB,IAAI,SAAS,QAAQ,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO,SAAS,KAC5E;GAGF,IAAI,SAAS,KACX,MAAM,YACJ,4FACF;GAGF,IAAI,SAAS,KAAK;IAChB,KAAK,YAAY;IAEjB;GACF;GAEA,IAAI,KAAK,gBAAgB,GAAG;IAC1B,QAAQ,UAAU,KAAK,KAAK,cAAc,CAAC;IAE3C;GACF;GAEA,MAAM,OAAO,KAAK,UAAU;GAE5B,IAAI,QAAQ,MAAM,WAAW,KAAK,KAAK,mBAAmB,QAAW;IACnE,QAAQ,YAAY,KAAK,IAAI;IAE7B;GACF;GAEA,QAAQ,MAAM,KAAK,IAAI;GAEvB,IAAI,QAAQ,MAAM,WAAW,KAAK,CAAC,KAAK,UAAU,eAAe,IAAI,KAAK,KAAK,GAC7E,MAAM,YACJ,KAAK,KAAK,MAAM,4FAClB;EAEJ;EAEA,IACE,QAAQ,MAAM,WAAW,KACzB,QAAQ,YAAY,WAAW,KAC/B,QAAQ,UAAU,WAAW,GAE7B,MAAM,YAAY,0DAA0D;EAG9E,QAAQ,SAAS,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK;EACtD,KAAK,MAAM,SAAS,KAAK,OAAO;CAClC;CAEA,AAAQ,gBAA+B;EACrC,IAAI,SAAS;EAEb,OAAO,QAAQ,KAAK,KAAK,KAAK,CAAC,GAAG;GAChC,UAAU,KAAK,KAAK;GACpB,KAAK;EACP;EAEA,MAAM,OAAO,KAAK,IAAI,MAAM,KAAK,KAAK,KAAK,MAAM,CAAC;EAClD,IAAI;EAEJ,IAAI,KAAK,WAAW,KAAK,GACvB,MAAM,YAAY,6DAA6D;OAC1E,IAAI,KAAK,WAAW,KAAK,GAC9B,KAAK;OACA,IAAI,KAAK,WAAW,IAAI,GAC7B,KAAK;OACA,IAAI,KAAK,WAAW,IAAI,GAC7B,KAAK;OACA,IAAI,KAAK,WAAW,IAAI,GAC7B,KAAK;OACA,IAAI,KAAK,WAAW,IAAI,KAAK,KAAK,WAAW,IAAI,GACtD,MAAM,YAAY,yDAAyD;OACtE,IAAI,KAAK,WAAW,IAAI,GAC7B,KAAK;OACA,IAAI,KAAK,WAAW,IAAI,GAC7B,KAAK;OACA,IAAI,KAAK,WAAW,IAAI,GAC7B,KAAK;OAEL,KAAK,KAAK,KAAK;EAGjB,KAAK,OAAO,GAAG;EAEf,MAAM,KAAK,WAAW,KAAK,SAAY,OAAO,MAAM;EAEpD,KAAK,WAAW;EAEhB,IAAI,KAAK,MAAM,KAAK,cAAc,KAAK,KAAK,KAAK,CAAC,GAChD,MAAM,YAAY,qBAAqB,GAAG,kBAAkB;EAG9D,MAAM,SAAS,KAAK,UAAU;EAE9B,IAAI,OAAO,QAAQ,OAAO,OAAO;GAC/B,IAAI,KAAK,MAAM,UAAU,GACvB,MAAM,YAAY,qEAAqE;GAGzF,IAAI,OAAO,SACT,MAAM,YAAY,iDAAiD;GAGrE,KAAK,gBAAgB,KAAK;IACxB,WAAW,OAAO;IAClB,WAAW,OAAO;IAClB,QAAQ,CAAC,OAAO;GAClB,CAAC;GAED,OAAO;IAAE;IAAI,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;IAAI;IAAQ,MAAM;GAAU;EAC5E;EAEA,IAAI,OAAO,QAAQ,OAAO,MAAM;GAC9B,IAAI,OAAO,WAAW,CAAC,iBAAiB,KAAK,OAAO,KAAK,GACvD,MAAM,YACJ,KAAK,KAAK,OAAO,IAAI,0EACvB;GAGF,OAAO;IAAE;IAAI,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;IAAI;IAAQ,MAAM;GAAM;EACxE;EAEA,OAAO;GACL;GACA,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;GACjC;GACA,MAAM,OAAO,MAAM,SAAS;EAC9B;CACF;;CAGA,AAAQ,YAAuB;EAC7B,MAAM,QAAQ,KAAK;EACnB,IAAI,QAAQ;EACZ,IAAI,UAAU;EACd,IAAI,SAAS;EACb,IAAI,SAAS;EACb,IAAI,gBAAgB;EACpB,IAAI;EAEJ,OAAO,CAAC,KAAK,MAAM,GAAG;GACpB,MAAM,OAAO,KAAK,KAAK;GAEvB,IACE,SAAS,OACT,SAAS,OACT,SAAS,QACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,KAET;GAGF,IAAI,SAAS,QAAQ,SAAS,MAC5B,MAAM,YAAY,qEAAqE;GAGzF,IAAI,SAAS,MAAM;IACjB,MAAM,OAAO,KAAK,KAAK,CAAC;IAExB,IAAI,SAAS,IACX,MAAM,YAAY,iCAAiC;IAGrD,KAAK,OAAO;IAEZ,IAAI,SAAS,MAAM;KACjB,SAAS;KACT,SAAS;KACT,gBAAgB;IAClB;IAEA;GACF;GAEA,IAAI,SAAS,KAAK;IAChB,MAAM,UAAU,KAAK,IAAI,QAAQ,KAAK,KAAK,MAAM,CAAC;IAElD,IAAI,YAAY,IACd,MAAM,WAAW,8BAA8B;IAGjD,SAAS,KAAK,IAAI,MAAM,KAAK,MAAM,GAAG,OAAO;IAC7C,SAAS;IACT,gBAAgB;IAChB,KAAK,MAAM,UAAU;IAErB;GACF;GAEA,IAAI,SAAS,MAAK;IAChB,MAAM,OAAO,KAAK,kBAAkB;IAEpC,SAAS,KAAK;IACd,UAAU,WAAW,KAAK;IAC1B,SAAS;IACT,gBAAgB;IAEhB;GACF;GAEA,IAAI,SAAS,KAAK;IAChB,MAAM,OAAO,KAAK,YAAY,UAAU;IAExC,SAAS,KAAK;IACd,UAAU,WAAW,KAAK;IAC1B,gBAAgB;IAEhB;GACF;GAEA,IAAI,SAAS,KAAK;IAChB,SAAS,KAAK,eAAe,KAAK;IAClC,UAAU;IACV,gBAAgB;IAEhB;GACF;GAEA,IAAI,SAAS,OAAO,iBAAiB,mBAAmB,QAAW;IACjE,IAAI,2BAA2B,KAAK,KAAK,GACvC,iBAAiB;IAGnB,gBAAgB;GAClB,OAAO,IAAI,CAAC,eAAe,KAAK,IAAI,GAClC,gBAAgB;GAGlB,IAAI,SAAS,OAAO,SAAS,OAAO,SAAS,KAC3C,UAAU;GAGZ,IAAI,SAAS,KAAK;IAChB,MAAM,WAAW,KAAK,IAAI,OAAO,KAAK,MAAM,CAAC;IAE7C,IACE,KAAK,QAAQ,SACZ,mBAAmB,WAAc,aAAa,OAAO,aAAa,MAEnE,UAAU;GAEd;GAEA,IAAI,SAAS,OAAO,SAAS,KAC3B,SAAS;GAGX,SAAS;GACT,KAAK;EACP;EAEA,MAAM,MAAM,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG;EAE1C,IAAI,UAAU,QAAQ,MACpB,MAAM,YACJ,gCAAgC,IAAI,oEACtC;EAIF,IAAI,QAAQ,OAAO,QAAQ,KACzB,UAAU;EAGZ,OAAO;GACL;GACA;GACA;GACA;GACA,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;EAC3D;CACF;;CAGA,AAAQ,oBAAyD;EAC/D,KAAK;EAEL,IAAI,QAAQ;EACZ,IAAI,UAAU;EAGd,OAAO,MAAM;GACX,IAAI,KAAK,MAAM,GACb,MAAM,WAAW,+BAA8B;GAGjD,MAAM,OAAO,KAAK,KAAK;GAEvB,IAAI,SAAS,MAAK;IAChB,KAAK;IAEL,OAAO;KAAE;KAAO;IAAQ;GAC1B;GAEA,IAAI,SAAS,MACX,MAAM,YAAY,gDAAgD;GAGpE,IAAI,SAAS,MAAM;IACjB,MAAM,OAAO,KAAK,KAAK,CAAC;IAExB,IAAI,SAAS,IACX,MAAM,WAAW,+BAA8B;IAGjD,IAAI,SAAS,OAAO,SAAS,OAAO,SAAS,QAAO,SAAS,MAAM;KACjE,SAAS;KACT,KAAK,OAAO;KAEZ;IACF;IAEA,IAAI,SAAS,MAAM;KACjB,KAAK,OAAO;KAEZ;IACF;IAEA,SAAS;IACT,KAAK;IAEL;GACF;GAEA,IAAI,SAAS,KAAK;IAChB,MAAM,OAAO,KAAK,YAAY,QAAQ;IAEtC,SAAS,KAAK;IACd,UAAU,WAAW,KAAK;IAE1B;GACF;GAEA,IAAI,SAAS,KAAK;IAChB,SAAS,KAAK,eAAe,IAAI;IACjC,UAAU;IAEV;GACF;GAEA,SAAS;GACT,KAAK;EACP;CACF;;CAGA,AAAQ,YAAY,SAGlB;EACA,MAAM,QAAQ,KAAK;EACnB,MAAM,OAAO,KAAK,KAAK,CAAC;EAExB,IAAI,SAAS,KAAK;GAChB,IAAI,KAAK,KAAK,CAAC,MAAM,KACnB,MAAM,YAAY,mDAAmD;GAGvE,KAAK,OAAO;GACZ,KAAK,aAAa;GAClB,KAAK,UAAU,KAAK,MAAS;GAE7B,IAAI,KAAK,KAAK,MAAM,KAClB,MAAM,WAAW,kCAAkC;GAGrD,KAAK;GACL,KAAK,YAAY;GAEjB,OAAO;IAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG;IAAG,SAAS;GAAK;EAChE;EAEA,IAAI,SAAS,KAAK;GAChB,MAAM,UAAU,KAAK,IAAI,QAAQ,KAAK,KAAK,MAAM,CAAC;GAElD,IAAI,YAAY,IACd,MAAM,WAAW,kCAAkC;GAGrD,MAAM,OAAO,KAAK,IAAI,MAAM,KAAK,MAAM,GAAG,OAAO;GAEjD,IAAI,CAAC,oBAAoB,KAAK,IAAI,GAChC,MAAM,YACJ,QAAQ,KAAK,kGACf;GAGF,KAAK,MAAM,UAAU;GAErB,OAAO;IAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG;IAAG,SAAS;GAAK;EAChE;EAEA,IAAI,SAAS,KACX,MAAM,YAAY,iDAAiD;EAGrE,IAAI,YAAY,eAAe,SAAS,OAAO,SAAS,OACtD,MAAM,YAAY,MAAM,KAAK,GAAG,KAAK,8CAA8C;EAGrF,IAAI,YAAY,KAAK,IAAI,GAAG;GAC1B,KAAK,OAAO;GAEZ,OAAO,eAAe,KAAK,KAAK,KAAK,CAAC,GACpC,KAAK;GAGP,OAAO;IAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG;IAAG,SAAS;GAAK;EAChE;EAEA,IAAI,eAAe,KAAK,IAAI,KAAK,SAAS,IAAI;GAC5C,KAAK,OAAO;GAEZ,OAAO;IAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG;IAAG,SAAS;GAAK;EAChE;EAGA,KAAK;EAEL,OAAO;GAAE,MAAM;GAAK,SAAS;EAAM;CACrC;;CAGA,AAAQ,eAAe,oBAAqC;EAC1D,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU;EAEd,KAAK;EAGL,OAAO,MAAM;GACX,IAAI,KAAK,MAAM,GACb,MAAM,WAAW,0CAA0C;GAG7D,MAAM,OAAO,KAAK,KAAK;GAEvB,IAAI,SAAS,KAAK;IAChB,KAAK;IAEL;GACF;GAEA,IAAI,SAAS,MAAM;IACjB,MAAM,OAAO,KAAK,KAAK,CAAC;IAExB,IAAI,SAAS,IACX,MAAM,WAAW,0CAA0C;IAG7D,IAAI,SAAS,OAAO,SAAS,QAAQ,SAAS,KAAK;KACjD,WAAW;KACX,KAAK,OAAO;KAEZ;IACF;IAEA,IAAI,SAAS,QAAO,oBAClB,MAAM,YACJ,sFACF;IAGF,WAAW,OAAO;IAClB,KAAK,OAAO;IAEZ;GACF;GAEA,WAAW;GACX,KAAK;EACP;EAEA,KAAK,aAAa;EAClB,IAAI,YAAY,SAAS,KAAK,KAAK,CAAC,CAAC,mBAAmB;EACxD,KAAK,YAAY;EAEjB,OAAO,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG;CACvC;AACF;;AAGA,IAAM,YAAN,MAAgB;CAGd,AAAO,YACL,AAAiB,KACjB,AAAiB,UACjB;EAFiB;EACA;aAJL;CAKX;CAEH,AAAO,aAAa,UAAgC;EAClD,IAAI,WAAW,KAAK,KAAK,GAAG,GAC1B,MAAM,YAAY,wEAAwE;EAI5F,OAAO,MAAM;GACX,KAAK,WAAW;GAEhB,IAAI,KAAK,MAAM,GACb,MAAM,YACJ,SAAS,WAAW,IAChB,0BACA,2DACN;GAGF,MAAM,QAAQ,KAAK;GAEnB,KAAK,cAAc;GAEnB,MAAM,MAAM,KAAK;GAEjB,KAAK,WAAW;GAEhB,MAAM,WAAW,KAAK,iBAAiB;GAEvC,SAAS,KAAK;IACZ,QAAQ,KAAK,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK;IACxC,GAAI,aAAa,OAAO,EAAE,SAAS,IAAI,CAAC;GAC1C,CAAC;GAED,IAAI,aAAa,MAAM;IACrB,IAAI,CAAC,KAAK,MAAM,GACd,MAAM,YAAY,gBAAgB,KAAK,KAAK,EAAE,IAAI;IAGpD;GACF;EACF;CACF;CAEA,AAAQ,QAAiB;EACvB,OAAO,KAAK,OAAO,KAAK,IAAI;CAC9B;CAEA,AAAQ,KAAK,SAAS,GAAW;EAC/B,OAAO,KAAK,IAAI,OAAO,KAAK,MAAM,MAAM;CAC1C;CAEA,AAAQ,aAAmB;EACzB,OAAO,KAAK,KAAK,MAAM,OAAO,KAAK,KAAK,MAAM,KAC5C,KAAK;CAET;CAEA,AAAQ,mBAAkC;EACxC,IAAI,KAAK,KAAK,MAAM,KAAK;GACvB,IAAI,KAAK,KAAK,CAAC,MAAM,KAAK;IACxB,KAAK,OAAO;IAEZ,OAAO;GACT;GAEA,KAAK;GAEL,OAAO;EACT;EAEA,IAAI,KAAK,KAAK,MAAM,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK;GAC/C,KAAK,OAAO;GAEZ,OAAO;EACT;EAEA,OAAO;CACT;CAEA,AAAQ,gBAAsB;EAC5B,KAAK,mBAAmB;EAExB,OAAO,KAAK,KAAK,MAAM,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK;GAClD,KAAK;GACL,KAAK,WAAW;GAEhB,IAAI,KAAK,MAAM,GACb,MAAM,YAAY,mDAAmD;GAGvE,KAAK,mBAAmB;EAC1B;CACF;CAEA,AAAQ,kBAA2B;EACjC,MAAM,OAAO,KAAK,KAAK;EAEvB,IAAI,SAAS,OAAO,SAAS,KAC3B,OAAO;EAGT,OAAO,QAAQ,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM,OAAO,KAAK,KAAK,CAAC,MAAM;CACzE;CAEA,AAAQ,qBAA2B;EACjC,MAAM,QAAQ,KAAK;EACnB,MAAM,UAA8B;GAAE,aAAa,CAAC;GAAG,OAAO,CAAC;GAAG,WAAW,CAAC;GAAG,QAAQ;EAAG;EAG5F,OAAO,MAAM;GACX,KAAK,WAAW;GAEhB,IAAI,KAAK,MAAM,KAAK,KAAK,KAAK,MAAM,OAAO,KAAK,KAAK,MAAM,KACzD;GAGF,IAAI,KAAK,gBAAgB,GAAG;IAC1B,QAAQ,UAAU,KAAK,KAAK,cAAc,CAAC;IAE3C;GACF;GAEA,MAAM,OAAO,KAAK,UAAU;GAE5B,QAAQ,MAAM,KAAK,IAAI;GAEvB,IAAI,QAAQ,MAAM,WAAW,GAC3B,KAAK,iBAAiB,IAAI;EAE9B;EAEA,IAAI,QAAQ,MAAM,WAAW,KAAK,QAAQ,UAAU,WAAW,GAC7D,MAAM,YAAY,0DAA0D;EAG9E,QAAQ,SAAS,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK;EACtD,KAAK,SAAS,KAAK,OAAO;CAC5B;CAEA,AAAQ,iBAAiB,MAAuB;EAC9C,IAAI,KAAK,IAAI,WAAW,GAAG,KAAK,KAAK,IAAI,WAAW,GAAG,GACrD,MAAM,YAAY,KAAK,KAAK,IAAI,+CAA+C;EAGjF,IAAI,OAAO,KAAK,KAAK,KAAK,GACxB,MAAM,YACJ,4DAA4D,KAAK,MAAM,0BACzE;EAGF,IAAI,CAAC,KAAK,UAAU,mBAAmB,IAAI,KAAK,MAAM,YAAY,CAAC,GACjE,MAAM,YACJ,KAAK,KAAK,MAAM,8DAClB;CAEJ;CAEA,AAAQ,gBAA+B;EACrC,IAAI;EAEJ,IAAI,QAAQ,KAAK,KAAK,KAAK,CAAC,GAAG;GAC7B,KAAK,OAAO,KAAK,KAAK,CAAC;GACvB,KAAK;EACP;EAEA,IAAI;EAEJ,IAAI,KAAK,IAAI,WAAW,MAAM,KAAK,GAAG,GACpC,KAAK;OACA,IAAI,KAAK,IAAI,WAAW,MAAM,KAAK,GAAG,GAC3C,KAAK;OACA,IAAI,KAAK,IAAI,WAAW,MAAM,KAAK,GAAG,KAAK,KAAK,IAAI,WAAW,MAAM,KAAK,GAAG,GAClF,MAAM,YACJ,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,KAAK,MAAM,CAAC,EAAE,gCAC9C;OAEA,KAAK,KAAK,KAAK;EAGjB,KAAK,OAAO,GAAG;EACf,KAAK,WAAW;EAEhB,IAAI,KAAK,MAAM,KAAK,SAAS,KAAK,KAAK,KAAK,CAAC,GAC3C,MAAM,YAAY,qBAAqB,GAAG,kBAAkB;EAG9D,MAAM,SAAS,KAAK,UAAU;EAE9B,IAAI,OAAO,MAAM;GACf,IAAI,CAAC,UAAU,KAAK,OAAO,KAAK,GAC9B,MAAM,YAAY,OAAO,OAAO,IAAI,4CAA4C;GAGlF,OAAO;IAAE;IAAI,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;IAAI;IAAQ,MAAM;GAAM;EACxE;EAEA,OAAO;GAAE;GAAI,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;GAAI;GAAQ,MAAM,OAAO,MAAM,SAAS;EAAQ;CAChG;CAEA,AAAQ,YAAuB;EAC7B,MAAM,QAAQ,KAAK;EACnB,IAAI,QAAQ;EACZ,IAAI,UAAU;EACd,IAAI,SAAS;EAEb,OAAO,CAAC,KAAK,MAAM,GAAG;GACpB,MAAM,OAAO,KAAK,KAAK;GAEvB,IACE,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,KAET;GAGF,IAAI,SAAS,MAAK;IAChB,MAAM,UAAU,KAAK,IAAI,QAAQ,MAAK,KAAK,MAAM,CAAC;IAElD,IAAI,YAAY,IACd,MAAM,WAAW,+BAA8B;IAGjD,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,MAAM,GAAG,OAAO;IAElD,IAAI,OAAO,KAAK,KAAK,GACnB,MAAM,YACJ,2EACF;IAGF,IAAI,MAAM,SAAS,IAAI,KAAK,MAAM,SAAS,IAAI,GAC7C,MAAM,YACJ,wEACF;IAGF,SAAS;IACT,SAAS;IACT,KAAK,MAAM,UAAU;IAErB;GACF;GAEA,IACE,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,OACT,SAAS,KAET,MAAM,YACJ,cAAc,KAAK,0DACrB;GAGF,IAAI,SAAS,QAAQ,KAAK,KAAK,CAAC,MAAM,MACpC,MAAM,YAAY,wEAAuE;GAG3F,IAAI,SAAS,OAAO,SAAS,KAC3B,UAAU;GAGZ,SAAS;GACT,KAAK;EACP;EAEA,OAAO;GAAE,KAAK,KAAK,IAAI,MAAM,OAAO,KAAK,GAAG;GAAG;GAAO;GAAS;EAAO;CACxE;AACF;;;;;;;;;;;AAYA,SAAgB,eAAe,MAAc,SAAyC;CACpF,MAAM,WAA2B,CAAC;CAElC,IAAI;EACF,IAAI,YAAY,OAAO;GACrB,MAAM,WAAiC,CAAC;GAExC,IAAI,UAAU,MAAM,QAAQ,CAAC,CAAC,aAAa,QAAQ;GAEnD,OAAO;IAAE,IAAI;IAAM;IAAU;IAAU,YAAY;GAAM;EAC3D;EAEA,MAAM,QAAqB;GAAE,UAAU,CAAC;GAAG,YAAY;GAAO,SAAS;EAAE;EAEzE,IAAI,YAAY,MAAM,KAAK,CAAC,CAAC,aAAa,QAAQ;EAElD,OAAO;GAAE,IAAI;GAAM,UAAU,MAAM;GAAU;GAAU,YAAY,MAAM;EAAW;CACtF,SAAS,OAAO;EACd,IAAI,iBAAiB,oBACnB,OAAO;GAAE,IAAI;GAAO,QAAQ,MAAM;GAAQ,SAAS,MAAM;EAAQ;EAGnE,MAAM;CACR;AACF"}
1
+ {"version":3,"file":"shell-parser.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/shell-parser.ts"],"sourcesContent":["/**\n * A deliberately conservative parser for the command lines `run_shell`\n * hands to a shell when the policy opts in to `interpreter: \"sh\"`.\n *\n * Its only job is to find **every simple command** a line would run — so\n * the policy can check each one — plus every redirection target. It is not\n * a shell: it never expands anything. What it cannot read with confidence\n * it refuses, because a construct the parser misreads is a construct a\n * denied command can hide in. `/bin/sh` is bash on some systems, dash or\n * busybox ash on others, so constructs that those shells read differently\n * (bash-only syntax such as `<<<`, `&>`, `$'…'`, brace expansion, `[[`,\n * arithmetic) are refused as well.\n *\n * Two dialects:\n * - `\"posix\"` — what `/bin/sh -c` reads: `;`, `&&`, `||`, `|`, `&`,\n * newlines, comments, `( … )` subshells, `$( … )` and backtick command\n * substitution (parsed recursively), `$name` / `${name…}` parameter\n * expansion (a restricted set of forms), single and double quotes,\n * backslash escapes, redirections and here-documents.\n * - `\"cmd\"` — what `cmd.exe /d /v:off /s /c` reads, restricted to the\n * subset whose meaning is unambiguous: `&&`, `||`, `|`, `&` (sequential\n * in cmd), double quotes and simple redirections. `%`, `!`, `^`,\n * parentheses, `;` and `,` are refused.\n */\n\n/** Which shell grammar to parse. */\nexport type ShellDialect = \"posix\" | \"cmd\";\n\n/** One word of a simple command, after quote removal. */\nexport interface ShellWord {\n /** The word exactly as written. */\n raw: string;\n /**\n * The word's text after quote removal and escapes. Expansions\n * (`$HOME`, `$(…)`, backticks) are kept as their source text — their real\n * value is only known when the shell runs.\n */\n value: string;\n /**\n * True when the shell decides part of the word at run time: a parameter\n * expansion, a command substitution, an unquoted glob character\n * (`*`, `?`, `[`) or a leading `~`.\n */\n dynamic: boolean;\n /** True when any part of the word was quoted or escaped. */\n quoted: boolean;\n /** For a `NAME=value` assignment word, the variable name. */\n assignmentName?: string;\n}\n\n/** A redirection attached to a simple command (or a subshell). */\nexport interface ShellRedirect {\n /** The operator: `>`, `>>`, `>|`, `<`, `<>`, `>&`, `<&`, `<<` or `<<-`. */\n op: string;\n /** Explicit file descriptor (`2>`), when written. */\n fd?: number;\n /** The target word (file, descriptor number or here-doc delimiter). */\n target: ShellWord;\n /**\n * `\"read\"` / `\"write\"` — a file target; `\"dup\"` — a descriptor copy or\n * close (`2>&1`, `>&-`); `\"heredoc\"` — inline input.\n */\n type: \"read\" | \"write\" | \"dup\" | \"heredoc\";\n}\n\n/** A simple command: assignments, words and redirections. */\nexport interface ShellSimpleCommand {\n /** Leading `NAME=value` words. */\n assignments: ShellWord[];\n /** The command's words — `words[0]` is the command name. Empty for a redirect- or assignment-only command. */\n words: ShellWord[];\n /** Redirections, in order. */\n redirects: ShellRedirect[];\n /** The command's source text (for messages). */\n source: string;\n}\n\n/** A top-level piece of the line and the operator that follows it. */\nexport interface ShellSegment {\n /** Source text of the pipeline. */\n source: string;\n /** The list operator after it (`&&`, `||`, `;`, `&`, a newline), if any. */\n operator?: string;\n}\n\n/** Outcome of {@link parseShellLine}. */\nexport type ShellParseResult =\n | {\n ok: true;\n /** Every simple command, in the order the shell meets them (substitutions first). */\n commands: ShellSimpleCommand[];\n /** The top-level pipelines and the operators joining them. */\n segments: ShellSegment[];\n /** True when the line backgrounds anything with `&` (POSIX dialect). */\n background: boolean;\n }\n | {\n ok: false;\n /** Why the line was refused. */\n reason: \"unbalanced-quotes\" | \"unsupported-syntax\";\n /** What was refused and why, for the model. */\n message: string;\n };\n\n/** Deepest nesting of subshells and substitutions the parser accepts. */\nconst MAX_NESTING = 16;\n\n/**\n * Words that start a compound command (or negate a pipeline) when they\n * appear unquoted in command position. The parser does not read compound\n * commands, so a line using one is refused.\n */\nconst RESERVED_WORDS = new Set([\n \"!\",\n \"{\",\n \"}\",\n \"[[\",\n \"]]\",\n \"case\",\n \"coproc\",\n \"do\",\n \"done\",\n \"elif\",\n \"else\",\n \"esac\",\n \"fi\",\n \"for\",\n \"function\",\n \"if\",\n \"in\",\n \"select\",\n \"then\",\n \"until\",\n \"while\",\n]);\n\n/** cmd.exe keywords the `\"cmd\"` dialect refuses in command position. */\nconst CMD_RESERVED_WORDS = new Set([\"if\", \"for\", \"goto\", \"setlocal\", \"endlocal\"]);\n\n/** A parameter name inside `${…}`: a variable, a positional or a special parameter. */\nconst PARAMETER_NAME = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!-])`;\n\n/**\n * The default/pattern word of `${name:-word}` / `${name%word}`: no quotes,\n * no nested expansion or substitution, no braces or parentheses, no `@`.\n */\nconst PARAMETER_WORD = \"[^`$'\\\"\\\\\\\\{}()@\\\\n]*\";\n\n/**\n * Allowed `${…}` forms: `${name}`, `${#name}`, `${name-word}`,\n * `${name:-word}` (also `=`, `+`, `?`), `${name%word}`, `${name%%word}`,\n * `${name#word}`, `${name##word}`. Everything else — substrings and\n * subscripts (arithmetic in bash), `${!name}`, `${name@op}`, pattern\n * substitution — is refused.\n */\nconst PARAMETER_EXPANSION = new RegExp(\n `^(?:#${PARAMETER_NAME}|${PARAMETER_NAME}(?:(?::?[-=+?]|%%?|##?)${PARAMETER_WORD})?)$`,\n);\n\n/** A refusal raised while parsing; caught by {@link parseShellLine}. */\nclass ShellSyntaxRefusal extends Error {\n public constructor(\n public readonly reason: \"unbalanced-quotes\" | \"unsupported-syntax\",\n message: string,\n ) {\n super(message);\n }\n}\n\n/** Refuse a construct the parser does not read with confidence. */\nfunction unsupported(message: string): ShellSyntaxRefusal {\n return new ShellSyntaxRefusal(\"unsupported-syntax\", message);\n}\n\n/** Refuse an unterminated quote / substitution. */\nfunction unbalanced(message: string): ShellSyntaxRefusal {\n return new ShellSyntaxRefusal(\"unbalanced-quotes\", message);\n}\n\n/** State shared by a parser and the nested parsers it starts (backticks, here-docs). */\ninterface SharedState {\n commands: ShellSimpleCommand[];\n background: boolean;\n nesting: number;\n}\n\n/** A here-document whose body starts after the next newline. */\ninterface PendingHeredoc {\n delimiter: string;\n stripTabs: boolean;\n expand: boolean;\n}\n\n/** Parser for the POSIX `sh` dialect. */\nclass PosixParser {\n private pos = 0;\n\n private pendingHeredocs: PendingHeredoc[] = [];\n\n public constructor(\n private readonly src: string,\n private readonly state: SharedState,\n ) {}\n\n /** Parse the whole line as a command list. */\n public parseProgram(segments: ShellSegment[]): void {\n this.parseList(null, segments);\n this.assertNoPendingHeredocs();\n }\n\n /** Parse the contents of a backtick substitution (a full list until EOF). */\n public parseNestedProgram(): void {\n this.parseList(null, undefined);\n this.assertNoPendingHeredocs();\n }\n\n /**\n * Scan an unquoted-delimiter here-document body: `$…`, `${…}`, `$(…)` and\n * backticks expand there exactly as inside double quotes (but `\"` is an\n * ordinary character), so any command substitution in it runs.\n */\n public parseExpandingText(): void {\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (char === \"\\\\\") {\n this.pos += 2;\n\n continue;\n }\n\n if (char === \"$\") {\n this.parseDollar(\"heredoc\");\n\n continue;\n }\n\n if (char === \"`\") {\n this.parseBackticks(false);\n\n continue;\n }\n\n this.pos++;\n }\n }\n\n private atEnd(): boolean {\n return this.pos >= this.src.length;\n }\n\n private peek(offset = 0): string {\n return this.src.charAt(this.pos + offset);\n }\n\n private assertNoPendingHeredocs(): void {\n if (this.pendingHeredocs.length > 0) {\n throw unsupported(\n `The here-document delimited by \\`${this.pendingHeredocs[0]?.delimiter ?? \"\"}\\` is never terminated.`,\n );\n }\n }\n\n private enterNesting(): void {\n this.state.nesting++;\n\n if (this.state.nesting > MAX_NESTING) {\n throw unsupported(\"The line nests subshells or substitutions too deeply.\");\n }\n }\n\n private exitNesting(): void {\n this.state.nesting--;\n }\n\n /** Skip spaces, tabs and backslash-newline continuations. */\n private skipBlanks(): void {\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (char === \" \" || char === \"\\t\") {\n this.pos++;\n\n continue;\n }\n\n if (char === \"\\\\\" && this.peek(1) === \"\\n\") {\n this.pos += 2;\n\n continue;\n }\n\n break;\n }\n }\n\n /** Skip a `#` comment up to (not including) the newline. */\n private skipComment(): void {\n const newline = this.src.indexOf(\"\\n\", this.pos);\n\n this.pos = newline === -1 ? this.src.length : newline;\n }\n\n /** Consume one newline and read any here-document bodies queued before it. */\n private consumeNewline(): void {\n this.pos++;\n\n if (this.pendingHeredocs.length === 0) {\n return;\n }\n\n if (this.state.nesting > 0) {\n throw unsupported(\"Here-documents inside subshells or substitutions are not supported.\");\n }\n\n const pending = this.pendingHeredocs;\n\n this.pendingHeredocs = [];\n\n for (const heredoc of pending) {\n this.readHeredocBody(heredoc);\n }\n }\n\n /** Skip blanks, comments and newlines (separators between commands). */\n private skipBlanksAndNewlines(): void {\n while (!this.atEnd()) {\n this.skipBlanks();\n\n if (this.peek() === \"#\") {\n this.skipComment();\n\n continue;\n }\n\n if (this.peek() === \"\\n\") {\n this.consumeNewline();\n\n continue;\n }\n\n break;\n }\n }\n\n private readHeredocBody(heredoc: PendingHeredoc): void {\n const bodyLines: string[] = [];\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n if (this.atEnd()) {\n throw unsupported(\n `The here-document delimited by \\`${heredoc.delimiter}\\` is never terminated.`,\n );\n }\n\n const lineEnd = this.src.indexOf(\"\\n\", this.pos);\n const line = lineEnd === -1 ? this.src.slice(this.pos) : this.src.slice(this.pos, lineEnd);\n const compared = heredoc.stripTabs ? line.replace(/^\\t+/, \"\") : line;\n\n this.pos = lineEnd === -1 ? this.src.length : lineEnd + 1;\n\n if (compared === heredoc.delimiter) {\n break;\n }\n\n bodyLines.push(compared);\n }\n\n if (heredoc.expand) {\n new PosixParser(bodyLines.join(\"\\n\"), this.state).parseExpandingText();\n }\n }\n\n /**\n * Parse a command list until `closing` (`\")\"`) or the end of the input.\n * `segments`, when given, collects the top-level pipelines.\n */\n private parseList(closing: string | null, segments: ShellSegment[] | undefined): void {\n let sawCommand = false;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanksAndNewlines();\n\n if (this.atEnd()) {\n if (closing !== null) {\n throw unbalanced(`A \\`(\\` or \\`$(\\` is never closed with \\`${closing}\\`.`);\n }\n\n break;\n }\n\n if (closing !== null && this.peek() === closing) {\n break;\n }\n\n const start = this.pos;\n\n this.parsePipeline(closing);\n\n const end = this.pos;\n\n sawCommand = true;\n this.skipBlanks();\n\n if (this.peek() === \"#\") {\n this.skipComment();\n }\n\n const operator = this.readListOperator();\n\n segments?.push({\n source: this.src.slice(start, end).trim(),\n ...(operator !== null ? { operator } : {}),\n });\n\n if (operator === \"&&\" || operator === \"||\") {\n this.skipBlanksAndNewlines();\n\n if (this.atEnd() || (closing !== null && this.peek() === closing)) {\n throw unsupported(`The line ends with \\`${operator}\\` but no command follows it.`);\n }\n\n continue;\n }\n\n if (operator === \"&\") {\n this.state.background = true;\n }\n\n if (operator === null) {\n if (this.atEnd() || (closing !== null && this.peek() === closing)) {\n break;\n }\n\n throw unsupported(`Unexpected \\`${this.peek()}\\`.`);\n }\n }\n\n if (!sawCommand) {\n throw unsupported(closing === null ? \"The command is empty.\" : \"A `( )` or `$( )` is empty.\");\n }\n }\n\n /** Read `&&`, `||`, `;`, `&` or a newline after a pipeline. */\n private readListOperator(): string | null {\n const char = this.peek();\n const next = this.peek(1);\n\n if (char === \"&\") {\n if (next === \"&\") {\n this.pos += 2;\n\n return \"&&\";\n }\n\n if (next === \">\") {\n throw unsupported(\n \"`&>` (redirect both streams) is not portable; write `> file 2>&1` instead.\",\n );\n }\n\n this.pos++;\n\n return \"&\";\n }\n\n if (char === \"|\" && next === \"|\") {\n this.pos += 2;\n\n return \"||\";\n }\n\n if (char === \";\") {\n if (next === \";\") {\n throw unsupported(\"`;;` (a `case` clause) is not supported.\");\n }\n\n this.pos++;\n\n return \";\";\n }\n\n if (char === \"\\n\") {\n this.consumeNewline();\n\n return \"\\n\";\n }\n\n return null;\n }\n\n private parsePipeline(closing: string | null): void {\n this.parseCommand(closing);\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.peek() !== \"|\" || this.peek(1) === \"|\") {\n return;\n }\n\n if (this.peek(1) === \"&\") {\n throw unsupported(\"`|&` is not portable; write `2>&1 |` instead.\");\n }\n\n this.pos++;\n this.skipBlanksAndNewlines();\n\n if (this.atEnd() || (closing !== null && this.peek() === closing)) {\n throw unsupported(\"The line ends with `|` but no command follows it.\");\n }\n\n this.parseCommand(closing);\n }\n }\n\n private parseCommand(closing: string | null): void {\n this.skipBlanks();\n\n if (this.peek() === \"(\") {\n if (this.peek(1) === \"(\") {\n throw unsupported(\"`((…))` (arithmetic) is not supported.\");\n }\n\n this.parseSubshell();\n\n return;\n }\n\n if (this.peek() === \")\") {\n throw unsupported(closing === null ? \"Unexpected `)`.\" : \"A `( )` or `$( )` is empty.\");\n }\n\n this.parseSimpleCommand();\n }\n\n private parseSubshell(): void {\n const start = this.pos;\n\n this.pos++;\n this.enterNesting();\n this.parseList(\")\", undefined);\n\n if (this.peek() !== \")\") {\n throw unbalanced(\"A `(` is never closed with `)`.\");\n }\n\n this.pos++;\n this.exitNesting();\n\n // Redirections may follow the closing parenthesis; nothing else may.\n const redirects: ShellRedirect[] = [];\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (!this.atRedirectStart()) {\n break;\n }\n\n redirects.push(this.parseRedirect());\n }\n\n if (redirects.length > 0) {\n this.state.commands.push({\n assignments: [],\n words: [],\n redirects,\n source: this.src.slice(start, this.pos).trim(),\n });\n }\n\n if (!this.atEnd() && !this.atOperatorBoundary()) {\n throw unsupported(\"Only an operator or a redirection may follow `)`.\");\n }\n }\n\n /** Whether the next character ends a simple command. */\n private atOperatorBoundary(): boolean {\n const char = this.peek();\n\n return (\n char === \"\\n\" ||\n char === \";\" ||\n char === \"&\" ||\n char === \"|\" ||\n char === \")\" ||\n char === \"#\" ||\n char === \" \" ||\n char === \"\\t\"\n );\n }\n\n /** Whether a redirection starts here (`>`, `<`, or digits glued to one). */\n private atRedirectStart(): boolean {\n let index = this.pos;\n\n while (index < this.src.length && /[0-9]/.test(this.src.charAt(index))) {\n index++;\n }\n\n const char = this.src.charAt(index);\n\n return char === \"<\" || char === \">\";\n }\n\n private parseSimpleCommand(): void {\n const start = this.pos;\n const command: ShellSimpleCommand = { assignments: [], words: [], redirects: [], source: \"\" };\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.atEnd()) {\n break;\n }\n\n const char = this.peek();\n\n if (char === \"\\n\" || char === \";\" || char === \"&\" || char === \"|\" || char === \")\") {\n break;\n }\n\n if (char === \"(\") {\n throw unsupported(\n \"`(` is only supported at the start of a command (a subshell); quote it to pass it as text.\",\n );\n }\n\n if (char === \"#\") {\n this.skipComment();\n\n break;\n }\n\n if (this.atRedirectStart()) {\n command.redirects.push(this.parseRedirect());\n\n continue;\n }\n\n const word = this.parseWord();\n\n if (command.words.length === 0 && word.assignmentName !== undefined) {\n command.assignments.push(word);\n\n continue;\n }\n\n command.words.push(word);\n\n if (command.words.length === 1 && !word.quoted && RESERVED_WORDS.has(word.value)) {\n throw unsupported(\n `\\`${word.value}\\` starts a shell compound command, which is not supported; run the commands one at a time.`,\n );\n }\n }\n\n if (\n command.words.length === 0 &&\n command.assignments.length === 0 &&\n command.redirects.length === 0\n ) {\n throw unsupported(\"The line has an empty command (two operators in a row?).\");\n }\n\n command.source = this.src.slice(start, this.pos).trim();\n this.state.commands.push(command);\n }\n\n private parseRedirect(): ShellRedirect {\n let digits = \"\";\n\n while (/[0-9]/.test(this.peek())) {\n digits += this.peek();\n this.pos++;\n }\n\n const rest = this.src.slice(this.pos, this.pos + 3);\n let op: string;\n\n if (rest.startsWith(\"<<<\")) {\n throw unsupported(\"`<<<` (a here-string) is not portable; use a here-document.\");\n } else if (rest.startsWith(\"<<-\")) {\n op = \"<<-\";\n } else if (rest.startsWith(\"<<\")) {\n op = \"<<\";\n } else if (rest.startsWith(\"<>\")) {\n op = \"<>\";\n } else if (rest.startsWith(\"<&\")) {\n op = \"<&\";\n } else if (rest.startsWith(\"<(\") || rest.startsWith(\">(\")) {\n throw unsupported(\"Process substitution (`<(…)`, `>(…)`) is not supported.\");\n } else if (rest.startsWith(\">>\")) {\n op = \">>\";\n } else if (rest.startsWith(\">|\")) {\n op = \">|\";\n } else if (rest.startsWith(\">&\")) {\n op = \">&\";\n } else {\n op = this.peek();\n }\n\n this.pos += op.length;\n\n const fd = digits === \"\" ? undefined : Number(digits);\n\n this.skipBlanks();\n\n if (this.atEnd() || /[\\n;&|<>()]/.test(this.peek())) {\n throw unsupported(`The redirection \\`${op}\\` has no target.`);\n }\n\n const target = this.parseWord();\n\n if (op === \"<<\" || op === \"<<-\") {\n if (this.state.nesting > 0) {\n throw unsupported(\"Here-documents inside subshells or substitutions are not supported.\");\n }\n\n if (target.dynamic) {\n throw unsupported(\"A here-document delimiter must be literal text.\");\n }\n\n this.pendingHeredocs.push({\n delimiter: target.value,\n stripTabs: op === \"<<-\",\n expand: !target.quoted,\n });\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: \"heredoc\" };\n }\n\n if (op === \">&\" || op === \"<&\") {\n if (target.dynamic || !/^(?:[0-9]+|-)$/.test(target.value)) {\n throw unsupported(\n `\\`${op}${target.raw}\\` is not portable; write \\`> file 2>&1\\` to send both streams to a file.`,\n );\n }\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: \"dup\" };\n }\n\n return {\n op,\n ...(fd !== undefined ? { fd } : {}),\n target,\n type: op === \"<\" ? \"read\" : \"write\",\n };\n }\n\n /** Parse one unquoted-context word. */\n private parseWord(): ShellWord {\n const start = this.pos;\n let value = \"\";\n let dynamic = false;\n let quoted = false;\n let braces = false;\n let nameCandidate = true;\n let assignmentName: string | undefined;\n\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (\n char === \" \" ||\n char === \"\\t\" ||\n char === \"\\n\" ||\n char === \";\" ||\n char === \"&\" ||\n char === \"|\" ||\n char === \"<\" ||\n char === \">\" ||\n char === \"(\" ||\n char === \")\"\n ) {\n break;\n }\n\n if (char === \"\\r\" || char === \"\\0\") {\n throw unsupported(\"Carriage returns and NUL bytes are not supported in a command line.\");\n }\n\n if (char === \"\\\\\") {\n const next = this.peek(1);\n\n if (next === \"\") {\n throw unsupported(\"The line ends with a backslash.\");\n }\n\n this.pos += 2;\n\n if (next !== \"\\n\") {\n value += next;\n quoted = true;\n nameCandidate = false;\n }\n\n continue;\n }\n\n if (char === \"'\") {\n const closing = this.src.indexOf(\"'\", this.pos + 1);\n\n if (closing === -1) {\n throw unbalanced(\"A `'` quote is never closed.\");\n }\n\n value += this.src.slice(this.pos + 1, closing);\n quoted = true;\n nameCandidate = false;\n this.pos = closing + 1;\n\n continue;\n }\n\n if (char === '\"') {\n const part = this.parseDoubleQuoted();\n\n value += part.value;\n dynamic = dynamic || part.dynamic;\n quoted = true;\n nameCandidate = false;\n\n continue;\n }\n\n if (char === \"$\") {\n const part = this.parseDollar(\"unquoted\");\n\n value += part.text;\n dynamic = dynamic || part.dynamic;\n nameCandidate = false;\n\n continue;\n }\n\n if (char === \"`\") {\n value += this.parseBackticks(false);\n dynamic = true;\n nameCandidate = false;\n\n continue;\n }\n\n if (char === \"=\" && nameCandidate && assignmentName === undefined) {\n if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {\n assignmentName = value;\n }\n\n nameCandidate = false;\n } else if (!/[A-Za-z0-9_]/.test(char)) {\n nameCandidate = false;\n }\n\n if (char === \"*\" || char === \"?\" || char === \"[\") {\n dynamic = true;\n }\n\n if (char === \"~\") {\n const previous = this.src.charAt(this.pos - 1);\n\n if (\n this.pos === start ||\n (assignmentName !== undefined && (previous === \"=\" || previous === \":\"))\n ) {\n dynamic = true;\n }\n }\n\n if (char === \"{\" || char === \"}\") {\n braces = true;\n }\n\n value += char;\n this.pos++;\n }\n\n const raw = this.src.slice(start, this.pos);\n\n if (braces && raw !== \"{}\") {\n throw unsupported(\n `Unquoted \\`{\\` or \\`}\\` in \\`${raw}\\` (brace expansion or a command group) is not supported; quote it.`,\n );\n }\n\n // A lone `[` or `]` is never a glob (it is the `test` command or its end).\n if (raw === \"[\" || raw === \"]\") {\n dynamic = false;\n }\n\n return {\n raw,\n value,\n dynamic,\n quoted,\n ...(assignmentName !== undefined ? { assignmentName } : {}),\n };\n }\n\n /** Parse a `\"…\"` span (cursor on the opening quote). */\n private parseDoubleQuoted(): { value: string; dynamic: boolean } {\n this.pos++;\n\n let value = \"\";\n let dynamic = false;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n if (this.atEnd()) {\n throw unbalanced('A `\"` quote is never closed.');\n }\n\n const char = this.peek();\n\n if (char === '\"') {\n this.pos++;\n\n return { value, dynamic };\n }\n\n if (char === \"\\0\") {\n throw unsupported(\"NUL bytes are not supported in a command line.\");\n }\n\n if (char === \"\\\\\") {\n const next = this.peek(1);\n\n if (next === \"\") {\n throw unbalanced('A `\"` quote is never closed.');\n }\n\n if (next === \"$\" || next === \"`\" || next === '\"' || next === \"\\\\\") {\n value += next;\n this.pos += 2;\n\n continue;\n }\n\n if (next === \"\\n\") {\n this.pos += 2;\n\n continue;\n }\n\n value += char;\n this.pos++;\n\n continue;\n }\n\n if (char === \"$\") {\n const part = this.parseDollar(\"dquote\");\n\n value += part.text;\n dynamic = dynamic || part.dynamic;\n\n continue;\n }\n\n if (char === \"`\") {\n value += this.parseBackticks(true);\n dynamic = true;\n\n continue;\n }\n\n value += char;\n this.pos++;\n }\n }\n\n /** Parse a `$…` expansion (cursor on the `$`). */\n private parseDollar(context: \"unquoted\" | \"dquote\" | \"heredoc\"): {\n text: string;\n dynamic: boolean;\n } {\n const start = this.pos;\n const next = this.peek(1);\n\n if (next === \"(\") {\n if (this.peek(2) === \"(\") {\n throw unsupported(\"`$((…))` (arithmetic expansion) is not supported.\");\n }\n\n this.pos += 2;\n this.enterNesting();\n this.parseList(\")\", undefined);\n\n if (this.peek() !== \")\") {\n throw unbalanced(\"A `$(` is never closed with `)`.\");\n }\n\n this.pos++;\n this.exitNesting();\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n if (next === \"{\") {\n const closing = this.src.indexOf(\"}\", this.pos + 2);\n\n if (closing === -1) {\n throw unbalanced(\"A `${` is never closed with `}`.\");\n }\n\n const body = this.src.slice(this.pos + 2, closing);\n\n if (!PARAMETER_EXPANSION.test(body)) {\n throw unsupported(\n `\\`\\${${body}}\\` is not a supported parameter expansion; use \\`$name\\`, \\`\\${name}\\` or \\`\\${name:-default}\\`.`,\n );\n }\n\n this.pos = closing + 1;\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n if (next === \"[\") {\n throw unsupported(\"`$[…]` (arithmetic expansion) is not supported.\");\n }\n\n if (context === \"unquoted\" && (next === \"'\" || next === '\"')) {\n throw unsupported(`\\`$${next}…${next}\\` quoting is not portable; use plain quotes.`);\n }\n\n if (/[A-Za-z_]/.test(next)) {\n this.pos += 2;\n\n while (/[A-Za-z0-9_]/.test(this.peek())) {\n this.pos++;\n }\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n if (/[0-9@*#?$!-]/.test(next) && next !== \"\") {\n this.pos += 2;\n\n return { text: this.src.slice(start, this.pos), dynamic: true };\n }\n\n // A `$` followed by anything else is an ordinary character.\n this.pos++;\n\n return { text: \"$\", dynamic: false };\n }\n\n /** Parse a backtick substitution (cursor on the opening backtick); returns its source. */\n private parseBackticks(insideDoubleQuotes: boolean): string {\n const start = this.pos;\n let content = \"\";\n\n this.pos++;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n if (this.atEnd()) {\n throw unbalanced(\"A backtick substitution is never closed.\");\n }\n\n const char = this.peek();\n\n if (char === \"`\") {\n this.pos++;\n\n break;\n }\n\n if (char === \"\\\\\") {\n const next = this.peek(1);\n\n if (next === \"\") {\n throw unbalanced(\"A backtick substitution is never closed.\");\n }\n\n if (next === \"`\" || next === \"\\\\\" || next === \"$\") {\n content += next;\n this.pos += 2;\n\n continue;\n }\n\n if (next === '\"' && insideDoubleQuotes) {\n throw unsupported(\n 'An escaped `\\\\\"` inside backticks inside double quotes is not portable; use `$(…)`.',\n );\n }\n\n content += char + next;\n this.pos += 2;\n\n continue;\n }\n\n content += char;\n this.pos++;\n }\n\n this.enterNesting();\n new PosixParser(content, this.state).parseNestedProgram();\n this.exitNesting();\n\n return this.src.slice(start, this.pos);\n }\n}\n\n/** Parser for the restricted `cmd.exe` dialect. */\nclass CmdParser {\n private pos = 0;\n\n public constructor(\n private readonly src: string,\n private readonly commands: ShellSimpleCommand[],\n ) {}\n\n public parseProgram(segments: ShellSegment[]): void {\n if (/[\\r\\n\\0]/.test(this.src)) {\n throw unsupported(\"Line breaks and NUL bytes are not supported in a cmd.exe command line.\");\n }\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.atEnd()) {\n throw unsupported(\n segments.length === 0\n ? \"The command is empty.\"\n : \"The line ends with an operator but no command follows it.\",\n );\n }\n\n const start = this.pos;\n\n this.parsePipeline();\n\n const end = this.pos;\n\n this.skipBlanks();\n\n const operator = this.readListOperator();\n\n segments.push({\n source: this.src.slice(start, end).trim(),\n ...(operator !== null ? { operator } : {}),\n });\n\n if (operator === null) {\n if (!this.atEnd()) {\n throw unsupported(`Unexpected \\`${this.peek()}\\`.`);\n }\n\n return;\n }\n }\n }\n\n private atEnd(): boolean {\n return this.pos >= this.src.length;\n }\n\n private peek(offset = 0): string {\n return this.src.charAt(this.pos + offset);\n }\n\n private skipBlanks(): void {\n while (this.peek() === \" \" || this.peek() === \"\\t\") {\n this.pos++;\n }\n }\n\n private readListOperator(): string | null {\n if (this.peek() === \"&\") {\n if (this.peek(1) === \"&\") {\n this.pos += 2;\n\n return \"&&\";\n }\n\n this.pos++;\n\n return \"&\";\n }\n\n if (this.peek() === \"|\" && this.peek(1) === \"|\") {\n this.pos += 2;\n\n return \"||\";\n }\n\n return null;\n }\n\n private parsePipeline(): void {\n this.parseSimpleCommand();\n\n while (this.peek() === \"|\" && this.peek(1) !== \"|\") {\n this.pos++;\n this.skipBlanks();\n\n if (this.atEnd()) {\n throw unsupported(\"The line ends with `|` but no command follows it.\");\n }\n\n this.parseSimpleCommand();\n }\n }\n\n private atRedirectStart(): boolean {\n const char = this.peek();\n\n if (char === \"<\" || char === \">\") {\n return true;\n }\n\n return /[0-9]/.test(char) && (this.peek(1) === \">\" || this.peek(1) === \"<\");\n }\n\n private parseSimpleCommand(): void {\n const start = this.pos;\n const command: ShellSimpleCommand = { assignments: [], words: [], redirects: [], source: \"\" };\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n this.skipBlanks();\n\n if (this.atEnd() || this.peek() === \"&\" || this.peek() === \"|\") {\n break;\n }\n\n if (this.atRedirectStart()) {\n command.redirects.push(this.parseRedirect());\n\n continue;\n }\n\n const word = this.parseWord();\n\n command.words.push(word);\n\n if (command.words.length === 1) {\n this.checkCommandWord(word);\n }\n }\n\n if (command.words.length === 0 && command.redirects.length === 0) {\n throw unsupported(\"The line has an empty command (two operators in a row?).\");\n }\n\n command.source = this.src.slice(start, this.pos).trim();\n this.commands.push(command);\n }\n\n private checkCommandWord(word: ShellWord): void {\n if (word.raw.startsWith(\"@\") || word.raw.startsWith(\":\")) {\n throw unsupported(`\\`${word.raw}\\` is not supported as a cmd.exe command name.`);\n }\n\n if (/[/=]/.test(word.value)) {\n throw unsupported(\n `A cmd.exe command name may not contain \\`/\\` or \\`=\\` (\\`${word.value}\\`); use \\`\\\\\\` in paths.`,\n );\n }\n\n if (!word.quoted && CMD_RESERVED_WORDS.has(word.value.toLowerCase())) {\n throw unsupported(\n `\\`${word.value}\\` starts a cmd.exe compound command, which is not supported.`,\n );\n }\n }\n\n private parseRedirect(): ShellRedirect {\n let fd: number | undefined;\n\n if (/[0-9]/.test(this.peek())) {\n fd = Number(this.peek());\n this.pos++;\n }\n\n let op: string;\n\n if (this.src.startsWith(\">>\", this.pos)) {\n op = \">>\";\n } else if (this.src.startsWith(\">&\", this.pos)) {\n op = \">&\";\n } else if (this.src.startsWith(\"<&\", this.pos) || this.src.startsWith(\"<<\", this.pos)) {\n throw unsupported(\n `\\`${this.src.slice(this.pos, this.pos + 2)}\\` is not supported by cmd.exe.`,\n );\n } else {\n op = this.peek();\n }\n\n this.pos += op.length;\n this.skipBlanks();\n\n if (this.atEnd() || /[&|<>]/.test(this.peek())) {\n throw unsupported(`The redirection \\`${op}\\` has no target.`);\n }\n\n const target = this.parseWord();\n\n if (op === \">&\") {\n if (!/^[0-9]$/.test(target.value)) {\n throw unsupported(`\\`>&${target.raw}\\` is not supported; write \\`> file 2>&1\\`.`);\n }\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: \"dup\" };\n }\n\n return { op, ...(fd !== undefined ? { fd } : {}), target, type: op === \"<\" ? \"read\" : \"write\" };\n }\n\n private parseWord(): ShellWord {\n const start = this.pos;\n let value = \"\";\n let dynamic = false;\n let quoted = false;\n\n while (!this.atEnd()) {\n const char = this.peek();\n\n if (\n char === \" \" ||\n char === \"\\t\" ||\n char === \"&\" ||\n char === \"|\" ||\n char === \"<\" ||\n char === \">\"\n ) {\n break;\n }\n\n if (char === '\"') {\n const closing = this.src.indexOf('\"', this.pos + 1);\n\n if (closing === -1) {\n throw unbalanced('A `\"` quote is never closed.');\n }\n\n const inner = this.src.slice(this.pos + 1, closing);\n\n if (/[%!]/.test(inner)) {\n throw unsupported(\n \"`%` and `!` expand even inside quotes in cmd.exe; they are not supported.\",\n );\n }\n\n if (value.endsWith(\"\\\\\") || inner.endsWith(\"\\\\\")) {\n throw unsupported(\n 'A backslash before a `\"` is read differently by cmd.exe and programs.',\n );\n }\n\n value += inner;\n quoted = true;\n this.pos = closing + 1;\n\n continue;\n }\n\n if (\n char === \"%\" ||\n char === \"!\" ||\n char === \"^\" ||\n char === \"(\" ||\n char === \")\" ||\n char === \";\" ||\n char === \",\"\n ) {\n throw unsupported(\n `Unquoted \\`${char}\\` has a special meaning in cmd.exe and is not supported.`,\n );\n }\n\n if (char === \"\\\\\" && this.peek(1) === '\"') {\n throw unsupported('A backslash before a `\"` is read differently by cmd.exe and programs.');\n }\n\n if (char === \"*\" || char === \"?\") {\n dynamic = true;\n }\n\n value += char;\n this.pos++;\n }\n\n return { raw: this.src.slice(start, this.pos), value, dynamic, quoted };\n }\n}\n\n/**\n * Parse a command line for the shell policy. Never throws: a construct the\n * parser does not read with confidence comes back as `{ ok: false }` with a\n * message naming it.\n *\n * @example\n * const parsed = parseShellLine(\"git add -A && git commit -m 'wip'\", \"posix\");\n * // parsed.ok === true; parsed.commands.map((c) => c.words.map((w) => w.value))\n * // → [[\"git\", \"add\", \"-A\"], [\"git\", \"commit\", \"-m\", \"wip\"]]\n */\nexport function parseShellLine(line: string, dialect: ShellDialect): ShellParseResult {\n const segments: ShellSegment[] = [];\n\n try {\n if (dialect === \"cmd\") {\n const commands: ShellSimpleCommand[] = [];\n\n new CmdParser(line, commands).parseProgram(segments);\n\n return { ok: true, commands, segments, background: false };\n }\n\n const state: SharedState = { commands: [], background: false, nesting: 0 };\n\n new PosixParser(line, state).parseProgram(segments);\n\n return { ok: true, commands: state.commands, segments, background: state.background };\n } catch (error) {\n if (error instanceof ShellSyntaxRefusal) {\n return { ok: false, reason: error.reason, message: error.message };\n }\n\n throw error;\n }\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@
6
6
  * - `"unbalanced-quotes"` — a `'` or `"` is never closed (`quote` says which).
7
7
  * - `"metacharacter"` — an unquoted shell operator (`token`, at `index`).
8
8
  */
9
- type ArgvAnalysis = {
9
+ export type ArgvAnalysis = {
10
10
  ok: true;
11
11
  argv: string[];
12
12
  } | {
@@ -34,7 +34,7 @@ type ArgvAnalysis = {
34
34
  * analyzeArgvCommand("npm test"); // { ok: true, argv: ["npm", "test"] }
35
35
  * analyzeArgvCommand("git add -A && git commit"); // { ok: false, type: "metacharacter", token: "&&", index: 11 }
36
36
  */
37
- declare function analyzeArgvCommand(command: string): ArgvAnalysis;
37
+ export declare function analyzeArgvCommand(command: string): ArgvAnalysis;
38
38
  /**
39
39
  * Tokenize a command line into an argv array WITHOUT any shell semantics.
40
40
  *
@@ -57,7 +57,6 @@ declare function analyzeArgvCommand(command: string): ArgvAnalysis;
57
57
  * tokenizeCommand('node -e "console.log(1)"'); // ["node", "-e", "console.log(1)"]
58
58
  * tokenizeCommand('npm test; curl http://evil'); // null (unquoted `;`)
59
59
  */
60
- declare function tokenizeCommand(command: string): string[] | null;
60
+ export declare function tokenizeCommand(command: string): string[] | null;
61
61
  //#endregion
62
- export { ArgvAnalysis, analyzeArgvCommand, tokenizeCommand };
63
62
  //# sourceMappingURL=tokenize-command.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokenize-command.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/tokenize-command.ts"],"mappings":";;AAqBA;;;;;;KAAY,YAAA;EACN,EAAA;EAAU,IAAA;AAAA;EACV,EAAA;EAAW,IAAA;AAAA;EACX,EAAA;EAAW,IAAA;EAA2B,KAAA;EAAkB,KAAA;AAAA;EACxD,EAAA;EAAW,IAAA;EAAuB,KAAA;EAAe,KAAA;AAAA;AA8FvD;;;;AAA+C;;;;;;;AAA/C,iBAjFgB,kBAAA,CAAmB,OAAA,WAAkB,YAAY;;;;;;;;;;;;;;;;;;;;;;;iBAiFjD,eAAA,CAAgB,OAAe"}
1
+ {"version":3,"file":"tokenize-command.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/tokenize-command.ts"],"mappings":";;;;;;;;YAqBY;EACN;EAAU;;EACV;EAAW;;EACX;EAAW;EAA2B;EAAkB;;EACxD;EAAW;EAAuB;EAAe;;;;;;;;;;;;;wBAavC,mBAAmB,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;wBAiFrC,gBAAgB"}
@@ -8,7 +8,7 @@
8
8
  * rejected instead of silently running commands past the allowlist. Inside
9
9
  * quotes they are ordinary literal bytes and pass through as argument data.
10
10
  */
11
- const UNQUOTED_METACHARACTERS = new Set([
11
+ const UNQUOTED_METACHARACTERS = /* @__PURE__ */ new Set([
12
12
  ";",
13
13
  "&",
14
14
  "|",
@@ -20,7 +20,7 @@ const UNQUOTED_METACHARACTERS = new Set([
20
20
  ")"
21
21
  ]);
22
22
  /** Two-character operators reported as one token in a refusal. */
23
- const TWO_CHARACTER_OPERATORS = new Set([
23
+ const TWO_CHARACTER_OPERATORS = /* @__PURE__ */ new Set([
24
24
  "&&",
25
25
  "||",
26
26
  ">>",