(
input: { command: string },
parsed: ParsedPowerShellCommand,
toolPermissionContext: ToolPermissionContext,
compoundCommandHasCd = false,
)
| 1526 | * - 'passthrough' if no path commands were found or all paths are valid |
| 1527 | */ |
| 1528 | export function checkPathConstraints( |
| 1529 | input: { command: string }, |
| 1530 | parsed: ParsedPowerShellCommand, |
| 1531 | toolPermissionContext: ToolPermissionContext, |
| 1532 | compoundCommandHasCd = false, |
| 1533 | ): PermissionResult { |
| 1534 | if (!parsed.valid) { |
| 1535 | return { |
| 1536 | behavior: 'passthrough', |
| 1537 | message: 'Cannot validate paths for unparsed command', |
| 1538 | } |
| 1539 | } |
| 1540 | |
| 1541 | // SECURITY: Two-pass approach — check ALL statements/paths so deny rules |
| 1542 | // always take precedence over ask. Without this, an ask on statement 1 |
| 1543 | // could return before checking statement 2 for deny rules, letting the |
| 1544 | // user approve a command that includes a denied path. |
| 1545 | let firstAsk: PermissionResult | undefined |
| 1546 | |
| 1547 | for (const statement of parsed.statements) { |
| 1548 | const result = checkPathConstraintsForStatement( |
| 1549 | statement, |
| 1550 | toolPermissionContext, |
| 1551 | compoundCommandHasCd, |
| 1552 | ) |
| 1553 | if (result.behavior === 'deny') { |
| 1554 | return result |
| 1555 | } |
| 1556 | if (result.behavior === 'ask' && !firstAsk) { |
| 1557 | firstAsk = result |
| 1558 | } |
| 1559 | } |
| 1560 | |
| 1561 | return ( |
| 1562 | firstAsk ?? { |
| 1563 | behavior: 'passthrough', |
| 1564 | message: 'All path constraints validated successfully', |
| 1565 | } |
| 1566 | ) |
| 1567 | } |
| 1568 | |
| 1569 | function checkPathConstraintsForStatement( |
| 1570 | statement: ParsedPowerShellCommand['statements'][number], |
no test coverage detected