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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"schemaVersion": 1,
"schemaVersion": 2,
"fixtureDefaults": {
"toolName": "shell_execute",
"audience": "Personal",
Expand Down Expand Up @@ -409,5 +409,153 @@
],
"expectedFinal": { "outcome": "RequiresApproval", "reason": "UncoveredCandidates", "approvalCandidates": ["gh pr close"], "isMessy": false, "agentCorrection": null }
}
],
"adversarialCases": [
{
"id": "A01",
"category": "DynamicIdentity",
"command": "\"$tool\" --version",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [] },
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": [], "isMessy": true, "optionKeys": ["approve_once", "deny"], "actorCheckCount": 0 }
},
{
"id": "A02",
"category": "DenyOnlyWrapper",
"command": "bash -c 'rm -rf /'",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [] },
"expected": { "outcome": "Deny", "denyReason": "hard_deny_system_destructive", "approvalCandidates": null, "isMessy": null, "optionKeys": null, "actorCheckCount": 0 }
},
{
"id": "A03",
"category": "FileWritingRedirect",
"command": "head README.md > report.txt",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [{ "tokens": ["head"], "proof": "ReviewedDiagnostic" }] },
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": ["head"], "isMessy": false, "optionKeys": ["approve_once", "approve_session", "approve_everywhere", "deny"], "actorCheckCount": 1 }
},
{
"id": "A04",
"category": "ProtectedPath",
"command": "Get-Content C:\\protected\\config",
"environment": { "platform": "Windows", "executablePath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", "commandArguments": ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"], "grammar": "PowerShell", "pathStyle": "Windows", "powerShellDialect": "PowerShell7" },
"initialWorkingDirectory": "C:\\work",
"projectDirectory": "C:\\work",
"sessionDirectory": "C:\\work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [{ "tokens": ["Get-Content"], "proof": "ReviewedDiagnostic" }] },
"expected": { "outcome": "Deny", "denyReason": "shell_references_protected_path", "approvalCandidates": null, "isMessy": null, "optionKeys": null, "actorCheckCount": 0 }
},
{
"id": "A05",
"category": "PrefixCollision",
"command": "git statusx",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [{ "kind": "PersistentGlobal", "shell": "Bash", "match": "TokenPrefix", "tokens": ["git", "status"], "directory": null }], "safePhrases": [] },
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": ["git statusx"], "isMessy": false, "optionKeys": ["approve_once", "approve_session", "approve_everywhere", "deny"], "actorCheckCount": 1 }
},
{
"id": "A06",
"category": "RuntimeIterator",
"command": "for f in $(list-files); do cat \"$f\"; done",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": {
"oneTimeApprovalKeys": [],
"sessionGrants": [],
"persistentGrants": [{ "kind": "PersistentGlobal", "shell": "Bash", "match": "TokenPrefix", "tokens": ["cat"], "directory": null }],
"safePhrases": [{ "tokens": ["cat"], "proof": "ReviewedDiagnostic" }]
},
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": [], "isMessy": true, "optionKeys": ["approve_once", "deny"], "actorCheckCount": 0 }
},
{
"id": "A07",
"category": "PowerShellProvider",
"command": "Get-Content Env:SECRET",
"environment": { "platform": "Windows", "executablePath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", "commandArguments": ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"], "grammar": "PowerShell", "pathStyle": "Windows", "powerShellDialect": "PowerShell7" },
"initialWorkingDirectory": "C:\\work",
"projectDirectory": "C:\\work",
"sessionDirectory": "C:\\work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [{ "tokens": ["Get-Content"], "proof": "ReviewedDiagnostic" }] },
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": [], "isMessy": true, "optionKeys": ["approve_once", "deny"], "actorCheckCount": 0 }
},
{
"id": "A08",
"category": "UnsafeCatalogFindExec",
"command": "find . -exec rm {} +",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [] },
"useBundledSafeCatalog": true,
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": ["find"], "isMessy": false, "optionKeys": ["approve_once", "approve_session", "approve_everywhere", "deny"], "actorCheckCount": 1 }
},
{
"id": "A09",
"category": "UnsafeCatalogAwkSystem",
"command": "awk 'BEGIN { system(\"touch marker\") }'",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [] },
"useBundledSafeCatalog": true,
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": ["awk"], "isMessy": false, "optionKeys": ["approve_once", "approve_session", "approve_everywhere", "deny"], "actorCheckCount": 1 }
},
{
"id": "A10",
"category": "UnsafeCatalogRgPreprocessor",
"command": "rg --pre helper pattern .",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [] },
"useBundledSafeCatalog": true,
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": ["rg"], "isMessy": false, "optionKeys": ["approve_once", "approve_session", "approve_everywhere", "deny"], "actorCheckCount": 1 }
},
{
"id": "A11",
"category": "UnsafeCatalogSortOutput",
"command": "sort -o output input",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": { "oneTimeApprovalKeys": [], "sessionGrants": [], "persistentGrants": [], "safePhrases": [] },
"useBundledSafeCatalog": true,
"expected": { "outcome": "RequiresApproval", "denyReason": null, "approvalCandidates": ["sort"], "isMessy": false, "optionKeys": ["approve_once", "approve_session", "approve_everywhere", "deny"], "actorCheckCount": 1 }
},
{
"id": "A12",
"category": "ExactOneTimeAuthority",
"command": "git push",
"environment": { "platform": "Linux", "executablePath": "/bin/bash", "commandArguments": ["-c"], "grammar": "Bash", "pathStyle": "Posix", "powerShellDialect": null },
"initialWorkingDirectory": "/work",
"projectDirectory": "/work",
"sessionDirectory": "/work",
"available": {
"oneTimeApprovalKeys": ["git push", "\u0000candidate-v2:D404315B7AE7E1AB2E5DCDFCBFC1BC8A39770526922A4FCD7AB854C40B06F8BF"],
"sessionGrants": [],
"persistentGrants": [],
"safePhrases": []
},
"expected": { "outcome": "Allow", "denyReason": null, "approvalCandidates": null, "isMessy": null, "optionKeys": null, "actorCheckCount": 1 }
}
]
}
12 changes: 6 additions & 6 deletions openspec/changes/structure-shell-approval-policy/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
candidates, phrases, scopes, grant and safe inputs, coverage, ordered trace,
and outcome for D02, D03, D07, D08, D09, D10, D11, D14, D17, and D18; tests
must load explicit authority defaults and fields, not branch on IDs.
- [ ] 1.5 Add adversarial dynamic identity, redirect, protected path, prefix
- [x] 1.5 Add adversarial dynamic identity, redirect, protected path, prefix
collision, runtime loop, wrapper, provider, and unsafe-catalog cases.
- [x] 1.6 Run the PII audit and manually inspect every command and fixture.

Expand All @@ -28,7 +28,7 @@
`ToolApprovalAttempt`.
- [x] 2.4 Route `DispatchingToolExecutor` through the coordinator without
changing the original source, argument object, or tool history.
- [ ] 2.5 Preserve session-pipeline pending-request persistence,
- [x] 2.5 Preserve session-pipeline pending-request persistence,
stale/duplicate response rejection and recovery; preserve exact-set one-time
retry in `ToolApprovalAttempt` and actor-owned subagent scope inheritance.

Expand All @@ -47,9 +47,9 @@
reviewed-safe phrase coverage for an interactive run. Deny with
`ApprovalStoreUnavailable` rather than open a prompt when any candidate still
depends on persistent state.
- [ ] 3.5 Let expected unresolved shell input offer only one-time approval and
- [x] 3.5 Let expected unresolved shell input offer only one-time approval and
deny; never create a reusable candidate.
- [ ] 3.6 Keep legacy token scans deny-only and prove they cannot authorize,
- [x] 3.6 Keep legacy token scans deny-only and prove they cannot authorize,
create persistence choices, or widen scope.
- [x] 3.7 Apply reviewed-safe phrase coverage only when interactive approval is
available. Prove that unattended calls need explicit one-time or stored-grant
Expand All @@ -66,9 +66,9 @@
internal store failures deny and never salvage partial authority.
- [x] 4.4 Use the same typed phrase comparison for one-time, session,
persistent, global, and folder coverage.
- [ ] 4.5 Require real exact scope, normalization, containment, and symlink
- [x] 4.5 Require real exact scope, normalization, containment, and symlink
checks for folder grants; global grants do not require cwd.
- [ ] 4.6 Preserve prompt display/spoof protections and store one clean entry per
- [x] 4.6 Preserve prompt display/spoof protections and store one clean entry per
persistable candidate.
- [x] 4.7 Update CLI list/add/revoke behavior and operator docs for schema 3,
including manual rollback by restoring the preserved `.v2.bak` while the
Expand Down
24 changes: 12 additions & 12 deletions src/Netclaw.Actors.Tests/Tools/ShellApprovalCaseCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,10 @@ public static class ShellApprovalCases
Approvals.None,
ExpectedApproval.Require(["git status"])),
Case(
"safe-verb-null-device-redirect-allows",
"safe-verb-null-device-redirect-prompts",
Bash("ls -la 2>/dev/null"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
ExpectedApproval.Require(["ls"])),
Case(
"mutating-verb-project-prompts",
Bash("git push"),
Expand Down Expand Up @@ -802,10 +802,10 @@ public static class ShellApprovalCases
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"powershell7-local-redirect-keeps-safe-command",
"powershell7-local-redirect-prompts-for-writer",
PowerShell7(@"Get-Content .\input.txt > .\output.txt"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
ExpectedApproval.Require(["Get-Content"])),
Case(
"powershell7-protected-path-denies-before-approval",
PowerShell7(@"Get-Content C:\protected\config\secret.txt"),
Expand Down Expand Up @@ -992,20 +992,20 @@ public static class ShellApprovalCases
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
Case(
"combined-output-project-redirect-safe-verb-allows",
"combined-output-project-redirect-safe-verb-prompts",
Bash("git status &> result.log"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
ExpectedApproval.Require(["git status"])),
Case(
"combined-output-append-project-redirect-safe-verb-allows",
"combined-output-append-project-redirect-safe-verb-prompts",
Bash("git status &>> result.log"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
ExpectedApproval.Require(["git status"])),
Case(
"numeric-source-project-redirect-safe-verb-allows",
"numeric-source-project-redirect-safe-verb-prompts",
Bash("git status 3> result.log"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
ExpectedApproval.Require(["git status"])),
Case(
"fd-dup-redirect-mutating-no-grant-prompts-not-messy",
Bash("git push origin dev 2>&1 | tail -2"),
Expand Down Expand Up @@ -1299,10 +1299,10 @@ public static class ShellApprovalCases
Approvals.PersistentHere(ApprovalDirectoryShape.Project, "printf"),
ExpectedApproval.Allow(ToolAllowReason.StoredApproval, 1, "persistent:printf")),
Case(
"workload-edit-search-pipeline-redirect-in-project-allows",
"workload-edit-search-pipeline-redirect-in-project-prompts-for-writer",
Bash("grep -R \"error\" logs | head -20 > reports/errors.txt"),
Approvals.None,
ExpectedApproval.Allow(ToolAllowReason.SafeVerbInTrustedScope)),
ExpectedApproval.Require(["head"])),
Case(
"workload-edit-search-pipeline-redirect-external-prompts",
Bash(
Expand Down
Loading
Loading