( command: string, allowedCommands: string[], deniedCommands?: string[], )
| 255 | * @returns Decision indicating whether to approve, deny, or ask user |
| 256 | */ |
| 257 | export function getCommandDecision( |
| 258 | command: string, |
| 259 | allowedCommands: string[], |
| 260 | deniedCommands?: string[], |
| 261 | ): CommandDecision { |
| 262 | if (!command?.trim()) { |
| 263 | return "auto_approve" |
| 264 | } |
| 265 | |
| 266 | // Parse into sub-commands (split by &&, ||, ;, |). parseCommand also |
| 267 | // detects shell syntax errors (unterminated quotes, unclosed heredocs) and |
| 268 | // returns a non-null parseError in that case. |
| 269 | const { commands: subCommands, parseError } = parseCommand(command) |
| 270 | |
| 271 | // Reject commands with a shell syntax error. An unterminated quote means |
| 272 | // the shell would report a parse error; in a compound command it may |
| 273 | // partially execute the well-formed prefix before aborting. Returning a |
| 274 | // distinct decision lets callers surface a useful message to the agent |
| 275 | // rather than silently presenting the command for user approval. |
| 276 | if (parseError !== null) { |
| 277 | return "malformed_command" |
| 278 | } |
| 279 | |
| 280 | // Check each sub-command and collect decisions |
| 281 | const decisions: CommandDecision[] = subCommands.map((cmd) => { |
| 282 | // Remove simple PowerShell-like redirections (e.g. 2>&1) before checking |
| 283 | const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, "").trim() |
| 284 | |
| 285 | return getSingleCommandDecision(cmdWithoutRedirection, allowedCommands, deniedCommands) |
| 286 | }) |
| 287 | |
| 288 | // If any sub-command is denied, deny the whole command |
| 289 | if (decisions.includes("auto_deny")) { |
| 290 | return "auto_deny" |
| 291 | } |
| 292 | |
| 293 | // Require explicit user approval for dangerous patterns |
| 294 | if (containsDangerousSubstitution(command)) { |
| 295 | return "ask_user" |
| 296 | } |
| 297 | |
| 298 | // If all sub-commands are approved, approve the whole command |
| 299 | if (decisions.every((decision) => decision === "auto_approve")) { |
| 300 | return "auto_approve" |
| 301 | } |
| 302 | |
| 303 | // Otherwise, ask user |
| 304 | return "ask_user" |
| 305 | } |
| 306 | |
| 307 | /** |
| 308 | * Get the decision for a single command using longest prefix match rule. |
no test coverage detected