From 1685f71695bb226838143c88e51e8488c7edb2f1 Mon Sep 17 00:00:00 2001 From: dceoy <1938249+dceoy@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:37:52 +0900 Subject: [PATCH 1/7] fix inline review submission --- .../local-qa/scripts/validate-opencode.bats | 16 +++++++++++++++- .opencode/opencode.jsonc | 4 +--- README.md | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.agents/skills/local-qa/scripts/validate-opencode.bats b/.agents/skills/local-qa/scripts/validate-opencode.bats index 0352940..b535a40 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,16 @@ opencode_jsonc_json() { return 1 } } + +@test "opencode.jsonc allow-lists the runtime review-state directory pattern" { + local state_pattern action + + # shellcheck disable=SC2016 + state_pattern='$HOME/.config/opencode/review-state/*' + action="$(opencode_jsonc_json | jq -r --arg pattern "${state_pattern}" '.permission.external_directory[$pattern] // empty')" + + [ "${action}" = "allow" ] || { + echo "opencode.jsonc must allow the runtime review-state pattern ${state_pattern} (got: '${action}')" + return 1 + } +} 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 From 2c7ac2085c1a17fdac143bf806ab6c202a877de1 Mon Sep 17 00:00:00 2001 From: dceoy <1938249+dceoy@users.noreply.github.com> Date: Mon, 13 Jul 2026 03:49:35 +0900 Subject: [PATCH 2/7] address review permission feedback --- .../local-qa/scripts/validate-opencode.bats | 28 ++++++++++++++----- .opencode/commands/review-pr.md | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.agents/skills/local-qa/scripts/validate-opencode.bats b/.agents/skills/local-qa/scripts/validate-opencode.bats index b535a40..ac9a880 100644 --- a/.agents/skills/local-qa/scripts/validate-opencode.bats +++ b/.agents/skills/local-qa/scripts/validate-opencode.bats @@ -137,15 +137,29 @@ opencode_jsonc_json() { } } -@test "opencode.jsonc allow-lists the runtime review-state directory pattern" { - local state_pattern action +@test "OpenCode permits writing the installed runtime initial review payload" { + local config_home state_dir payload output - # shellcheck disable=SC2016 - state_pattern='$HOME/.config/opencode/review-state/*' - action="$(opencode_jsonc_json | jq -r --arg pattern "${state_pattern}" '.permission.external_directory[$pattern] // empty')" + command -v opencode >/dev/null || skip "opencode is required for the runtime permission regression" + + config_home="$(mktemp -d)" + state_dir="${config_home}/.config/opencode/review-state" + payload="${state_dir}/initial.json" + mkdir -p "${state_dir}" + cp "${opencode_jsonc}" "${config_home}/.config/opencode/opencode.jsonc" + + run env HOME="${config_home}" \ + OPENCODE_CONFIG="${config_home}/.config/opencode/opencode.jsonc" \ + opencode debug agent build --pure --tool write \ + --params "{filePath:'${payload}',content:'{\"body\":\"test\",\"comments\":[]}'}" + rm -rf "${config_home}" - [ "${action}" = "allow" ] || { - echo "opencode.jsonc must allow the runtime review-state pattern ${state_pattern} (got: '${action}')" + [ "${status}" -eq 0 ] || { + echo "OpenCode denied the runtime payload write: ${output}" + return 1 + } + [[ "${output}" == *'Wrote file successfully.'* ]] || { + echo "OpenCode did not report a successful runtime payload write: ${output}" return 1 } } 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" From 7782596781fb01865901b0a5ebf6452d94b70bfd Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:08:35 +0900 Subject: [PATCH 3/7] fix orchestrator external path permissions --- .opencode/agents/review-pr-orchestrator.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.opencode/agents/review-pr-orchestrator.md b/.opencode/agents/review-pr-orchestrator.md index 8165b88..34a26d0 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 From 33b0258abe015442ce5334b3cc1f1e148cf3a082 Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:09:13 +0900 Subject: [PATCH 4/7] test effective orchestrator permissions --- .../local-qa/scripts/validate-opencode.bats | 64 +++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/.agents/skills/local-qa/scripts/validate-opencode.bats b/.agents/skills/local-qa/scripts/validate-opencode.bats index ac9a880..07ad048 100644 --- a/.agents/skills/local-qa/scripts/validate-opencode.bats +++ b/.agents/skills/local-qa/scripts/validate-opencode.bats @@ -137,29 +137,69 @@ opencode_jsonc_json() { } } -@test "OpenCode permits writing the installed runtime initial review payload" { - local config_home state_dir payload output - - command -v opencode >/dev/null || skip "opencode is required for the runtime permission regression" +@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":"openai/gpt-4o-mini"}' + mkdir -p "${config_home}/.config/opencode" + cp -r "${repo_root}/.opencode/." "${config_home}/.config/opencode/" mkdir -p "${state_dir}" - cp "${opencode_jsonc}" "${config_home}/.config/opencode/opencode.jsonc" run env HOME="${config_home}" \ - OPENCODE_CONFIG="${config_home}/.config/opencode/opencode.jsonc" \ - opencode debug agent build --pure --tool write \ - --params "{filePath:'${payload}',content:'{\"body\":\"test\",\"comments\":[]}'}" + XDG_CONFIG_HOME="${config_home}/.config" \ + OPENCODE_DISABLE_PROJECT_CONFIG=1 \ + OPENCODE_CONFIG_CONTENT="${model_config}" \ + OPENAI_API_KEY=test \ + 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}" \ + OPENAI_API_KEY=test \ + 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}" - [ "${status}" -eq 0 ] || { - echo "OpenCode denied the runtime payload write: ${output}" + [ "${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 } - [[ "${output}" == *'Wrote file successfully.'* ]] || { - echo "OpenCode did not report a successful runtime payload write: ${output}" + [ "${negative_exists}" = false ] || { + echo "OpenCode created an unrelated external file despite the deny boundary: ${negative_output}" return 1 } } From fb4d03b13e94d4470594ba0e9ce3e88df771a186 Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:09:29 +0900 Subject: [PATCH 5/7] run OpenCode permission regression in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) 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' From 7057af0b17b39c596bc6d1c2810e2c59503dbeca Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:12:09 +0900 Subject: [PATCH 6/7] use a keyless model for permission regression --- .agents/skills/local-qa/scripts/validate-opencode.bats | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.agents/skills/local-qa/scripts/validate-opencode.bats b/.agents/skills/local-qa/scripts/validate-opencode.bats index 07ad048..3bc421e 100644 --- a/.agents/skills/local-qa/scripts/validate-opencode.bats +++ b/.agents/skills/local-qa/scripts/validate-opencode.bats @@ -152,7 +152,7 @@ opencode_jsonc_json() { state_dir="${config_home}/.config/opencode/review-state" payload="${state_dir}/initial.json" unauthorized_path="${config_home}/unauthorized.json" - model_config='{"model":"openai/gpt-4o-mini"}' + 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}" @@ -161,7 +161,6 @@ opencode_jsonc_json() { XDG_CONFIG_HOME="${config_home}/.config" \ OPENCODE_DISABLE_PROJECT_CONFIG=1 \ OPENCODE_CONFIG_CONTENT="${model_config}" \ - OPENAI_API_KEY=test \ opencode debug agent review-pr-orchestrator --tool write \ --params "{filePath:'${payload}',content:'{\"body\":\"test\",\"comments\":[]}' }" positive_status="${status}" @@ -173,7 +172,6 @@ opencode_jsonc_json() { XDG_CONFIG_HOME="${config_home}/.config" \ OPENCODE_DISABLE_PROJECT_CONFIG=1 \ OPENCODE_CONFIG_CONTENT="${model_config}" \ - OPENAI_API_KEY=test \ opencode debug agent review-pr-orchestrator --tool write \ --params "{filePath:'${unauthorized_path}',content:'denied'}" negative_status="${status}" From b30b675cc85f2153c98131b48a4adaf0d4fdba85 Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:15:23 +0900 Subject: [PATCH 7/7] allow worktree-relative review payload edits --- .opencode/agents/review-pr-orchestrator.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.opencode/agents/review-pr-orchestrator.md b/.opencode/agents/review-pr-orchestrator.md index 34a26d0..c531078 100644 --- a/.opencode/agents/review-pr-orchestrator.md +++ b/.opencode/agents/review-pr-orchestrator.md @@ -20,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: