diff --git a/.agents/skills/local-qa/scripts/validate-opencode.bats b/.agents/skills/local-qa/scripts/validate-opencode.bats index 0352940..3bc421e 100644 --- a/.agents/skills/local-qa/scripts/validate-opencode.bats +++ b/.agents/skills/local-qa/scripts/validate-opencode.bats @@ -1,7 +1,8 @@ #!/usr/bin/env bats # Validate .opencode/ agent frontmatter, review-pr command/skill references, # that opencode.jsonc parses, and that its external_directory permission -# allow-lists the resolver path review-pr.md actually sources. +# allow-lists the resolver path review-pr.md actually sources and the runtime +# review-state directory pattern. setup() { repo_root="$(git -C "${BATS_TEST_DIRNAME}" rev-parse --show-toplevel)" @@ -135,3 +136,68 @@ opencode_jsonc_json() { return 1 } } + +@test "OpenCode permits only the orchestrator's installed runtime review payloads" { + local config_home state_dir payload unauthorized_path + local positive_status positive_output positive_exists + local negative_status negative_output negative_exists + local model_config + + command -v opencode >/dev/null || { + echo "opencode is required for the runtime permission regression" + return 1 + } + + config_home="$(mktemp -d)" + state_dir="${config_home}/.config/opencode/review-state" + payload="${state_dir}/initial.json" + unauthorized_path="${config_home}/unauthorized.json" + model_config='{"model":"opencode/big-pickle"}' + mkdir -p "${config_home}/.config/opencode" + cp -r "${repo_root}/.opencode/." "${config_home}/.config/opencode/" + mkdir -p "${state_dir}" + + run env HOME="${config_home}" \ + XDG_CONFIG_HOME="${config_home}/.config" \ + OPENCODE_DISABLE_PROJECT_CONFIG=1 \ + OPENCODE_CONFIG_CONTENT="${model_config}" \ + opencode debug agent review-pr-orchestrator --tool write \ + --params "{filePath:'${payload}',content:'{\"body\":\"test\",\"comments\":[]}' }" + positive_status="${status}" + positive_output="${output}" + positive_exists=false + [ -f "${payload}" ] && positive_exists=true + + run env HOME="${config_home}" \ + XDG_CONFIG_HOME="${config_home}/.config" \ + OPENCODE_DISABLE_PROJECT_CONFIG=1 \ + OPENCODE_CONFIG_CONTENT="${model_config}" \ + opencode debug agent review-pr-orchestrator --tool write \ + --params "{filePath:'${unauthorized_path}',content:'denied'}" + negative_status="${status}" + negative_output="${output}" + negative_exists=false + [ -f "${unauthorized_path}" ] && negative_exists=true + rm -rf "${config_home}" + + [ "${positive_status}" -eq 0 ] || { + echo "OpenCode denied the orchestrator runtime payload write: ${positive_output}" + return 1 + } + [ "${positive_exists}" = true ] || { + echo "OpenCode reported success without creating the runtime payload: ${positive_output}" + return 1 + } + [[ "${positive_output}" == *'Wrote file successfully.'* ]] || { + echo "OpenCode did not report a successful runtime payload write: ${positive_output}" + return 1 + } + [ "${negative_status}" -ne 0 ] || { + echo "OpenCode allowed an unrelated external write: ${negative_output}" + return 1 + } + [ "${negative_exists}" = false ] || { + echo "OpenCode created an unrelated external file despite the deny boundary: ${negative_output}" + return 1 + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 327e3da..4bed082 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,9 @@ jobs: uses: dceoy/gh-actions-for-devops/.github/workflows/bats-test.yml@main # zizmor: ignore[unpinned-uses] with: search-path: . + setup-run: | + curl -fsSL https://opencode.ai/install | bash -s -- --version 1.2.14 + echo "${HOME}/.opencode/bin" >> "${GITHUB_PATH}" github-codeql-analysis: if: > github.event_name == 'push' diff --git a/.opencode/agents/review-pr-orchestrator.md b/.opencode/agents/review-pr-orchestrator.md index 8165b88..c531078 100644 --- a/.opencode/agents/review-pr-orchestrator.md +++ b/.opencode/agents/review-pr-orchestrator.md @@ -5,6 +5,12 @@ mode: primary color: info permission: "*": deny + external_directory: + "*": deny + "$HOME/.config/opencode/scripts/resolve-app-token.sh": allow + "$HOME/.config/opencode/scripts/review-pr-submit.sh": allow + "$HOME/.config/opencode/scripts/review-pr-gh.sh": allow + "$HOME/.config/opencode/review-state/*": allow read: "*": allow "*.env": deny @@ -14,6 +20,8 @@ permission: "*": deny "$HOME/.config/opencode/review-state/initial.json": allow "$HOME/.config/opencode/review-state/update.json": allow + "../*.config/opencode/review-state/initial.json": allow + "../*.config/opencode/review-state/update.json": allow glob: allow grep: allow bash: diff --git a/.opencode/commands/review-pr.md b/.opencode/commands/review-pr.md index 248a454..3d42c25 100644 --- a/.opencode/commands/review-pr.md +++ b/.opencode/commands/review-pr.md @@ -9,7 +9,7 @@ This is a strictly read-only repository review. Analyze and report only. Do not Do not run repository-wide QA scripts, formatters, auto-fixing linters, generators, dependency installers, or anything that can create caches, reports, snapshots, lockfiles, coverage output, scan output, or configuration exports in the checkout. -Every helper this command invokes — the read-only `gh` wrapper, the constrained submission helper, and the App-token resolver they source — lives only at its `${HOME}/.config/opencode/scripts/` path, installed there by the action before the reviewed repository is ever checked out. Never invoke any of them by a repository-relative path such as `.opencode/scripts/...`: the checkout under review is untrusted input, and a repository-relative path would let a malicious PR that edits or adds a same-named file substitute its own script for the trusted one. These helper paths and the three fixed review-state JSON files are the sole allow-listed external paths. The helpers use `opencode_app_token_lib="${HOME}/.config/opencode/scripts/resolve-app-token.sh"` for authentication. +Every helper this command invokes — the read-only `gh` wrapper, the constrained submission helper, and the App-token resolver they source — lives only at its `${HOME}/.config/opencode/scripts/` path, installed there by the action before the reviewed repository is ever checked out. Never invoke any of them by a repository-relative path such as `.opencode/scripts/...`: the checkout under review is untrusted input, and a repository-relative path would let a malicious PR that edits or adds a same-named file substitute its own script for the trusted one. These helper paths and the dedicated `${HOME}/.config/opencode/review-state/` directory are the sole allow-listed external locations. Despite the directory-level external access required by OpenCode, use the edit tool only for `initial.json` and `update.json` as instructed below. The helpers use `opencode_app_token_lib="${HOME}/.config/opencode/scripts/resolve-app-token.sh"` for authentication. **Requested review aspects (optional):** "$ARGUMENTS" diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 16f0da7..203c0a8 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -6,9 +6,7 @@ "$HOME/.config/opencode/scripts/resolve-app-token.sh": "allow", "$HOME/.config/opencode/scripts/review-pr-submit.sh": "allow", "$HOME/.config/opencode/scripts/review-pr-gh.sh": "allow", - "$HOME/.config/opencode/review-state/initial.json": "allow", - "$HOME/.config/opencode/review-state/update.json": "allow", - "$HOME/.config/opencode/review-state/context.json": "allow" + "$HOME/.config/opencode/review-state/*": "allow" } } } diff --git a/README.md b/README.md index 1d0e4a1..3933af5 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ The bundled `/review-pr` command submits a GitHub pull request review through `g A successful structured review run produces one GitHub review summary (with its inline comments), which `/review-pr` updates in place with the run link. `/review-pr` never calls `gh pr comment` or the issue comment API itself. It may still return a short final assistant message after submitting the review; `opencode github run` posts that as a separate top-level completion comment, so a run can produce the review plus at most one additional completion comment — not necessarily exactly one top-level artifact. -The bundled toolkit ships an `external_directory` permission in `.opencode/opencode.jsonc` (copied into `~/.config/opencode/` with the rest of the toolkit) that denies by default, so a stray attempt to read outside the checked-out repository, such as inspecting `/opt/pipx/logs/*` after a failed tool install, is denied immediately instead of blocking on the default "ask" prompt, which nothing can answer in a non-interactive GitHub Actions run and would otherwise hang until `timeout-minutes` kills it. The narrow allow-list contains only the three trusted helpers (`resolve-app-token.sh`, `review-pr-gh.sh`, and `review-pr-submit.sh`) under `~/.config/opencode/scripts/` and the fixed `context.json`, `initial.json`, and `update.json` files under `~/.config/opencode/review-state/` that those helpers use. +The bundled toolkit ships an `external_directory` permission in `.opencode/opencode.jsonc` (copied into `~/.config/opencode/` with the rest of the toolkit) that denies by default, so a stray attempt to read outside the checked-out repository, such as inspecting `/opt/pipx/logs/*` after a failed tool install, is denied immediately instead of blocking on the default "ask" prompt, which nothing can answer in a non-interactive GitHub Actions run and would otherwise hang until `timeout-minutes` kills it. The narrow allow-list contains only the three trusted helpers (`resolve-app-token.sh`, `review-pr-gh.sh`, and `review-pr-submit.sh`) under `~/.config/opencode/scripts/` and the dedicated `~/.config/opencode/review-state/` directory that those helpers populate with the fixed `context.json`, `initial.json`, and `update.json` files. OpenCode evaluates access to those state files through the directory wildcard, so the allow rule matches `~/.config/opencode/review-state/*` while the orchestrator's edit permission remains limited to the two payload files. ### Review author and the OpenCode App token