Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions src/Netclaw.Actors.Tests/Tools/ShellApprovalCaseCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,232 @@ public static class ShellApprovalCases
Approvals.None,
ExpectedApproval.Require([], isMessy: true, approvalChecks: 0)),

// These synthetic cases represent the dominant search, pipeline, and
// file-change shapes in the sanitized local approval-prompt sample.
// No command text, path, identifier, or free text came from the sample.
Case(
"workload-search-rg-in-project-allows",
Bash("rg -n \"TODO\" src"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-grep-in-project-allows",
Bash("grep -R \"error\" src"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-find-in-project-allows",
Bash("find src -name \"*.cs\" -print"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-cat-in-project-allows",
Bash("cat src/file.txt"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-head-in-project-allows",
Bash("head -40 src/file.txt"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-tail-in-project-allows",
Bash("tail -100 logs/app.log"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-sed-print-in-project-currently-prompts",
Bash("sed -n '20,80p' src/file.txt"),
Approvals.None,
ExpectedApproval.Require(["sed"])),
Case(
"workload-search-rg-external-prompts",
Bash("rg -n \"TODO\" .", ApprovalDirectoryShape.External),
Approvals.None,
ExpectedApproval.Require(["rg"])),
Case(
"workload-search-rg-external-grant-allows",
Bash("rg -n \"TODO\" .", ApprovalDirectoryShape.External),
Approvals.PersistentHere(ApprovalDirectoryShape.External, "rg"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:rg")),
Case(
"workload-search-rg-head-pipeline-allows",
Bash("rg -n \"TODO\" src | head -40"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-grep-tail-pipeline-allows",
Bash("grep -R \"error\" logs | tail -20"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-find-head-pipeline-allows",
Bash("find src -name \"*.cs\" -print | head -20"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-search-cat-jq-pipeline-currently-prompts",
Bash("cat config.json | jq '.items[]'"),
Approvals.None,
ExpectedApproval.Require(["cat", "jq"])),
Case(
"workload-search-jq-direct-prompts",
Bash("jq '.items[]' config.json"),
Approvals.None,
ExpectedApproval.Require(["jq"])),
Case(
"workload-search-jq-direct-grant-allows",
Bash("jq '.items[]' config.json"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "jq"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:jq")),
Case(
"workload-search-cat-jq-stored-tail-currently-prompts",
Bash("cat config.json | jq '.items[]'"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "jq"),
ExpectedApproval.Require(
["cat", "jq"],
approvalMatches: ["persistent:jq"])),
Case(
"workload-edit-grep-tee-pipeline-prompts",
Bash("grep \"error\" logs/app.log | tee reports/errors.txt"),
Approvals.None,
ExpectedApproval.Require(["grep", "tee"])),
Case(
"workload-edit-tee-direct-prompts",
Bash("tee reports/output.txt"),
Approvals.None,
ExpectedApproval.Require(["tee"])),
Case(
"workload-edit-tee-direct-grant-allows",
Bash("tee reports/output.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "tee"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:tee")),
Case(
"workload-edit-grep-tee-stored-tail-currently-prompts",
Bash("grep \"error\" logs/app.log | tee reports/errors.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "tee"),
ExpectedApproval.Require(
["grep", "tee"],
approvalMatches: ["persistent:tee"])),
Case(
"workload-edit-sed-in-place-prompts",
Bash("sed -i 's/old/new/' src/file.txt"),
Approvals.None,
ExpectedApproval.Require(["sed"])),
Case(
"workload-edit-sed-in-place-grant-allows",
Bash("sed -i 's/old/new/' src/file.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "sed"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:sed")),
Case(
"workload-edit-copy-prompts",
Bash("cp src/input.txt src/output.txt"),
Approvals.None,
ExpectedApproval.Require(["cp"])),
Case(
"workload-edit-copy-grant-allows",
Bash("cp src/input.txt src/output.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "cp"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:cp")),
Case(
"workload-edit-move-prompts",
Bash("mv src/old.txt src/new.txt"),
Approvals.None,
ExpectedApproval.Require(["mv"])),
Case(
"workload-edit-move-grant-allows",
Bash("mv src/old.txt src/new.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "mv"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:mv")),
Case(
"workload-edit-touch-prompts",
Bash("touch src/new.txt"),
Approvals.None,
ExpectedApproval.Require(["touch"])),
Case(
"workload-edit-touch-grant-allows",
Bash("touch src/new.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "touch"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:touch")),
Case(
"workload-edit-mkdir-prompts",
Bash("mkdir -p reports/output"),
Approvals.None,
ExpectedApproval.Require(["mkdir"])),
Case(
"workload-edit-mkdir-grant-allows",
Bash("mkdir -p reports/output"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "mkdir"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:mkdir")),
Case(
"workload-edit-remove-prompts",
Bash("rm -- src/obsolete.txt"),
Approvals.None,
ExpectedApproval.Require(["rm"])),
Case(
"workload-edit-remove-grant-allows",
Bash("rm -- src/obsolete.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "rm"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:rm")),
Case(
"workload-edit-printf-redirect-prompts",
Bash("printf '%s\\n' \"text\" > reports/output.txt"),
Approvals.None,
ExpectedApproval.Require(["printf"])),
Case(
"workload-edit-printf-redirect-grant-allows",
Bash("printf '%s\\n' \"text\" > reports/output.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "printf"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:printf")),
Case(
"workload-edit-search-pipeline-redirect-in-project-allows",
Bash("grep -R \"error\" logs | head -20 > reports/errors.txt"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"workload-edit-search-pipeline-redirect-external-prompts",
Bash(
"grep -R \"error\" logs | head -20 > reports/errors.txt",
ApprovalDirectoryShape.External),
Approvals.None,
ExpectedApproval.Require(["grep", "head"])),
Case(
"workload-edit-search-pipeline-redirect-external-grant-allows",
Bash(
"grep -R \"error\" logs | head -20 > reports/errors.txt",
ApprovalDirectoryShape.External),
Approvals.PersistentHere(ApprovalDirectoryShape.External, "grep", "head"),
ExpectedApproval.Allow(
ToolAllowReason.StoredApproval,
1,
"persistent:grep",
"persistent:head")),
Case(
"workload-search-loop-currently-complex",
Bash("for f in src/*.cs; do grep -n \"TODO\" \"$f\"; done"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "grep"),
ExpectedApproval.Require([], isMessy: true, approvalChecks: 0)),
Case(
"workload-edit-loop-currently-complex",
Bash("for f in src/a.txt src/b.txt; do sed -i 's/old/new/' \"$f\"; done"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "sed"),
ExpectedApproval.Require([], isMessy: true, approvalChecks: 0)),
Case(
"workload-search-dynamic-root-remains-complex",
Bash("grep -R \"error\" \"$SEARCH_ROOT\""),
Approvals.PersistentAnywhere("grep"),
ExpectedApproval.Require([], isMessy: true, approvalChecks: 0)),
Case(
"workload-search-substitution-pipeline-redirect-remains-complex",
Bash("pattern=$(printf '%s' error); grep -R \"$pattern\" src | head -20 > reports/errors.txt"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "grep", "head", "printf"),
ExpectedApproval.Require([], isMessy: true, approvalChecks: 0)),
Case(
"workload-search-loop-substitution-pipeline-redirect-remains-complex",
Bash("for f in logs/*.log; do grep -n \"$(printf '%s' error)\" \"$f\" | head -20 > \"reports/$f.txt\"; done"),
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "grep", "head", "printf"),
ExpectedApproval.Require([], isMessy: true, approvalChecks: 0)),

Case(
"echo-allows-without-grant",
Bash("echo hello"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,48 @@
| multiple-cd-then-safe-prompts | Personal | Project | Interactive | cd . && cd .. && git status | none | RequiresApproval | approval required | cd, git status | No |
| side-effect-before-mutation-prompts | Personal | Project | Interactive | echo ready && git push | none | RequiresApproval | approval required | echo, git push | No |
| heredoc-prompts | Personal | Project | Interactive | cat <<'EOF'\nhello\nEOF | none | RequiresApproval | approval required | none | Yes |
| workload-search-rg-in-project-allows | Personal | Project | Interactive | rg -n "TODO" src | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-grep-in-project-allows | Personal | Project | Interactive | grep -R "error" src | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-find-in-project-allows | Personal | Project | Interactive | find src -name "*.cs" -print | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-cat-in-project-allows | Personal | Project | Interactive | cat src/file.txt | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-head-in-project-allows | Personal | Project | Interactive | head -40 src/file.txt | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-tail-in-project-allows | Personal | Project | Interactive | tail -100 logs/app.log | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-sed-print-in-project-currently-prompts | Personal | Project | Interactive | sed -n '20,80p' src/file.txt | none | RequiresApproval | approval required | sed | No |
| workload-search-rg-external-prompts | Personal | External | Interactive | rg -n "TODO" . | none | RequiresApproval | approval required | rg | No |
| workload-search-rg-external-grant-allows | Personal | External | Interactive | rg -n "TODO" . | persistent[external]:rg | Allowed | StoredApproval | none | Not applicable |
| workload-search-rg-head-pipeline-allows | Personal | Project | Interactive | rg -n "TODO" src \| head -40 | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-grep-tail-pipeline-allows | Personal | Project | Interactive | grep -R "error" logs \| tail -20 | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-find-head-pipeline-allows | Personal | Project | Interactive | find src -name "*.cs" -print \| head -20 | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-search-cat-jq-pipeline-currently-prompts | Personal | Project | Interactive | cat config.json \| jq '.items[]' | none | RequiresApproval | approval required | cat, jq | No |
| workload-search-jq-direct-prompts | Personal | Project | Interactive | jq '.items[]' config.json | none | RequiresApproval | approval required | jq | No |
| workload-search-jq-direct-grant-allows | Personal | Project | Interactive | jq '.items[]' config.json | persistent[project]:jq | Allowed | StoredApproval | none | Not applicable |
| workload-search-cat-jq-stored-tail-currently-prompts | Personal | Project | Interactive | cat config.json \| jq '.items[]' | persistent[project]:jq | RequiresApproval | approval required | cat, jq | No |
| workload-edit-grep-tee-pipeline-prompts | Personal | Project | Interactive | grep "error" logs/app.log \| tee reports/errors.txt | none | RequiresApproval | approval required | grep, tee | No |
| workload-edit-tee-direct-prompts | Personal | Project | Interactive | tee reports/output.txt | none | RequiresApproval | approval required | tee | No |
| workload-edit-tee-direct-grant-allows | Personal | Project | Interactive | tee reports/output.txt | persistent[project]:tee | Allowed | StoredApproval | none | Not applicable |
| workload-edit-grep-tee-stored-tail-currently-prompts | Personal | Project | Interactive | grep "error" logs/app.log \| tee reports/errors.txt | persistent[project]:tee | RequiresApproval | approval required | grep, tee | No |
| workload-edit-sed-in-place-prompts | Personal | Project | Interactive | sed -i 's/old/new/' src/file.txt | none | RequiresApproval | approval required | sed | No |
| workload-edit-sed-in-place-grant-allows | Personal | Project | Interactive | sed -i 's/old/new/' src/file.txt | persistent[project]:sed | Allowed | StoredApproval | none | Not applicable |
| workload-edit-copy-prompts | Personal | Project | Interactive | cp src/input.txt src/output.txt | none | RequiresApproval | approval required | cp | No |
| workload-edit-copy-grant-allows | Personal | Project | Interactive | cp src/input.txt src/output.txt | persistent[project]:cp | Allowed | StoredApproval | none | Not applicable |
| workload-edit-move-prompts | Personal | Project | Interactive | mv src/old.txt src/new.txt | none | RequiresApproval | approval required | mv | No |
| workload-edit-move-grant-allows | Personal | Project | Interactive | mv src/old.txt src/new.txt | persistent[project]:mv | Allowed | StoredApproval | none | Not applicable |
| workload-edit-touch-prompts | Personal | Project | Interactive | touch src/new.txt | none | RequiresApproval | approval required | touch | No |
| workload-edit-touch-grant-allows | Personal | Project | Interactive | touch src/new.txt | persistent[project]:touch | Allowed | StoredApproval | none | Not applicable |
| workload-edit-mkdir-prompts | Personal | Project | Interactive | mkdir -p reports/output | none | RequiresApproval | approval required | mkdir | No |
| workload-edit-mkdir-grant-allows | Personal | Project | Interactive | mkdir -p reports/output | persistent[project]:mkdir | Allowed | StoredApproval | none | Not applicable |
| workload-edit-remove-prompts | Personal | Project | Interactive | rm -- src/obsolete.txt | none | RequiresApproval | approval required | rm | No |
| workload-edit-remove-grant-allows | Personal | Project | Interactive | rm -- src/obsolete.txt | persistent[project]:rm | Allowed | StoredApproval | none | Not applicable |
| workload-edit-printf-redirect-prompts | Personal | Project | Interactive | printf '%s\n' "text" > reports/output.txt | none | RequiresApproval | approval required | printf | No |
| workload-edit-printf-redirect-grant-allows | Personal | Project | Interactive | printf '%s\n' "text" > reports/output.txt | persistent[project]:printf | Allowed | StoredApproval | none | Not applicable |
| workload-edit-search-pipeline-redirect-in-project-allows | Personal | Project | Interactive | grep -R "error" logs \| head -20 > reports/errors.txt | none | Allowed | SafeVerbInTrustedScope | none | Not applicable |
| workload-edit-search-pipeline-redirect-external-prompts | Personal | External | Interactive | grep -R "error" logs \| head -20 > reports/errors.txt | none | RequiresApproval | approval required | grep, head | No |
| workload-edit-search-pipeline-redirect-external-grant-allows | Personal | External | Interactive | grep -R "error" logs \| head -20 > reports/errors.txt | persistent[external]:grep, persistent[external]:head | Allowed | StoredApproval | none | Not applicable |
| workload-search-loop-currently-complex | Personal | Project | Interactive | for f in src/*.cs; do grep -n "TODO" "$f"; done | persistent[project]:grep | RequiresApproval | approval required | none | Yes |
| workload-edit-loop-currently-complex | Personal | Project | Interactive | for f in src/a.txt src/b.txt; do sed -i 's/old/new/' "$f"; done | persistent[project]:sed | RequiresApproval | approval required | none | Yes |
| workload-search-dynamic-root-remains-complex | Personal | Project | Interactive | grep -R "error" "$SEARCH_ROOT" | persistent[anywhere]:grep | RequiresApproval | approval required | none | Yes |
| workload-search-substitution-pipeline-redirect-remains-complex | Personal | Project | Interactive | pattern=$(printf '%s' error); grep -R "$pattern" src \| head -20 > reports/errors.txt | persistent[project]:grep, persistent[project]:head, persistent[project]:printf | RequiresApproval | approval required | none | Yes |
| workload-search-loop-substitution-pipeline-redirect-remains-complex | Personal | Project | Interactive | for f in logs/*.log; do grep -n "$(printf '%s' error)" "$f" \| head -20 > "reports/$f.txt"; done | persistent[project]:grep, persistent[project]:head, persistent[project]:printf | RequiresApproval | approval required | none | Yes |
| echo-allows-without-grant | Personal | Project | Interactive | echo hello | none | Allowed | ApprovalExemptShellCandidates | none | Not applicable |
| printf-allows-without-grant | Personal | Project | Interactive | printf hello | none | Allowed | ApprovalExemptShellCandidates | none | Not applicable |
| echo-redirect-prompts | Personal | Project | Interactive | echo hello > result.txt | none | RequiresApproval | approval required | echo | No |
Expand Down
Loading