From 274ae2eb86a2ea29fd320b943ce71d20ea21918c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 14 Oct 2026 15:43:19 +0900 Subject: [PATCH 1/2] fix(opencode): bound coverage CPU and private artifact destination Cap the coverage sandbox to the selected Docker daemon capacity. Refuse private-source coverage transport when the receiving artifact repository is public or unknown. Preserve the local quality and formal review gates. Independent composition source review: b25a471ca7d97d5a34ff5df7be0e3291ed96846619651d676774ae304858ee47 Local full gate: 6518 passed, 4 skipped, scripts/ci statement and branch coverage 100%. Remote Actions, inference, formal App approval: not executed. --- .../workflows/opencode-review-dispatch.yml | 35 ++++++++++- .../opencode-coverage-daemon-cpu-capacity.md | 20 ++++++ .../opencode-private-artifact-destination.md | 20 ++++++ tests/test_opencode_agent_contract.py | 52 +++++++++++++++ .../test_opencode_public_source_transport.py | 63 +++++++++++++++++++ ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- 6 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 docs/doctoring/opencode-coverage-daemon-cpu-capacity.md create mode 100644 docs/doctoring/opencode-private-artifact-destination.md diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index ebeb485d69..96bb5cd86e 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -258,6 +258,25 @@ jobs: } >>"$GITHUB_OUTPUT" printf 'Validated current live metadata for %s#%s: base=%s/%s head=%s/%s.\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$live_base_ref" "$live_base_sha" "$live_head_ref" "$live_head_sha" + - name: Refuse private source on a public artifact destination + env: + SOURCE_REPOSITORY_PRIVATE: ${{ steps.validate.outputs.is_private }} + ARTIFACT_REPOSITORY_PRIVATE: ${{ github.event.repository.private }} + run: | + set -euo pipefail + # Artifact readers follow the receiving repository's ACL, not the + # private target's ACL. A one-day expiry is not access control. + case "$SOURCE_REPOSITORY_PRIVATE" in + false) ;; + true) + if [ "$ARTIFACT_REPOSITORY_PRIVATE" != true ]; then + echo '::error::Private coverage source requires a private artifact destination; public or unknown destinations are refused before source transport.' + exit 1 + fi + ;; + *) echo '::error::Coverage source visibility is unknown.'; exit 1 ;; + esac + - name: Exchange OpenCode app token for target repository coverage reads id: coverage_read_app_token if: >- @@ -1079,12 +1098,26 @@ jobs: fi sudo rm -rf "$coverage_build_dir" + # Use the daemon's capacity, not the runner host's CPU count. + # Keep four CPUs as an upper limit without rejecting smaller guests. + if ! coverage_daemon_cpus="$(docker info --format '{{.NCPU}}')"; then + echo '::error::Coverage Docker daemon CPU capacity is unavailable.' + exit 1 + fi + if [[ ! "$coverage_daemon_cpus" =~ ^[1-9][0-9]*$ ]]; then + echo '::error::Coverage Docker daemon CPU capacity is invalid.' + exit 1 + fi + case "$coverage_daemon_cpus" in + 1|2|3) coverage_sandbox_cpus="$coverage_daemon_cpus" ;; + *) coverage_sandbox_cpus=4 ;; + esac sandbox_status=0 docker run --rm --init --network=none \ --name "opencode-coverage-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ --pids-limit 2048 \ --memory 14g \ - --cpus 4 \ + --cpus "$coverage_sandbox_cpus" \ --security-opt no-new-privileges:true \ --cap-drop ALL \ --cap-add CHOWN \ diff --git a/docs/doctoring/opencode-coverage-daemon-cpu-capacity.md b/docs/doctoring/opencode-coverage-daemon-cpu-capacity.md new file mode 100644 index 0000000000..2eda65469f --- /dev/null +++ b/docs/doctoring/opencode-coverage-daemon-cpu-capacity.md @@ -0,0 +1,20 @@ +# OpenCode coverage sandbox의 Docker CPU 용량 결속 + +## 실제 실패 + +공개 target `.github#2617@b3983b467c2d7f1440b5024acb6a3860eb3e043d`의 중앙 run `38443671498`, coverage job `115456508271`은 `orgmetra-ci-01` / group 13에서 실행됐다. 공개 exact source 확보와 tool image build는 성공했다. 이후 고정 `--cpus 4`가 CPU 2개인 Docker daemon에서 거부됐다. sandbox exit 125로 출력 파일이 생성되지 않았고 테스트·coverage 측정·모델 추론은 실행되지 않았다. diagnostic `COMMENTED` 리뷰는 정식 승인으로 인정하지 않는다. + +## 좁은 수리와 검증 + +Docker daemon의 기존 `docker info --format '{{.NCPU}}'` 결과를 양의 정수로 검증한다. sandbox CPU limit는 `min(4, NCPU)`로 설정한다. 조회 실패·빈 값·0·부호·소수·줄 삽입은 실행 전 거부한다. host의 `nproc`를 daemon 용량으로 대체하지 않는다. + +메모리 14g, PID limit, network none, capability set, mount, 출력 인증, cleanup 및 100% coverage 기준은 변경하지 않는다. CPU 수리를 메모리 적합성이나 실제 runner 실행의 성공으로 주장하지 않는다. 원래 실행은 고정 workflow source를 유지하므로 수리 소스를 소비하는 fresh 지원 경로가 별도로 필요하다. 새 dispatch나 reviewer identity 우회는 이 소스 수리에 포함하지 않는다. + +실제 YAML의 launch shell을 추출하고 synthetic Docker CLI만 치환하여 argv를 검증한다. 첫 harness는 동일 cleanup 문자열의 첫 위치를 잘못 선택해 실패했다. 위치를 바로잡은 후 원래 `4 != 2` assertion 실패를 관측했고, 최소 수리 후 CPU/거부 입력 15개가 통과했다. 이는 실제 Docker daemon이나 Actions 실행의 검증은 아니다. + +## 공식 문서 + +- Docker. (2026). *docker system info*. https://docs.docker.com/reference/cli/docker/system/info/ +- Docker. (2026). *Resource constraints*. https://docs.docker.com/engine/containers/resource_constraints/ + +부모의 실제 문서 조회는 첫 구현 및 15개 GREEN 이후인 2026-10-14에 수행했다. 이를 구현 전 조회로 소급하지 않는다. `--cpus`는 container CPU 사용의 상한이며 테스트 수나 정확성 기준을 바꾸는 옵션이 아니다. diff --git a/docs/doctoring/opencode-private-artifact-destination.md b/docs/doctoring/opencode-private-artifact-destination.md new file mode 100644 index 0000000000..087bce20c9 --- /dev/null +++ b/docs/doctoring/opencode-private-artifact-destination.md @@ -0,0 +1,20 @@ +# OpenCode private source의 artifact 수신 경계 + +## 요구와 근거 + +private PR source archive를 공개 review 저장소의 Actions artifact로 발행하지 않는다. GitHub 공식 안내는 workflow artifact의 다운로드 권한을 해당 저장소의 read access에 결속한다. private 원본 저장소의 ACL이나 `retention-days: 1`은 공개 수신 저장소의 다운로드 권한을 제한하지 않는다. + +- GitHub. (2026). *Downloading workflow artifacts*. https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts (확인: 2026-10-14). +- 근거 소스: `acb8b34f9e7b8649fa1478139fa21c2217bdfbc7`의 `.github/workflows/opencode-review-dispatch.yml`. + +## 좁은 수리 + +검증된 `steps.validate.outputs.is_private`와 기존 `github.event.repository.private`를 비교한다. private source의 artifact 수신 저장소가 정확히 private가 아니면 coverage 전용 token exchange, source fetch, archive, upload 전에 nonzero로 거부한다. source visibility가 미확인이어도 거부한다. 새 허가 Boolean이나 정책 variable은 추가하지 않는다. + +public source의 credential-free, artifact-free 경로는 유지한다. private 수신 저장소의 기존 artifact 경로와 1일 보존값도 유지한다. 이 수리는 공개 중앙 저장소로의 private archive 전달을 금지하는 경계이며, 암호화 전송 구현이나 private 실행 활성화가 아니다. 실제 private 데이터 전송이나 과거 유출이 있었다고 주장하지 않는다. + +## 검증과 남은 조건 + +실제 YAML의 guard 위치·환경 binding을 검사하고 inline Bash를 합성 visibility 입력으로 실행한다. 정상 public 및 private destination을 별도 양성 대조로 유지한다. 원본 artifact-free helper의 실제 로컬 Git 회귀도 함께 실행한다. `tests/test_opencode_public_source_transport.py`는 기존 quality CI의 실행 목록에 포함되어 있다. + +모델 비용·retention admission, 실제 source transport, 독립 App의 exact-head 리뷰, 원격 Actions 및 병합은 별도 gate다. private target의 나머지 경로를 이 guard만으로 모두 안전하다고 선언하지 않는다. guard를 우회하거나 정책 attestation을 조작해 실행하지 않는다. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 234b23f914..4c46340904 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -3,10 +3,12 @@ import re import shutil import subprocess +import sys import textwrap from pathlib import Path import pytest +import yaml from scripts.ci.assert_opencode_reasoning_effort import strip_jsonc_comments @@ -16,6 +18,56 @@ def load_opencode_jsonc() -> dict: return json.loads(strip_jsonc_comments(Path("opencode.jsonc").read_text(encoding="utf-8"))) +@pytest.mark.parametrize("daemon_cpus, expected", [ + ("1", "1"), ("2", "2"), ("3", "3"), ("4", "4"), ("8", "4"), + ("999999999999999999999999999999999", "4"), + ("", None), ("0", None), ("-1", None), ("2.0", None), ("02", None), + ("true", None), ("2\n3", None), ("2\r", None), ("unavailable", None), +]) +def test_coverage_cpu_limit_fits_daemon_capacity(tmp_path, daemon_cpus, expected): + """실제 sandbox argv는 daemon 용량과 기존 상한 4를 넘지 않는다.""" + workflow = yaml.safe_load(Path(".github/workflows/opencode-review-dispatch.yml").read_text()) + steps = workflow["jobs"]["coverage-evidence"]["steps"] + script = next(step["run"] for step in steps if step.get("name") == "Measure test and docstring evidence") + start = script.rindex('sudo rm -rf "$coverage_build_dir"\n') + len('sudo rm -rf "$coverage_build_dir"\n') + end = script.index('sandbox_output="${sandbox_result_dir}/github-output"', start) + binary = tmp_path / "bin" + binary.mkdir() + (binary / "python3").symlink_to(sys.executable) + docker = binary / "docker" + docker.write_text( + f"#!{sys.executable}\nimport os,sys,json\nfrom pathlib import Path\n" + "if sys.argv[1]=='info':\n" + " assert sys.argv[2:]==['--format','{{.NCPU}}']\n" + " if os.environ['DAEMON_CPUS']=='unavailable':raise SystemExit(12)\n" + " print(os.environ['DAEMON_CPUS']);raise SystemExit(0)\n" + "Path(os.environ['ARGV_CAPTURE']).write_text(json.dumps(sys.argv[1:]))\n" + ) + docker.chmod(0o755) + capture = tmp_path / "argv.json" + result = subprocess.run( + ["/bin/bash", "-c", "set -euo pipefail\n" + script[start:end]], + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), "DAEMON_CPUS": daemon_cpus, + "ARGV_CAPTURE": str(capture), "GITHUB_RUN_ID": "1", "GITHUB_RUN_ATTEMPT": "1", + "GITHUB_WORKSPACE": str(tmp_path / "trusted"), "COVERAGE_SOURCE_WORKDIR": str(tmp_path / "source"), + "sandbox_result_dir": str(tmp_path / "out"), "measure_step_script": str(tmp_path / "measure.sh"), + "PR_BASE_SHA": "a" * 40, "PR_HEAD_SHA": "b" * 40, + "coverage_llvm_cov": "/llvm-cov", "coverage_llvm_profdata": "/llvm-profdata", + "coverage_tool_image": "synthetic:image"}, capture_output=True, text=True, check=False, timeout=15, + ) + if expected is None: + assert result.returncode == 1 + assert not capture.exists() + assert "CPU capacity is" in result.stdout + return + assert result.returncode == 0, result.stderr + argv = json.loads(capture.read_text()) + assert argv[argv.index("--cpus") + 1] == expected + assert argv[argv.index("--memory") + 1] == "14g" + assert "--network=none" in argv + assert "no-new-privileges:true" in argv + + def test_code_reviewer_subagent_contract_is_configured(): """Guard the read-only code-reviewer subagent contract.""" config = load_opencode_jsonc() diff --git a/tests/test_opencode_public_source_transport.py b/tests/test_opencode_public_source_transport.py index f5f4b40087..a4e63a5115 100644 --- a/tests/test_opencode_public_source_transport.py +++ b/tests/test_opencode_public_source_transport.py @@ -2,6 +2,7 @@ from pathlib import Path import os import subprocess +import sys import pytest import yaml @@ -29,6 +30,68 @@ def test_public_transfer_does_not_require_an_artifact_upload() -> None: assert download["with"]["name"] == upload["with"]["name"] +def test_private_source_refuses_public_artifact_destination_before_transport() -> None: + """비공개 PR 원문이 공개 리뷰 저장소의 artifact 저장소에 전달되지 않음을 검증한다.""" + jobs = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"] + steps = jobs["validate-pr-metadata"]["steps"] + names = [step.get("name") for step in steps] + guard = _step(jobs["validate-pr-metadata"], "Refuse private source on a public artifact destination") + assert names.index(guard["name"]) < names.index("Exchange OpenCode app token for target repository coverage reads") + assert names.index(guard["name"]) < names.index("Materialize pull request merge tree for coverage measurement") + assert names.index(guard["name"]) < names.index("Upload materialized pull request merge tree") + assert "if" not in guard + assert guard.get("continue-on-error", False) is False + assert guard["env"]["SOURCE_REPOSITORY_PRIVATE"] == "${{ steps.validate.outputs.is_private }}" + assert guard["env"]["ARTIFACT_REPOSITORY_PRIVATE"] == "${{ github.event.repository.private }}" + for source, destination, allowed in ( + ("true", "false", False), ("true", "", False), ("true", "unknown", False), + ("true", "true", True), ("false", "false", True), ("false", "true", True), + ("true", "True", False), ("true", "true\n", False), + ("", "true", False), ("unknown", "true", False), ("True", "true", False), + ): + result = subprocess.run( + ["/bin/bash", "-c", guard["run"]], capture_output=True, text=True, + check=False, env={"PATH": "/usr/bin:/bin", "HOME": "/dev/null", + "SOURCE_REPOSITORY_PRIVATE": source, + "ARTIFACT_REPOSITORY_PRIVATE": destination}, + ) + assert (result.returncode == 0) is allowed, (source, destination, result.stderr) + + +@pytest.mark.parametrize("destination, reaches_git", [("false", False), ("", False), ("true", True)]) +def test_private_destination_guard_stops_actual_materialization(destination, reaches_git, tmp_path): + """실제 guard와 materialize shell을 연결해 Git 진입 전 거부를 검증한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + guard = _step(metadata, "Refuse private source on a public artifact destination")["run"] + materialize = _step(metadata, "Materialize pull request merge tree for coverage measurement")["run"] + binary = tmp_path / "bin" + binary.mkdir() + marker = tmp_path / "git-called" + git = binary / "git" + git.write_text( + f"#!{sys.executable}\nimport os\nfrom pathlib import Path\n" + "Path(os.environ['GIT_MARKER']).write_text('synthetic git boundary')\n" + "raise SystemExit(37)\n" + ) + git.chmod(0o755) + runner = tmp_path / "runner" + runner.mkdir() + result = subprocess.run( + ["/bin/bash", "-c", guard + "\n" + materialize], + capture_output=True, text=True, check=False, timeout=15, + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), + "SOURCE_REPOSITORY_PRIVATE": "true", "ARTIFACT_REPOSITORY_PRIVATE": destination, + "RUNNER_TEMP": str(runner), "COVERAGE_SOURCE_WORKDIR": str(runner / "source"), + "COVERAGE_SOURCE_ARCHIVE": str(runner / "source.tar"), "GH_TOKEN": "synthetic-only", + "TARGET_REPOSITORY": "Fixture/private", "PR_NUMBER": "1", + "PR_BASE_SHA": "a" * 40, "PR_HEAD_SHA": "b" * 40, + "GITHUB_SERVER_URL": "https://fixture.invalid", "GIT_MARKER": str(marker)}, + ) + assert marker.exists() is reaches_git + assert result.returncode == (37 if reaches_git else 1) + assert not list(runner.iterdir()) + + def test_public_source_is_reconstructed_in_the_credential_free_coverage_job() -> None: """공개 경로도 검증한 base/head와 격리된 기존 destination에 결속한다.""" jobs = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"] diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 625ddaa21b..7b5f3a5b8b 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,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 = "ebeb485d690cfa7e6c368befb21ce73f74cb2a4c" +REVIEW_DISPATCH_BLOB_SHA = "96bb5cd86e4ca49d7e9d2987f1382f9a524f72d9" def _workflow_text(path: Path) -> str: From 82e4f3c8011e1b074d13a370c60990d0c0f5c1bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 14 Oct 2026 16:29:34 +0900 Subject: [PATCH 2/2] fix(opencode): recheck receiving visibility before private upload Resolve PR2652 stale event visibility finding with current repository identity and typed private metadata before transport and immediately before upload. Public source makes no extra API request. Independent SOURCE_ONLY review d55844bec622fa008977dcf23660bbae9acaae24f876931421fab15bc41ee585. Full gate 6533 passed, 4 skipped, 92 subtests; scripts/ci statements and branches 100%. Runtime and native App approval remain separate. --- .../workflows/opencode-review-dispatch.yml | 56 ++++++- .../opencode-private-artifact-destination.md | 4 +- .../test_opencode_public_source_transport.py | 140 ++++++++++++++++-- ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- 4 files changed, 183 insertions(+), 19 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 96bb5cd86e..cb3f10e0f6 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -261,16 +261,29 @@ jobs: - name: Refuse private source on a public artifact destination env: SOURCE_REPOSITORY_PRIVATE: ${{ steps.validate.outputs.is_private }} - ARTIFACT_REPOSITORY_PRIVATE: ${{ github.event.repository.private }} + ARTIFACT_REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - # Artifact readers follow the receiving repository's ACL, not the - # private target's ACL. A one-day expiry is not access control. + # Artifact readers follow the receiving repository's current ACL. + # Do not use the dispatch event's potentially stale visibility. case "$SOURCE_REPOSITORY_PRIVATE" in false) ;; true) - if [ "$ARTIFACT_REPOSITORY_PRIVATE" != true ]; then - echo '::error::Private coverage source requires a private artifact destination; public or unknown destinations are refused before source transport.' + if ! [[ "$ARTIFACT_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo '::error::Receiving artifact repository identity is invalid.' + exit 1 + fi + if ! artifact_metadata="$(gh api "repos/${ARTIFACT_REPOSITORY}" 2>/dev/null)"; then + echo '::error::Receiving artifact repository visibility could not be read.' + exit 1 + fi + if ! jq -se --arg repository "$ARTIFACT_REPOSITORY" ' + length == 1 and (.[0] | + type == "object" and .full_name == $repository and + (.private | type == "boolean") and .private == true) + ' >/dev/null 2>&1 <<<"$artifact_metadata"; then + echo '::error::Private coverage source requires a currently private artifact destination; public or unknown destinations are refused.' exit 1 fi ;; @@ -403,6 +416,39 @@ jobs: git -C "$COVERAGE_SOURCE_WORKDIR" status --short tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . + - name: Recheck private artifact destination before upload + if: steps.validate.outputs.is_private == 'true' + env: + SOURCE_REPOSITORY_PRIVATE: ${{ steps.validate.outputs.is_private }} + ARTIFACT_REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # Artifact readers follow the receiving repository's current ACL. + # Do not use the dispatch event's potentially stale visibility. + case "$SOURCE_REPOSITORY_PRIVATE" in + false) ;; + true) + if ! [[ "$ARTIFACT_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo '::error::Receiving artifact repository identity is invalid.' + exit 1 + fi + if ! artifact_metadata="$(gh api "repos/${ARTIFACT_REPOSITORY}" 2>/dev/null)"; then + echo '::error::Receiving artifact repository visibility could not be read.' + exit 1 + fi + if ! jq -se --arg repository "$ARTIFACT_REPOSITORY" ' + length == 1 and (.[0] | + type == "object" and .full_name == $repository and + (.private | type == "boolean") and .private == true) + ' >/dev/null 2>&1 <<<"$artifact_metadata"; then + echo '::error::Private coverage source requires a currently private artifact destination; public or unknown destinations are refused.' + exit 1 + fi + ;; + *) echo '::error::Coverage source visibility is unknown.'; exit 1 ;; + esac + - name: Upload materialized pull request merge tree if: steps.validate.outputs.is_private == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/docs/doctoring/opencode-private-artifact-destination.md b/docs/doctoring/opencode-private-artifact-destination.md index 087bce20c9..e3a64770ff 100644 --- a/docs/doctoring/opencode-private-artifact-destination.md +++ b/docs/doctoring/opencode-private-artifact-destination.md @@ -9,7 +9,7 @@ private PR source archive를 공개 review 저장소의 Actions artifact로 발 ## 좁은 수리 -검증된 `steps.validate.outputs.is_private`와 기존 `github.event.repository.private`를 비교한다. private source의 artifact 수신 저장소가 정확히 private가 아니면 coverage 전용 token exchange, source fetch, archive, upload 전에 nonzero로 거부한다. source visibility가 미확인이어도 거부한다. 새 허가 Boolean이나 정책 variable은 추가하지 않는다. +검증된 `steps.validate.outputs.is_private`와 현재 수신 저장소의 GitHub REST metadata를 비교한다. 이벤트 시점의 `github.event.repository.private`는 사용하지 않는다. private source는 token exchange·source fetch·archive 전에 수신 저장소를 조회하고, upload 직전에 같은 검사를 다시 실행한다. 응답의 `full_name`이 실제 수신 저장소와 같고 `private`이 Boolean true인 경우만 허용한다. API 실패·잘못된 JSON·여러 JSON 응답·누락·문자열 Boolean·public은 nonzero로 거부한다. public source는 이 추가 API를 호출하지 않는다. 새 허가 Boolean이나 정책 variable은 추가하지 않는다. public source의 credential-free, artifact-free 경로는 유지한다. private 수신 저장소의 기존 artifact 경로와 1일 보존값도 유지한다. 이 수리는 공개 중앙 저장소로의 private archive 전달을 금지하는 경계이며, 암호화 전송 구현이나 private 실행 활성화가 아니다. 실제 private 데이터 전송이나 과거 유출이 있었다고 주장하지 않는다. @@ -17,4 +17,6 @@ public source의 credential-free, artifact-free 경로는 유지한다. private 실제 YAML의 guard 위치·환경 binding을 검사하고 inline Bash를 합성 visibility 입력으로 실행한다. 정상 public 및 private destination을 별도 양성 대조로 유지한다. 원본 artifact-free helper의 실제 로컬 Git 회귀도 함께 실행한다. `tests/test_opencode_public_source_transport.py`는 기존 quality CI의 실행 목록에 포함되어 있다. +GitHub REST `GET /repos/{owner}/{repo}`의 현재 metadata를 사용한다. 전송 전과 upload 직전 조회는 atomic ACL 보장이 아니며, 마지막 조회 후 또는 artifact 보존 중 저장소 공개 전환을 방지하지 않는다. private artifact 경로 활성화에는 별도의 수신 ACL·보존 정책이 필요하다. 현재 공개 중앙 저장소에서는 private source 전달이 거부된다. + 모델 비용·retention admission, 실제 source transport, 독립 App의 exact-head 리뷰, 원격 Actions 및 병합은 별도 gate다. private target의 나머지 경로를 이 guard만으로 모두 안전하다고 선언하지 않는다. guard를 우회하거나 정책 attestation을 조작해 실행하지 않는다. diff --git a/tests/test_opencode_public_source_transport.py b/tests/test_opencode_public_source_transport.py index a4e63a5115..55cdf76bb8 100644 --- a/tests/test_opencode_public_source_transport.py +++ b/tests/test_opencode_public_source_transport.py @@ -1,5 +1,6 @@ """공개 exact-head 전달은 quota에 의존하지 않고 private 경로는 보존한다.""" from pathlib import Path +import json import os import subprocess import sys @@ -30,7 +31,7 @@ def test_public_transfer_does_not_require_an_artifact_upload() -> None: assert download["with"]["name"] == upload["with"]["name"] -def test_private_source_refuses_public_artifact_destination_before_transport() -> None: +def test_private_source_refuses_public_artifact_destination_before_transport(tmp_path) -> None: """비공개 PR 원문이 공개 리뷰 저장소의 artifact 저장소에 전달되지 않음을 검증한다.""" jobs = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"] steps = jobs["validate-pr-metadata"]["steps"] @@ -42,19 +43,17 @@ def test_private_source_refuses_public_artifact_destination_before_transport() - assert "if" not in guard assert guard.get("continue-on-error", False) is False assert guard["env"]["SOURCE_REPOSITORY_PRIVATE"] == "${{ steps.validate.outputs.is_private }}" - assert guard["env"]["ARTIFACT_REPOSITORY_PRIVATE"] == "${{ github.event.repository.private }}" - for source, destination, allowed in ( + assert guard["env"]["ARTIFACT_REPOSITORY"] == "${{ github.repository }}" + assert guard["env"]["GH_TOKEN"] == "${{ github.token }}" + assert "ARTIFACT_REPOSITORY_PRIVATE" not in guard["env"] + for index, (source, destination, allowed) in enumerate(( ("true", "false", False), ("true", "", False), ("true", "unknown", False), ("true", "true", True), ("false", "false", True), ("false", "true", True), - ("true", "True", False), ("true", "true\n", False), + ("true", '"true"', False), ("true", "null", False), ("", "true", False), ("unknown", "true", False), ("True", "true", False), - ): - result = subprocess.run( - ["/bin/bash", "-c", guard["run"]], capture_output=True, text=True, - check=False, env={"PATH": "/usr/bin:/bin", "HOME": "/dev/null", - "SOURCE_REPOSITORY_PRIVATE": source, - "ARTIFACT_REPOSITORY_PRIVATE": destination}, - ) + )): + response = '{"full_name":"Fixture/review","private":' + (destination or "null") + '}' + result, _ = _run_receiving_guard(guard, tmp_path / str(index), source, response) assert (result.returncode == 0) is allowed, (source, destination, result.stderr) @@ -74,13 +73,20 @@ def test_private_destination_guard_stops_actual_materialization(destination, rea "raise SystemExit(37)\n" ) git.chmod(0o755) + gh = binary / "gh" + gh.write_text( + f"#!{sys.executable}\nimport os\n" + "print(os.environ['API_RESPONSE'])\n" + ) + gh.chmod(0o755) runner = tmp_path / "runner" runner.mkdir() result = subprocess.run( ["/bin/bash", "-c", guard + "\n" + materialize], capture_output=True, text=True, check=False, timeout=15, env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), - "SOURCE_REPOSITORY_PRIVATE": "true", "ARTIFACT_REPOSITORY_PRIVATE": destination, + "SOURCE_REPOSITORY_PRIVATE": "true", "ARTIFACT_REPOSITORY": "Fixture/review", + "API_RESPONSE": '{"full_name":"Fixture/review","private":' + (destination or "null") + '}', "RUNNER_TEMP": str(runner), "COVERAGE_SOURCE_WORKDIR": str(runner / "source"), "COVERAGE_SOURCE_ARCHIVE": str(runner / "source.tar"), "GH_TOKEN": "synthetic-only", "TARGET_REPOSITORY": "Fixture/private", "PR_NUMBER": "1", @@ -92,6 +98,116 @@ def test_private_destination_guard_stops_actual_materialization(destination, rea assert not list(runner.iterdir()) +def _run_receiving_guard(guard, tmp_path, source, response, status=0, event_private="true"): + """실제 guard를 합성 GitHub metadata 경계와 실행하고 API argv를 보존한다.""" + binary = tmp_path / "bin" + binary.mkdir(parents=True) + calls = tmp_path / "api-calls" + gh = binary / "gh" + gh.write_text( + f"#!{sys.executable}\nimport json, os, sys\nfrom pathlib import Path\n" + "Path(os.environ['API_CALLS']).write_text(json.dumps(sys.argv[1:]))\n" + "print(os.environ['API_RESPONSE'])\n" + "raise SystemExit(int(os.environ['API_STATUS']))\n" + ) + gh.chmod(0o755) + result = subprocess.run( + ["/bin/bash", "-c", guard["run"]], capture_output=True, text=True, + check=False, timeout=15, + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), + "SOURCE_REPOSITORY_PRIVATE": source, "ARTIFACT_REPOSITORY_PRIVATE": event_private, + "ARTIFACT_REPOSITORY": "Fixture/review", "GH_TOKEN": "synthetic-only", + "API_CALLS": str(calls), "API_RESPONSE": response, "API_STATUS": str(status)}, + ) + return result, json.loads(calls.read_text()) if calls.exists() else None + + +def test_live_receiving_visibility_rejects_stale_private_event(tmp_path): + """이벤트가 private였어도 현재 수신 저장소가 public이면 전송 전에 거부한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + guard = _step(metadata, "Refuse private source on a public artifact destination") + result, calls = _run_receiving_guard( + guard, tmp_path, "true", '{"full_name":"Fixture/review","private":false}' + ) + assert result.returncode != 0 + assert calls == ["api", "repos/Fixture/review"] + + +@pytest.mark.parametrize("source,response,status,allowed,query", [ + ("true", '{"full_name":"Fixture/review","private":true}', 0, True, True), + ("true", '{"full_name":"Fixture/review","private":false}', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":"true"}', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":null}', 0, False, True), + ("true", '{"full_name":"Fixture/review"}', 0, False, True), + ("true", '{"full_name":"Other/review","private":true}', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":true}', 1, False, True), + ("true", 'invalid-json', 0, False, True), + ("true", '', 0, False, True), + ("true", '{"full_name":"Fixture/review","private":false}\n' + '{"full_name":"Fixture/review","private":true}', 0, False, True), + ("false", 'invalid-json', 1, True, False), + ("unknown", '{"full_name":"Fixture/review","private":true}', 0, False, False), +]) +def test_receiving_guard_uses_typed_live_metadata(source, response, status, allowed, query, tmp_path): + """현재 API의 저장소 identity와 Boolean을 확인하며 public 경로는 조회하지 않는다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + guard = _step(metadata, "Refuse private source on a public artifact destination") + result, calls = _run_receiving_guard(guard, tmp_path, source, response, status) + assert (result.returncode == 0) is allowed + assert calls == (["api", "repos/Fixture/review"] if query else None) + + +def test_private_upload_rechecks_live_receiving_visibility(tmp_path): + """archive 생성 뒤 업로드 직전에 동일한 fail-closed 조회를 다시 실행한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + names = [step.get("name") for step in metadata["steps"]] + name = "Recheck private artifact destination before upload" + assert name in names + recheck = _step(metadata, name) + early = _step(metadata, "Refuse private source on a public artifact destination") + assert recheck["if"] == "steps.validate.outputs.is_private == 'true'" + assert recheck.get("continue-on-error", False) is False + assert names.index(name) + 1 == names.index("Upload materialized pull request merge tree") + assert recheck["env"] == early["env"] + assert recheck["run"] == early["run"] + result, calls = _run_receiving_guard( + recheck, tmp_path, "true", '{"full_name":"Fixture/review","private":false}' + ) + assert result.returncode != 0 + assert calls == ["api", "repos/Fixture/review"] + + +def test_private_to_public_transition_stops_upload_witness(tmp_path): + """전송 전 private 양성 뒤 public 전환을 재조회하여 upload witness 전에 거부한다.""" + metadata = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["validate-pr-metadata"] + early = _step(metadata, "Refuse private source on a public artifact destination") + late = _step(metadata, "Recheck private artifact destination before upload") + binary = tmp_path / "bin" + binary.mkdir() + calls = tmp_path / "calls" + upload = tmp_path / "upload-called" + gh = binary / "gh" + gh.write_text( + f"#!{sys.executable}\nimport json, os, sys\nfrom pathlib import Path\n" + "assert sys.argv[1:] == ['api', 'repos/Fixture/review']\n" + "calls = Path(os.environ['API_CALLS'])\n" + "n = int(calls.read_text()) if calls.exists() else 0\n" + "calls.write_text(str(n + 1))\n" + "print(json.dumps({'full_name': 'Fixture/review', 'private': n == 0}))\n" + ) + gh.chmod(0o755) + result = subprocess.run( + ["/bin/bash", "-c", early["run"] + "\n" + late["run"] + '\ntouch "$UPLOAD_MARKER"'], + capture_output=True, text=True, check=False, timeout=15, + env={"PATH": f"{binary}:/usr/bin:/bin", "HOME": str(tmp_path), + "SOURCE_REPOSITORY_PRIVATE": "true", "ARTIFACT_REPOSITORY": "Fixture/review", + "GH_TOKEN": "synthetic-only", "API_CALLS": str(calls), "UPLOAD_MARKER": str(upload)}, + ) + assert result.returncode == 1 + assert calls.read_text() == "2" + assert not upload.exists() + + def test_public_source_is_reconstructed_in_the_credential_free_coverage_job() -> None: """공개 경로도 검증한 base/head와 격리된 기존 destination에 결속한다.""" jobs = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"] diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 7b5f3a5b8b..da69740c25 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,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 = "96bb5cd86e4ca49d7e9d2987f1382f9a524f72d9" +REVIEW_DISPATCH_BLOB_SHA = "cb3f10e0f616e0ae443206a22d379173fe5056b1" def _workflow_text(path: Path) -> str: