diff --git a/src/Netclaw.Actors.Tests/Tools/ShellApprovalCaseCatalog.cs b/src/Netclaw.Actors.Tests/Tools/ShellApprovalCaseCatalog.cs index 552b11939..7263aced8 100644 --- a/src/Netclaw.Actors.Tests/Tools/ShellApprovalCaseCatalog.cs +++ b/src/Netclaw.Actors.Tests/Tools/ShellApprovalCaseCatalog.cs @@ -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"), diff --git a/src/Netclaw.Actors.Tests/Tools/ShellApprovalDispositionMatrixTests.Shell_approval_cases_match_review_table.verified.md b/src/Netclaw.Actors.Tests/Tools/ShellApprovalDispositionMatrixTests.Shell_approval_cases_match_review_table.verified.md index 67552a9df..34f3108db 100644 --- a/src/Netclaw.Actors.Tests/Tools/ShellApprovalDispositionMatrixTests.Shell_approval_cases_match_review_table.verified.md +++ b/src/Netclaw.Actors.Tests/Tools/ShellApprovalDispositionMatrixTests.Shell_approval_cases_match_review_table.verified.md @@ -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 |