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
81 changes: 80 additions & 1 deletion .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,38 @@ 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: ${{ 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: Exchange OpenCode app token for target repository coverage reads
id: coverage_read_app_token
if: >-
Expand Down Expand Up @@ -384,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
Expand Down Expand Up @@ -1079,12 +1144,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 \
Expand Down
20 changes: 20 additions & 0 deletions docs/doctoring/opencode-coverage-daemon-cpu-capacity.md
Original file line number Diff line number Diff line change
@@ -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 사용의 상한이며 테스트 수나 정확성 기준을 바꾸는 옵션이 아니다.
22 changes: 22 additions & 0 deletions docs/doctoring/opencode-private-artifact-destination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 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 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 데이터 전송이나 과거 유출이 있었다고 주장하지 않는다.

## 검증과 남은 조건

실제 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을 조작해 실행하지 않는다.
52 changes: 52 additions & 0 deletions tests/test_opencode_agent_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down
Loading
Loading