diff --git a/conductor/tier2/opencode.json.fragment b/conductor/tier2/opencode.json.fragment index 582273bd..620d12bb 100644 --- a/conductor/tier2/opencode.json.fragment +++ b/conductor/tier2/opencode.json.fragment @@ -1,6 +1,53 @@ { "$schema": "https://opencode.ai/config.json", "default_agent": "tier2-autonomous", + "permission": { + "edit": "deny", + "read": { + "*": "deny", + "C:\\projects\\manual_slop_tier2\\**": "allow", + "C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2\\**": "allow", + "C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2_failures\\**": "allow" + }, + "write": { + "*": "deny", + "C:\\projects\\manual_slop_tier2\\**": "allow", + "C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2\\**": "allow", + "C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2_failures\\**": "allow" + }, + "bash": { + "*": "deny", + "git status*": "allow", + "git diff*": "allow", + "git log*": "allow", + "git add*": "allow", + "git commit*": "allow", + "git switch*": "allow", + "git branch*": "allow", + "git fetch*": "allow", + "git remote*": "allow", + "git rev-parse*": "allow", + "git show*": "allow", + "git config --get*": "allow", + "ls*": "allow", + "cat*": "allow", + "head*": "allow", + "tail*": "allow", + "find*": "allow", + "echo*": "allow", + "mkdir*": "allow", + "cp*": "allow", + "mv*": "allow", + "rm*": "allow", + "uv run python scripts/run_tests_batched.py*": "allow", + "uv run python scripts/tier2/*": "allow", + "pwsh -File scripts/tier2/*": "allow", + "git push*": "deny", + "git checkout*": "deny", + "git restore*": "deny", + "git reset*": "deny" + } + }, "agent": { "tier2-autonomous": { "model": "minimax-coding-plan/MiniMax-M3", diff --git a/scripts/tier2/setup_tier2_clone.ps1 b/scripts/tier2/setup_tier2_clone.ps1 index e874451f..7b0c38b9 100644 --- a/scripts/tier2/setup_tier2_clone.ps1 +++ b/scripts/tier2/setup_tier2_clone.ps1 @@ -61,13 +61,24 @@ if ($PSCmdlet.ShouldProcess("Bootstrap Tier 2 clone at $Tier2ClonePath")) { Copy-Item -Force "$MainRepoPath\conductor\tier2\agents\tier2-autonomous.md" "$Tier2ClonePath\.opencode\agents\tier2-autonomous.md" Copy-Item -Force "$MainRepoPath\conductor\tier2\commands\tier-2-auto-execute.md" "$Tier2ClonePath\.opencode\commands\tier-2-auto-execute.md" - # Merge opencode.json.fragment into the clone's opencode.json + # Merge opencode.json.fragment into the clone's opencode.json. + # The clone inherits a copy of the main repo's opencode.json (via + # `git clone`), which has top-level `permission.edit: ask` and + # `permission.bash: ask`. Those would be unsafe in the sandbox: the + # build/plan default agents could read/write anywhere on disk, and + # there is no file-system allowlist at the top level. We replace + # the top-level `permission` with the hardened sandbox version + # (deny-all + allowlist for the sandbox dirs + the tier2-autonomous + # agent's permission block). The agent's `permission` overrides the + # top-level for that agent's tool calls. $cloneConfig = "$Tier2ClonePath\opencode.json" $fragment = Get-Content "$MainRepoPath\conductor\tier2\opencode.json.fragment" -Raw | ConvertFrom-Json if (Test-Path $cloneConfig) { $existing = Get-Content $cloneConfig -Raw | ConvertFrom-Json if (-not $existing.agent) { $existing | Add-Member -MemberType NoteProperty -Name agent -Value ([PSCustomObject]@{}) } $existing.agent | Add-Member -MemberType NoteProperty -Name "tier2-autonomous" -Value $fragment.agent."tier2-autonomous" -Force + if (-not $existing.permission) { $existing | Add-Member -MemberType NoteProperty -Name permission -Value ([PSCustomObject]@{}) } + $existing.permission = $fragment.permission $existing | Add-Member -MemberType NoteProperty -Name default_agent -Value "tier2-autonomous" -Force $existing | ConvertTo-Json -Depth 10 | Set-Content $cloneConfig } else { diff --git a/tests/test_tier2_slash_command_spec.py b/tests/test_tier2_slash_command_spec.py index 31c4ff52..cddbcdb8 100644 --- a/tests/test_tier2_slash_command_spec.py +++ b/tests/test_tier2_slash_command_spec.py @@ -42,9 +42,9 @@ def test_command_uses_git_switch_not_checkout() -> None: assert all("checkout" not in line for line in shell_lines), f"protocol uses git checkout: {shell_lines}" -def test_command_fetches_origin_main() -> None: +def test_command_fetches_origin_master() -> None: content = COMMAND_PATH.read_text(encoding="utf-8") - assert "git fetch origin main" in content + assert "git fetch origin master" in content def test_command_initializes_failcount_state() -> None: @@ -87,3 +87,26 @@ def test_config_fragment_valid_json() -> None: assert "git checkout*" in perms["bash"] assert "git restore*" in perms["bash"] assert "git reset*" in perms["bash"] + + +def test_config_fragment_has_top_level_permission() -> None: + """Top-level permission.read/write MUST allow the sandbox dirs (added + 2026-06-17 after the bug where the agent's permission.read was not + enforced for the default agent, leading to ACCESS DENIED on + manual_slop_tier2 paths).""" + data = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) + assert "permission" in data + top = data["permission"] + assert "read" in top, "top-level permission.read is required" + assert top["read"].get("*") == "deny", "top-level permission.read MUST deny *" + assert top["read"].get("C:\\projects\\manual_slop_tier2\\**") == "allow", "sandbox clone path must be allowlisted" + assert "write" in top + assert top["write"].get("*") == "deny" + assert top["write"].get("C:\\projects\\manual_slop_tier2\\**") == "allow" + assert "bash" in top + assert top["bash"].get("*") == "deny", "top-level bash MUST deny * (default agents are locked down)" + assert top["bash"].get("git status*") == "allow", "read-only git commands must be in the allowlist" + assert top["bash"].get("git push*") == "deny" + assert top["bash"].get("git checkout*") == "deny" + assert top["bash"].get("git restore*") == "deny" + assert top["bash"].get("git reset*") == "deny"