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
8 changes: 4 additions & 4 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ jobs:

if [ "$live_state" != "open" ] ||
[ "$live_base_repository" != "$TARGET_REPOSITORY" ] ||
[ "$live_head_repository" != "$TARGET_REPOSITORY" ] ||
! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
Comment thread
seonghobae marked this conversation as resolved.
! [[ "$live_base_sha" =~ ^[0-9a-fA-F]{40}$ ]] ||
! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]] ||
! [[ "$live_is_private" =~ ^(true|false)$ ]] ||
[ -z "$live_base_ref" ] ||
[ -z "$live_head_ref" ]; then
printf '::error::PR metadata validation rejected closed, missing, cross-repository, or malformed live metadata. target=%s#%s state=%s base_repo=%s head_repo=%s base=%s head=%s\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${live_state:-<missing>}" "${live_base_repository:-<missing>}" "${live_head_repository:-<missing>}" "${live_base_sha:-<missing>}" "${live_head_sha:-<missing>}"
printf '::error::PR metadata validation rejected closed, missing, or malformed live metadata. target=%s#%s state=%s base_repo=%s head_repo=%s base=%s head=%s\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${live_state:-<missing>}" "${live_base_repository:-<missing>}" "${live_head_repository:-<missing>}" "${live_base_sha:-<missing>}" "${live_head_sha:-<missing>}"
exit 1
fi

Expand Down Expand Up @@ -2165,7 +2165,7 @@ jobs:
live_is_private="$(jq -r '.base.repo.private | tostring' <<<"$pull_request_json")"
if [ "$live_state" != "open" ] ||
[ "$base_repository" != "$GH_REPOSITORY" ] ||
[ "$head_repository" != "$GH_REPOSITORY" ] ||
! [[ "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
[ "$live_base_ref" != "$EXPECTED_BASE_REF" ] ||
[ "$live_base_sha" != "$EXPECTED_BASE_SHA" ] ||
[ "$live_head_ref" != "$EXPECTED_HEAD_REF" ] ||
Expand All @@ -2177,7 +2177,7 @@ jobs:
"$GH_REPOSITORY" "$PR_NUMBER" "${live_state:-<missing>}" "${base_repository:-<missing>}" "${live_base_ref:-<missing>}" "${live_base_sha:-<missing>}" "$EXPECTED_BASE_REF" "$EXPECTED_BASE_SHA" "${head_repository:-<missing>}" "${live_head_ref:-<missing>}" "${live_head_sha:-<missing>}" "$EXPECTED_HEAD_REF" "$EXPECTED_HEAD_SHA" "${live_is_private:-<missing>}" "${EXPECTED_IS_PRIVATE:-<missing>}"
exit 1
fi
printf 'Validated same-repository OpenCode review source for %s#%s (%s).\n' \
printf 'Validated exact-head OpenCode review source for %s#%s (%s).\n' \
"$GH_REPOSITORY" "$PR_NUMBER" "$head_repository"

- name: Exchange OpenCode app token for target repository review reads
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,10 @@ jobs:
if [ "$live_number" != "$TARGET_PR_NUMBER" ] ||
[ "$live_state" != "open" ] ||
[ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ] ||
[ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ] ||
! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
[ -z "$live_base_branch" ] ||
! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
printf '::error::Targeted scheduler dispatch rejected closed, cross-repository, or malformed live PR metadata. target=%s pr=%s state=%s base_repository=%s head_repository=%s base_branch=%s head_sha=%s\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "${live_state:-<missing>}" "${live_base_repository:-<missing>}" "${live_head_repository:-<missing>}" "${live_base_branch:-<missing>}" "${live_head_sha:-<missing>}"
printf '::error::Targeted scheduler dispatch rejected closed or malformed live PR metadata. target=%s pr=%s state=%s base_repository=%s head_repository=%s base_branch=%s head_sha=%s\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "${live_state:-<missing>}" "${live_base_repository:-<missing>}" "${live_head_repository:-<missing>}" "${live_base_branch:-<missing>}" "${live_head_sha:-<missing>}"
exit 1
fi
if [ -n "$TARGET_BASE_BRANCH_INPUT" ] &&
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Semantic Versioning where the repository publishes a release.
### Fixed

- Forward `trigger_reviews=true` explicitly from the trusted OpenCode mention wrapper to the authoritative scheduler while retaining GitHub's ten-key dispatch limit. Source-comment identity remains bound in the verified invocation claim and durable ledger instead of occupying an unused scheduler field, so a successfully routed `@opencode-agent` request now dispatches review work rather than entering queue maintenance with reviews disabled.
- Allowed an allowlisted base repository's open fork-head PR to enter the central exact-head OpenCode review path. The scheduler and privileged reviewer still re-read the live PR, bind base/head refs and SHAs, reject malformed repository identities, keep fork source as untrusted data, preserve the existing maintainer-writable update rule, and reserve the final external-head merge for a maintainer.
- Confined OSV base and head repository checkouts to the same `source/` child directory, so a cross-fork head checkout can replace that repository without deleting the base-scan JSON held at the workspace root. Both scans retain identical source paths and the required base/head vulnerability comparison remains fail-closed.
- Restored 100% docstring coverage for the commercial-readiness GitHub transport constructor.
- Refused PR Review Merge Scheduler head mutations, `update-branch` and the last-push approval head restamp, whenever the resolved mutation credential is the workflow `GITHUB_TOKEN`. GitHub starts no workflow run for events created with that credential, so the moved head collected no current-head required checks and the PR stayed permanently `BLOCKED` with a `github-actions[bot]` merge commit that no later scheduler run could repair, because the branch was no longer behind. The scheduler now waits with `head_mutation_credential_upgrade` guidance naming `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, and the OpenCode app token exchange.
Expand Down
56 changes: 56 additions & 0 deletions docs/doctoring/fork-head-review-dispatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Fork-head OpenCode review dispatch

## Decision

An allowlisted ContextualWisdomLab base repository may dispatch an open pull
request whose head repository is a canonical `owner/repository` fork. The fork
is review data, never a trusted workflow source. The central
scheduler and OpenCode workflow continue to re-read the live pull request,
bind the base and head refs and commit SHAs, run protected default-branch
workflow code, update an external head only under the existing
maintainer-writable rule, and exclude external heads from automated merge.

## Root cause

Production scheduler run `32549777222` received an exact request for
`ContextualWisdomLab/contextual-orchestrator#820`, but rejected the open PR
before review because its head repository was a fork. This contradicted the
existing scheduler policy, which already classifies external heads as
reviewable while reserving their merge and update decisions for a maintainer.

## Trust boundary

The smallest repair removes only the false base-equals-head requirement. The
allowlisted base repository must still match the live PR, the head repository
must be a canonical GitHub repository name, and both validation passes must
observe the same exact base/head refs and SHAs. Source is fetched through the
base repository's authenticated PR boundary, materialized at the validated
commits, and handled by the existing credential-scrubbed review sandbox. No
fork workflow is loaded and no new write permission, provider credential, or
mutation or merge authority is granted. This follows GitHub's requirement to treat fork
content as untrusted and the SSDF practice of addressing a root cause without
weakening the surrounding security controls.

## Verification

The executable scheduler regression accepts a canonical fork head while
preserving the exact base branch and head SHA outputs, then rejects a malformed
three-component head repository. Static workflow contracts keep the live base
repository match, exact-head revalidation before OIDC/model work, canonical
head-repository validation, and the absence of the former same-repository
guard.

## APA 7th references

GitHub, Inc. (n.d.-a). *REST API endpoints for pull requests*. GitHub Docs.
Retrieved August 22, 2026, from
https://docs.github.com/en/rest/pulls/pulls

GitHub, Inc. (n.d.-b). *Secure use reference*. GitHub Docs. Retrieved August
22, 2026, from
https://docs.github.com/en/actions/reference/security/secure-use

Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development
Framework (SSDF) version 1.1: Recommendations for mitigating the risk of
software vulnerabilities* (NIST Special Publication 800-218). National
Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218
11 changes: 7 additions & 4 deletions docs/org-required-workflow-rollout.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ContextualWisdomLab central required workflow rollout

Updated: 2026-07-23 06:35 KST
Updated: 2026-08-22 KST

## Decision

Expand Down Expand Up @@ -39,7 +39,7 @@ The central `.github/workflows/opencode-review.yml` is now part of the active or
- Stable branch-protection job names: `required-workflow-bootstrap`, `coverage-source-tree`, `coverage-evidence`, and `opencode-review`; these jobs are data-only sentinels, while approval remains a separate current-head PR-review requirement
- Trusted source: `ContextualWisdomLab/.github`
- PR-head handling: authenticated current-head `repository_dispatch` runs `.github/workflows/opencode-review-dispatch.yml` from the protected default branch; that workflow owns metadata validation, bounded coverage, source-as-data inspection, model review, and publication
- Manual target support: the central scheduler sends exact repository, PR, base, and head metadata through `repository_dispatch`; the dispatch workflow rejects an unauthorized actor, an unallowlisted repository, a fork head, or any live metadata mismatch
- Manual target support: the central scheduler sends exact repository, PR, base, and head metadata through `repository_dispatch`; the dispatch workflow rejects an unauthorized actor, an unallowlisted base repository, a malformed head-repository identity, or any live base/head metadata mismatch. A canonical fork head remains reviewable as untrusted source data.
- Model token posture: use the organization `STRIX_GITHUB_MODELS_TOKEN` secret for GitHub Models calls, with `github.token` as the fallback; live workflow evidence showed `github.token` alone can return 403 from `models.github.ai/inference`
- Write posture: OpenCode may create review/comment side effects through the OpenCode app token when available; the workflow token is limited to the same-repository PR context and publication failures remain visible
- Coverage execution posture: PR-controlled package, test, build, R, Rust, and Docker inputs are never executed from `pull_request_target`; the dispatch workflow runs bounded low-privilege coverage only after exact live metadata and scheduler identity validation
Expand All @@ -52,8 +52,11 @@ For a bounded current-head retry in one repository, dispatch `merge-scheduler`
to the central repository with `target_repository`, `pr_number`, and the live
`base_branch`. The target must exactly match
`OPENCODE_REPOSITORY_DISPATCH_TARGETS`; the scheduler then re-reads the open PR
and rejects a noncanonical repository name, fork head, base mismatch, malformed
head SHA, or changed/closed PR before using cross-repository credentials:
and rejects a noncanonical base or head repository name, base mismatch,
malformed head SHA, or changed/closed PR before using cross-repository
credentials. A fork head may pass this read/review boundary, but the scheduler
still excludes it from direct merge, auto-merge, and non-maintainer branch
mutation:

```bash
jq -n '{
Expand Down
15 changes: 15 additions & 0 deletions tests/test_opencode_agent_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2193,6 +2193,16 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed():
assert workflow.count("ref: ${{ steps.trusted_source.outputs.ref }}") == 1
assert "TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.ref }}" in workflow
assert "ref: ${{ github.workflow_sha }}" not in workflow
metadata_step = workflow.split(
" - name: Bind workflow inputs to live organization pull request metadata",
1,
)[1].split("\n - name:", 1)[0]
assert (
'! [[ "$live_head_repository" =~ '
'^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]'
) in metadata_step
assert '[ "$live_head_repository" != "$TARGET_REPOSITORY" ]' not in metadata_step
assert '[ "$SUPPLIED_HEAD_SHA" = "$live_head_sha" ]' in metadata_step
trust_step = target_job.split(
" - name: Validate pull request head repository trust", 1
)[1].split("\n - name:", 1)[0]
Expand All @@ -2201,6 +2211,11 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed():
assert "metadata changed before OIDC" in trust_step
assert 'live_head_sha="$(jq -r' in trust_step
assert '[ "$live_head_sha" != "$EXPECTED_HEAD_SHA" ]' in trust_step
assert (
'! [[ "$head_repository" =~ '
'^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]'
) in trust_step
assert '[ "$head_repository" != "$GH_REPOSITORY" ]' not in trust_step
assert (
"EXPECTED_IS_PRIVATE: "
"${{ needs.validate-pr-metadata.outputs.is_private }}"
Expand Down
35 changes: 32 additions & 3 deletions tests/test_opencode_workflow_shell_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def test_merge_scheduler_targeted_dispatch_run_block_is_valid_bash():


def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path):
"""Only an allowlisted same-repository open PR reaches scheduler outputs."""
"""Allowlisted open PRs keep exact outputs even when their head is a fork."""
if sys.platform == "win32":
return
bash = shutil.which("bash")
Expand Down Expand Up @@ -260,6 +260,35 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path):
env=cross_repo_env,
)

assert cross_repo.returncode == 1
assert "cross-repository" in cross_repo.stdout
assert cross_repo.returncode == 0, cross_repo.stderr
assert output.read_text(encoding="utf-8").splitlines() == [
"repository=ContextualWisdomLab/naruon",
"base_branch=develop",
"head_sha=4afd4af7ad343660356791873d940aa2846f40c2",
]

output.unlink()
malformed_head_env = {
**env,
"FAKE_PULL_JSON": json.dumps(
{
**cross_repo_pull,
"head": {
**cross_repo_pull["head"],
"repo": {"full_name": "outside/fork/extra"},
},
}
),
}
malformed_head = subprocess.run(
[bash],
input=script,
text=True,
capture_output=True,
check=False,
env=malformed_head_env,
)

assert malformed_head.returncode == 1
assert "malformed live PR metadata" in malformed_head.stdout
assert not output.exists()
6 changes: 3 additions & 3 deletions tests/test_pr_review_autofix_nvidia_nim_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md")
CHANGELOG = Path("CHANGELOG.md")
REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml")
REVIEW_DISPATCH_BLOB_SHA = "de1c4800d362bda4a90c31a0c8e39687a782afb4"
REVIEW_DISPATCH_BLOB_SHA = "545a2ad7e667220de263805d2c560752a4448347"


def _workflow_text(path: Path) -> str:
Expand Down Expand Up @@ -156,8 +156,8 @@ def test_missing_nvidia_nim_secret_fails_closed_before_model_execution() -> None
assert guard in workflow[conflict_start:]


def test_independent_review_agent_key_system_is_unchanged() -> None:
"""Pin the existing read-only reviewer workflow byte-for-byte."""
def test_independent_review_agent_workflow_matches_reviewed_blob() -> None:
"""Pin the reviewed read-only reviewer workflow byte-for-byte."""
result = subprocess.run(
["git", "hash-object", str(REVIEW_DISPATCH_WORKFLOW)],
check=True,
Expand Down
8 changes: 6 additions & 2 deletions tests/test_required_workflow_queue_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_merge_scheduler_provides_same_repository_dispatch_credential() -> None:


def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> None:
"""Central single-PR dispatch must validate live metadata before cross-repo use."""
"""Central single-PR dispatch accepts a bounded fork head without trusting it."""
workflow = workflow_text("pr-review-merge-scheduler.yml")
validation = workflow_step(workflow, "Validate targeted repository dispatch")
inspect = workflow_step(workflow, "Inspect PR review and merge queue")
Expand All @@ -111,7 +111,11 @@ def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> Non
assert '"repos/${TARGET_REPOSITORY_INPUT}/pulls/${TARGET_PR_NUMBER}"' in validation
assert '[ "$live_state" != "open" ]' in validation
assert '[ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation
assert '[ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation
assert (
'! [[ "$live_head_repository" =~ '
'^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]'
) in validation
assert '[ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ]' not in validation
assert "Targeted scheduler dispatch base branch does not match the live PR" in validation
assert "TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }}" in inspect
assert (
Expand Down
Loading