From 9996a21600b3751491842de20ca4c87e1cc257a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 10 Oct 2026 00:12:37 +0900 Subject: [PATCH 1/9] =?UTF-8?q?ci:=20K-CSAP=20=EC=A4=91=EC=95=99=20self-ho?= =?UTF-8?q?sted=20=ED=92=88=EC=A7=88=20callee=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #2612. 로컬 계약 및 독립 소스 검토는 활성화 제한부 통과. 실제 Actions 실행 및 AI App 승인은 미검증. --- .github/workflows/k-csap-skills-quality.yml | 138 ++++++++++++ GOAL.md | 77 +++++++ .../k-csap-skills-central-quality.md | 44 ++++ requirements-k-csap-quality-tests-hashes.txt | 89 ++++++++ requirements-k-csap-quality-tests.in | 3 + ...k_csap_skills_quality_workflow_contract.py | 210 ++++++++++++++++++ 6 files changed, 561 insertions(+) create mode 100644 .github/workflows/k-csap-skills-quality.yml create mode 100644 GOAL.md create mode 100644 docs/doctoring/k-csap-skills-central-quality.md create mode 100644 requirements-k-csap-quality-tests-hashes.txt create mode 100644 requirements-k-csap-quality-tests.in create mode 100644 tests/test_k_csap_skills_quality_workflow_contract.py diff --git a/.github/workflows/k-csap-skills-quality.yml b/.github/workflows/k-csap-skills-quality.yml new file mode 100644 index 0000000000..647a5169dd --- /dev/null +++ b/.github/workflows/k-csap-skills-quality.yml @@ -0,0 +1,138 @@ +name: K-CSAP Skills Quality + +on: + workflow_call: + +permissions: + contents: read + +jobs: + quality: + name: k-csap-skills-quality + # This job gate is evaluated before runner assignment. A skipped job is not a quality verdict. + if: >- + github.repository == 'ContextualWisdomLab/k-csap-skills' && + ((github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.state == 'open') || + ((github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + github.ref == 'refs/heads/master')) + runs-on: [self-hosted, Linux, X64, k-csap-isolated] + timeout-minutes: 20 + steps: + - name: Admit only trusted K-CSAP source + id: admission + env: + TARGET_REPOSITORY: ${{ github.repository }} + EVENT_NAME: ${{ github.event_name }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || '' }} + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + EVENT_REF: ${{ github.ref }} + PR_STATE: ${{ github.event.pull_request.state || 'open' }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + set -euo pipefail + if [ "$TARGET_REPOSITORY" != "ContextualWisdomLab/k-csap-skills" ]; then + echo "::error::K-CSAP quality rejected an unexpected caller repository." + exit 1 + fi + case "$EVENT_NAME" in + push|workflow_dispatch) + if [ "$EVENT_REF" != 'refs/heads/master' ]; then + echo "::error::K-CSAP quality only accepts master branch pushes or dispatches." + exit 1 + fi + ;; + pull_request) + if [ "$HEAD_REPOSITORY" != "$TARGET_REPOSITORY" ] || [ "$PR_STATE" != "open" ]; then + echo "::error::K-CSAP quality accepts only open same-repository pull requests." + exit 1 + fi + ;; + *) + echo "::error::K-CSAP quality rejected an unsupported event." + exit 1 + ;; + esac + if ! [[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::K-CSAP quality rejected a malformed source SHA." + exit 1 + fi + echo "admitted=true" >>"$GITHUB_OUTPUT" + + - name: Prepare run-owned source directory + id: prepare + env: + RUN_ID: ${{ github.run_id }} + RUN_ATTEMPT: ${{ github.run_attempt }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + set -euo pipefail + if ! [[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]] || ! [[ "$RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Invalid run identity." + exit 1 + fi + if [ -L "${GITHUB_WORKSPACE:?}" ] || [ ! -d "$GITHUB_WORKSPACE" ]; then + echo "::error::Workspace root is not a regular directory." + exit 1 + fi + source_path="k-csap-quality.${RUN_ID}.${RUN_ATTEMPT}" + if [ -e "$GITHUB_WORKSPACE/$source_path" ] || [ -L "$GITHUB_WORKSPACE/$source_path" ]; then + echo "::error::Source path already exists; refusing stale source." + exit 1 + fi + mkdir -- "$GITHUB_WORKSPACE/$source_path" + echo "source_path=$source_path" >>"$GITHUB_OUTPUT" + + # PR exact head is checked; merge-candidate integration is a separate gate. + - name: Checkout exact caller source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + path: ${{ steps.prepare.outputs.source_path }} + persist-credentials: false + clean: true + + - name: Install pinned uv action + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: '0.9.7' + enable-cache: false + + - name: Verify K-CSAP quality in a job-private environment + working-directory: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }} + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + set -euo pipefail + job_root="$(mktemp -d "${RUNNER_TEMP:?}/k-csap-quality.XXXXXX")" + cleanup() { rm -rf -- "$job_root"; } + trap cleanup EXIT + unset PYTHONHOME PYTHONPATH + export UV_CACHE_DIR="$job_root/uv-cache" + export UV_PYTHON_INSTALL_DIR="$job_root/python" + export UV_PROJECT_ENVIRONMENT="$job_root/venv" + export UV_NO_CONFIG=1 + test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + uv python install 3.11.14 + uv sync --locked --dev --python 3.11.14 + uv run --python 3.11.14 ruff check . + uv run --python 3.11.14 pytest -q + uv run --python 3.11.14 python scripts/validate.py + git diff --check + + - name: Clean run-owned source after any step result + if: always() + env: + SOURCE_PATH: ${{ steps.prepare.outputs.source_path }} + RUN_ID: ${{ github.run_id }} + RUN_ATTEMPT: ${{ github.run_attempt }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + set -euo pipefail + if [ -z "$SOURCE_PATH" ]; then exit 0; fi + if ! [[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]] || ! [[ "$RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]]; then exit 1; fi + if [ "$SOURCE_PATH" != "k-csap-quality.${RUN_ID}.${RUN_ATTEMPT}" ]; then exit 1; fi + if [ -L "${GITHUB_WORKSPACE:?}" ] || [ ! -d "$GITHUB_WORKSPACE" ]; then exit 1; fi + rm -rf -- "${GITHUB_WORKSPACE:?}/$SOURCE_PATH" diff --git a/GOAL.md b/GOAL.md new file mode 100644 index 0000000000..b237464a6d --- /dev/null +++ b/GOAL.md @@ -0,0 +1,77 @@ +# Goal — K-CSAP 중앙 self-hosted 품질 실행 + +갱신: 2026-10-09 KST. 사용자 직접 지시를 따른다. + +## 결과 + +`ContextualWisdomLab/k-csap-skills`의 일반 품질 검증을 조직 `ContextualWisdomLab/.github`의 중앙 reusable workflow로 이동한다. GitHub-hosted fallback을 두지 않는다. 현재 제품 저장소의 `ubuntu-latest` workflow는 중앙 workflow의 immutable commit을 호출하는 얇은 caller로 교체한다. + +이 Goal은 Noema/OpenCode 공유 모델·App reviewer나 LiteLLM key를 구현하지 않는다. 그 파일과 key custody는 기존 #2560, PR #2565 및 Remora worktree 소유다. 이 increment는 중앙 품질 실행의 disjoint additive callee와 소비자 caller 계약만 소유한다. + +## 검증된 현재 상태 + +- 중앙 protected base: `7554587c2e3106a388998bcad048a3d7121de25e`. +- 제품 protected master: `0c66aec00f9f501cbbd530d8f74141d633928dc2`. +- 제품 `.github/workflows/ci.yml`은 `ubuntu-latest`를 직접 사용한다. +- 제품 repo-scoped runner `k-csap-isolated-linux` is online. Labels allow routing; its current group/ACL, running service and ID do not by themselves prove caller-context reusable-workflow admission or worktree isolation. Live check is #2560 / salmon ownership; keep group ACL changes with its operator. +- Group 13 is not the route for this candidate and all three members are offline; do not substitute the privileged control group. +- 제품 저장소는 비공개다. 2026-10-09 23:53 KST GraphQL `isPrivate=true`를 직접 확인했다. 앞선 공개 저장소 서술은 철회한다. fork 여부와 무관하게 same-repository open PR, protected master push/manual만 허용한다. +- OpenCode target allowlist와 auto LiteLLM route도 K-CSAP에 활성화되지 않았다. 품질 workflow 성공과 AI review/App approval은 별도다. +- 중앙 `LLM_GATEWAY_API_KEY` Secret metadata와 `LLM_GATEWAY_MODEL=auto`는 확인했다. 값은 읽지 않았고 새 키를 발급하거나 덮어쓰지 않는다. Secret/variable 존재는 실제 inference 또는 허용된 route의 증거가 아니다. + +## 소유 파일 + +- `.github/workflows/k-csap-skills-quality.yml` +- `tests/test_k_csap_skills_quality_workflow_contract.py` +- `requirements-k-csap-quality-tests.in` +- `requirements-k-csap-quality-tests-hashes.txt` +- `docs/doctoring/k-csap-skills-central-quality.md` +- 이 `GOAL.md` + +공유 Noema/OpenCode/runner migration workflow·relay·key·ACL은 변경하지 않는다. + +## 계약 + +- `on: workflow_call`만 사용. 실행 입력과 inherited secret 없음. +- job 이름 `k-csap-skills-quality`. +- 제품 repo-scoped labels `self-hosted`, `Linux`, `X64`, `k-csap-isolated`만 사용. hosted fallback 금지. +- first inline step가 checkout 전에 fixed caller repository, event, ref/head를 거부한다. k-csap repository의 same-repo pull_request/push/workflow_dispatch 호출만 admit한다. **첫 step는 runner 배정 이전의 방어책이 아니다.** caller job-level gate가 실행 이전 제한이고 skip은 검증 성공으로 간주하지 않는다. repo-scoped runner는 조직 group 13의 workflow allowlist에 속하지 않는다. +- PR은 exact head SHA를 검사한다. merge candidate 통합 검증은 별도 필요하며 head 통과로 대체하지 않는다. +- checkout은 중앙 source를 불변 workflow SHA에서 읽어 실행하지 않고, caller repository의 검증 대상 SHA를 credentials 비보존으로 checkout한다. +- 권한은 contents:read, timeout과 caller-owned concurrency를 명시. callee에는 동일 concurrency group을 두지 않는다. +- uv/Python 3.11 lock 기반으로 `ruff`, `pytest`, `scripts/validate.py`, `git diff --check`를 실행한다. source corpus·secret 없음. + +### 운영 수락표 + +| 환경 | 현재 runner에서 확인된 값 | 활성화 전 필요 조건 | +|---|---|---| +| source tree | uid 1001, cap-drop ALL, no-new-privileges, mounts 없음 | same-UID 격리는 아직 미증명. persistent shared checkout을 쓰지 않고 새 workspace 격리를 검증한다. | +| Python/uv | Python 3.12.3, uv 미설치 (salmon 보고) | locked uv/Python을 ephemeral runtime에 준비하고 버전과 package origin을 run receipt에 묶는다. | +| Group 13 | K-CSAP 접근 없음, 구성원 세 runner offline | 이 후보에서 사용하지 않는다. | +| repo runner id 2 | GitHub API online/idle | label, 별도 SSH gate, 또는 중앙 workflow 호출만으로 caller admission·network isolation이 증명되지 않는다. 실제 호출 job이 필요하다. | + +### 중앙 reusable 실행격리 + +| 영역 | 구현 계약 | 상태 | +|---|---|---| +| runner 접근 | product repo runner id 2만 허용. group 13/privileged control fallback 없음. ACL/group 변경은 owner 전용 | 실행 전 admission 확인 필요 | +| 동시 작업 | shared host/container를 사용하는 한 same-UID untrusted 동시 job이 없음을 증명하거나, 운영자가 보장하는 상호배제 lease 사용 | 미증명; host process scan은 원자적 lease 대체 아님 | +| source/runtime | stale checkout reuse 금지; run-id/attempt 전용 source path, private runtime/cache, exact SHA checkout | workflow `9c4cc914…` 로컬 lifecycle 통과; hostile 동시 writer/강제 종료 격리는 미검증 | +| cleanup | 실제 격리·exclusive lease의 운영 수락이 필요 | 소스 재리뷰 PASS_WITH_ACTIVATION_LIMITATIONS; 원격 activation 미수락 | +| network | 허용/거부 egress 경계를 실제 workflow runner에서 확인 | NOT_RUN | +| toolchain | uv 없음이 보고됨. pinned uv와 Python 3.11을 job 내부 설치, locked sync, 실제 버전/모듈 origin 기록 | 중앙 job 미실행 | +| admission | caller repository/event/ref/head, fork 거부, checkout 전 trusted caller gate | 실제 Actions 미검증 | + +## 수락 단계 + +1. 계약 테스트 RED→GREEN, YAML parse, actionlint. +2. 정확 파일 diff 독립 리뷰. +3. 중앙 PR merge 및 immutable commit 획득. +4. 제품 caller에서 중앙 commit pin과 기존 hosted workflow 제거. +5. 제품 repo-scoped `k-csap-isolated-linux`에서 exact-head job 실행. +6. 실행 ID, runner_name, step exits를 확인. 그 전에는 configured/proposed 상태다. +7. AI review는 #2560/Remora를 통해 literal auto actual inference와 author-distinct App review를 별도 확인한다. + +## Hosted-only retirement + +K-CSAP 일반 품질 검증은 hosted-only가 아니다. 중앙 self-hosted가 실제 활성화되면 제품의 hosted 실행 경로를 제거한다. group access/runner가 준비되지 않은 동안 old hosted workflow를 먼저 끄고 성공으로 표시하지 않는다. 새 중앙 gate가 실제 실행되지 않으면 unavailable evidence로 남는다. diff --git a/docs/doctoring/k-csap-skills-central-quality.md b/docs/doctoring/k-csap-skills-central-quality.md new file mode 100644 index 0000000000..80d1827b6c --- /dev/null +++ b/docs/doctoring/k-csap-skills-central-quality.md @@ -0,0 +1,44 @@ +# K-CSAP 중앙 품질 실행 상태 + +확인일: 2026-10-09 KST. 관련 issue: 중앙 #2612. 이 문서는 실행 성공 또는 출시 승인이 아니다. + +## 현재 단계 + +초안의 독립 리뷰가 FAIL이다. 원본 workflow SHA-256은 `1743d6c788606bdbd6caa2006a2de47a22029ed5696d2a9a705085542224fa45`, 테스트는 `29e2d825a666332c6bf8a3272359d72c743856606f5d940c3f6920869e354db6`이다. 리뷰 report SHA-256은 `a67ed03a6136c0bdc98b48201b68720489f10a68c10a7eaa8eaa5e72f5546fd9`이다. 초안의 9개 계약 테스트와 actionlint 통과는 이 FAIL을 대체하지 않는다. + +독립 재리뷰는 동일 workflow/test 해시에서 `PASS_WITH_ACTIVATION_LIMITATIONS`를 반환했다. 부모가 report SHA-256 `923b518cf61b072d3fb8ca7e03e5a1825faaca3c248942dd3048d2fbc2a2a378` 및 두 파일 해시를 직접 대조했다. 리뷰의 focused pytest 21개, custom-label actionlint exit 0, 별도 harmless cleanup probe 11/11은 로컬 계약 증거다. 초기 FAIL report는 역사 기록으로 보존한다. setup-uv 도구 상태는 private cleanup 범위 밖이며 외부 action 내부 cleanup, immutable download 무결성, 실제 원격 실행/App 승인은 미검증이다. + +현재 수리본은 job-level `if`를 runner 배정 전 gate로 추가하고, repo-scoped `k-csap-isolated` label을 사용한다. 기존 step admission은 checkout 전 방어층으로 유지했다. exact head, protected master ref, 일회용 source path, Python 3.11.14 및 uv/private venv/cache를 좁은 테스트로 검증했다. 초안 FAIL을 역사 기록으로 유지하며 수리본의 독립 재리뷰는 로컬 소스 한정 조건부 통과다. 수리 과정의 위임 writer 2회는 provider transport 실패로 결과 없이 종료했으며 부모가 테스트 RED를 확인하고 직접 수리했다. 수정본 로컬 `pytest` 21개 통과, scratch custom-label actionlint와 `git diff --check` exit 0; workflow SHA-256 `9c4cc914b57f919da06f434527670d34597ac4a8c1fc4c24008ce36440fdc5c9`, test SHA-256 `f053b9e139ddf3c2e566ce7966df1988af2a467d9627813404a2ee119da03f05`. 이는 실 runner에서의 실행/격리를 입증하지 않는다. + +## 리뷰 사실관계 구분 + +리뷰는 “checkout도 admission 전에 실행된다”고 기록했으나 frozen YAML은 첫 step admission, 다음 step checkout 순서다. 이 서술은 현재 파일로 지지되지 않는다. 반면 job runner 배정은 첫 step보다 앞서므로 **step admission만으로 배정 이전 신뢰 경계가 생기지 않는다는 지적은 유효하다.** 원본 FAIL을 삭제하거나 PASS로 덮어쓰지 않는다. 다음 리뷰에서 이 두 결론을 구분한다. + +PR exact head 검증은 기존 PR #30의 exact-head 증거 계약을 이어받은 의도다. GitHub merge candidate를 검증했다는 주장은 하지 않는다. push/manual 호출은 protected master로 제한하는 수리 계약이다. + +## 원격 activation + +직접 API 조회 결과: + +- 조직 group 13 `CWL CI isolated`는 `visibility=selected`, `restricted_to_workflows=false`이다. +- 허용 repository 목록에 K-CSAP이 없다. 소속 runner `orgmetra-ci-01`, `keyverse-ci-01`, `calendarweave-ci-01`은 모두 offline이다. +- 제품 repo-scoped `k-csap-isolated-linux`는 online/idle이다. 조직 group 13의 runner가 아니다. group 구성원이란 이유 없이 옮기거나 relabel하지 않는다. +- 중앙 global PR #2565의 head는 `ef65b2fdc2036ccaca8d19c1db88dc8075ae9164`, OPEN이다. 기존 owner source는 수정하지 않는다. + +중앙 reusable job은 caller에 제공된 repo-scoped runner도 사용할 수 있다는 소유권 교정을 반영했다. group 13 ACL 추가는 이 경로의 선행조건이 아니다. 현재 labels가 맞는 runner의 등록 상태만 확인했고 실제 reusable job 배정과 host isolation은 미검증이다. 동일 UID의 적대적 동시 writer, 잔류 프로세스, 강제 종료와 tool download 무결성은 activation gate에 남는다. 사용자 요구에 따라 hosted fallback은 추가하지 않는다. 실제 execution evidence가 없으면 unavailable로 남긴다. + +## AI 리뷰 및 키 경계 + +중앙 #2560 key/relay custody와 Remora shared review owner를 유지한다. `LLM_GATEWAY_API_KEY` Secret metadata updated `2026-10-09T05:07:41Z`와 `LLM_GATEWAY_MODEL=auto`를 확인했다. 비밀 값은 읽지 않았고 중복 발급/덮어쓰기는 하지 않았다. 사용자가 지정한 `https://litellm.poinnetworks.net`의 실제 authenticated auto inference, private-data route 적격성, author-distinct App의 exact-head APPROVED는 아직 이 lane에서 검증하지 않았다. + +## 재현 가능한 테스트 환경 + +새 전용 테스트 lock `requirements-k-csap-quality-tests-hashes.txt`는 `requirements-k-csap-quality-tests.in`에서 uv 0.12.5의 실제 compile로 생성했다. 공유 manifest/lock은 수정하지 않았다. private scratch CPython 3.11.16 환경에서 hash-required binary-only sync로 pytest 9.1.1, PyYAML 6.0.3 및 4개 전이 의존성을 설치하고 21개 계약 테스트를 실행해 통과했다. YAML 텍스트에서 기대값을 합성하는 대안 parser는 폐기했으며 실제 PyYAML로 구조를 읽는다. `subprocess.run(check=False)`를 명시하여 실패 exit 검증을 유지했다. 이 테스트 파일 delta는 별도 재검토 대상이다. runtime workflow의 Python pin은 3.11.14로 유지되어, 이 실행은 workflow 원격 runtime 실행 증거가 아니다. + +## 다음 gate + +1. 수리 writer의 실제 테스트 결과, 파일 해시, 수정 중단을 확인한다. +2. 최종 해시에서 독립 재리뷰를 수행한다. +3. 중앙 callee를 출판하고 immutable commit을 확보한다. 소비자 caller에 가짜 SHA나 mutable branch를 넣지 않는다. +4. caller·runner ACL·capacity를 승인된 경계에서 연결하고 run ID와 job.runner_name을 검증한다. +5. 실제 Noema/OpenCode auto inference 및 조건부 App 승인을 별도 확인한다. 단독 개발자에게 가상의 다른 인간 리뷰어를 요구하지 않는다. diff --git a/requirements-k-csap-quality-tests-hashes.txt b/requirements-k-csap-quality-tests-hashes.txt new file mode 100644 index 0000000000..d04a9d179c --- /dev/null +++ b/requirements-k-csap-quality-tests-hashes.txt @@ -0,0 +1,89 @@ +iniconfig==2.3.1 \ + --hash=sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960 \ + --hash=sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 diff --git a/requirements-k-csap-quality-tests.in b/requirements-k-csap-quality-tests.in new file mode 100644 index 0000000000..4cc8ae66e5 --- /dev/null +++ b/requirements-k-csap-quality-tests.in @@ -0,0 +1,3 @@ +# 중앙 K-CSAP workflow 계약 테스트 전용. 런타임 제품 의존성 아님. +pytest==9.1.1 +PyYAML==6.0.3 diff --git a/tests/test_k_csap_skills_quality_workflow_contract.py b/tests/test_k_csap_skills_quality_workflow_contract.py new file mode 100644 index 0000000000..605e84a2c8 --- /dev/null +++ b/tests/test_k_csap_skills_quality_workflow_contract.py @@ -0,0 +1,210 @@ +"""중앙 K-CSAP 품질 workflow 계약. 합성 메타데이터 테스트 전용.""" +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +CENTRAL = ROOT / ".github/workflows/k-csap-skills-quality.yml" + + +def workflow(): + assert CENTRAL.is_file() + return yaml.safe_load(CENTRAL.read_text(encoding="utf-8")) + + +def test_reusable_only_and_no_hosted_fallback(): + obj = workflow() + assert obj.get("on", obj.get(True)) == {"workflow_call": None} + assert obj["permissions"] == {"contents": "read"} + assert set(obj["jobs"]) == {"quality"} + job = obj["jobs"]["quality"] + assert job["name"] == "k-csap-skills-quality" + assert job["runs-on"] == ["self-hosted", "Linux", "X64", "k-csap-isolated"] + source = CENTRAL.read_text(encoding="utf-8") + assert "ubuntu-latest" not in source and "ubuntu-24.04" not in source + assert "secrets:" not in source and "continue-on-error" not in source + assert "concurrency:" not in source + assert job["timeout-minutes"] <= 20 + + +def test_runner_assignment_admits_only_trusted_caller_before_lease(): + job = workflow()["jobs"]["quality"] + gate = job["if"] + assert "github.repository == 'ContextualWisdomLab/k-csap-skills'" in gate + assert "github.event.pull_request.head.repo.full_name == github.repository" in gate + assert "github.event.pull_request.state == 'open'" in gate + assert "github.ref == 'refs/heads/master'" in gate + assert "github.event_name == 'workflow_dispatch'" in gate + assert "github.event_name == 'push'" in gate + + +def test_admission_before_checkout_and_no_credential_persistence(): + steps = workflow()["jobs"]["quality"]["steps"] + assert steps[0]["id"] == "admission" + text = steps[0]["run"] + assert 'ContextualWisdomLab/k-csap-skills' in text + assert 'pull_request' in text and 'push' in text and 'workflow_dispatch' in text + assert 'exit 1' in text + checkout = next(step for step in steps if step.get("uses", "").startswith("actions/checkout@")) + assert checkout["with"]["persist-credentials"] is False + assert checkout["with"]["path"].startswith("${{") + assert "ref" in checkout["with"] + assert steps[0]["env"]["TARGET_REPOSITORY"].startswith("${{") + assert steps[0]["env"]["HEAD_REPOSITORY"].startswith("${{") + + +def test_locked_quality_steps_and_scope_are_executable(): + steps = workflow()["jobs"]["quality"]["steps"] + source = "\n".join(step.get("run", "") for step in steps) + for required in ( + "uv python install 3.11.14", "uv sync --locked --dev --python 3.11.14", + "uv run --python 3.11.14 ruff check .", "uv run --python 3.11.14 pytest -q", + "uv run --python 3.11.14 python scripts/validate.py", "git diff --check", + ): + assert required in source + assert "set -euo pipefail" in source + assert "git rev-parse HEAD" in source + assert "RUNNER_TEMP" in source + assert "trap " in source + assert "UV_PYTHON_INSTALL_DIR" in source + assert "UV_CACHE_DIR" in source + + +def test_checkout_preparation_and_cleanup_are_owned(): + steps = workflow()["jobs"]["quality"]["steps"] + checkout = next(step for step in steps if step.get("uses", "").startswith("actions/checkout@")) + assert checkout["with"]["path"] == "${{ steps.prepare.outputs.source_path }}" + assert steps[0]["id"] == "admission" + assert steps[1]["id"] == "prepare" + cleanup = steps[-1] + assert cleanup["if"] == "always()" + assert "source_path" in cleanup["env"] or "SOURCE_PATH" in cleanup["env"] + assert "rm -rf" in cleanup["run"] + + +@pytest.mark.parametrize("failed_at", ["success", "install", "test"]) +def test_quality_shell_keeps_original_status_and_removes_private_runtime(tmp_path, failed_at): + import os + import subprocess + + steps = workflow()["jobs"]["quality"]["steps"] + verify = next(step for step in steps if step.get("name", "").startswith("Verify K-CSAP")) + script = verify["run"] + work = tmp_path / "source" + work.mkdir() + (work / ".git").mkdir() + sentinel = tmp_path / "untouched" + sentinel.write_text("keep") + bindir = tmp_path / "bin" + bindir.mkdir() + marker = tmp_path / "marker" + for name in ("uv", "git"): + tool = bindir / name + tool.write_text( + "#!/bin/sh\n" + "if [ \"$1\" = rev-parse ]; then printf '%s\\n' \"$EXPECTED_SHA\"; exit 0; fi\n" + "if [ \"$1\" = diff ]; then exit 0; fi\n" + "if [ \"$1\" = python ] && [ \"$2\" = install ]; then : >\"$MARKER\"; fi\n" + "if [ \"$FAIL_AT\" = install ] && [ \"$1\" = python ]; then exit 37; fi\n" + "if [ \"$FAIL_AT\" = test ] && [ \"$1\" = run ] && " + "[ \"$4\" = pytest ]; then exit 43; fi\n" + "exit 0\n" + ) + tool.chmod(0o755) + env = {**os.environ, "PATH": f"{bindir}:{os.environ['PATH']}", + "RUNNER_TEMP": str(tmp_path), "EXPECTED_SHA": "a" * 40, + "MARKER": str(marker), "FAIL_AT": failed_at} + proc = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", script], + cwd=work, env=env, capture_output=True, text=True, check=False) + assert proc.returncode == {"success": 0, "install": 37, "test": 43}[failed_at], proc.stderr + assert sentinel.read_text() == "keep" + assert list(tmp_path.glob("k-csap-quality.*")) == [] + assert marker.exists() + + +@pytest.mark.parametrize( + ("repository", "event", "head_repository", "expected_exit"), + [ + ("ContextualWisdomLab/k-csap-skills", "push", "", 0), + ("ContextualWisdomLab/k-csap-skills", "workflow_dispatch", "", 0), + ("ContextualWisdomLab/k-csap-skills", "pull_request", "ContextualWisdomLab/k-csap-skills", 0), + ("ContextualWisdomLab/k-csap-skills", "pull_request", "outside/fork", 1), + ("ContextualWisdomLab/other", "push", "", 1), + ("ContextualWisdomLab/k-csap-skills", "schedule", "", 1), + ], +) +def test_actual_admission_shell_rejects_untrusted_before_checkout( + repository, event, head_repository, expected_exit, tmp_path +): + import os + import subprocess + + script = workflow()["jobs"]["quality"]["steps"][0]["run"] + env = {**os.environ, "TARGET_REPOSITORY": repository, "EVENT_NAME": event, + "HEAD_REPOSITORY": head_repository, "EXPECTED_SHA": "a" * 40, + "PR_STATE": "open", "EVENT_REF": "refs/heads/master", + "GITHUB_OUTPUT": str(tmp_path / "output")} + proc = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", script], + env=env, capture_output=True, text=True, check=False) + assert proc.returncode == expected_exit, proc.stderr + assert "Traceback" not in proc.stderr + + +@pytest.mark.parametrize("stale", [False, True]) +def test_real_prepare_and_cleanup_preserve_stale_and_sibling_source(tmp_path, stale): + import os + import subprocess + + steps = workflow()["jobs"]["quality"]["steps"] + owned = tmp_path / "k-csap-quality.83.2" + sentinel = tmp_path / "other-job" + sentinel.write_text("unchanged") + if stale: + owned.mkdir() + (owned / "stale").write_text("untrusted") + output = tmp_path / "output" + env = {**os.environ, "RUN_ID": "83", "RUN_ATTEMPT": "2", + "GITHUB_WORKSPACE": str(tmp_path), "GITHUB_OUTPUT": str(output)} + prepared = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", steps[1]["run"]], + env=env, capture_output=True, text=True, check=False) + assert prepared.returncode == (1 if stale else 0), prepared.stderr + if stale: + assert (owned / "stale").read_text() == "untrusted" + source_path = "" + else: + assert output.read_text().strip() == "source_path=k-csap-quality.83.2" + assert owned.is_dir() + source_path = owned.name + cleared = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", steps[-1]["run"]], + env={**env, "SOURCE_PATH": source_path}, + capture_output=True, text=True, check=False) + assert cleared.returncode == 0, cleared.stderr + assert owned.exists() is stale + assert sentinel.read_text() == "unchanged" + + +@pytest.mark.parametrize( + ("event", "head", "ref", "state", "sha", "expected"), + [ + ("push", "", "refs/heads/master", "open", "a" * 40, 0), + ("push", "", "refs/heads/dev", "open", "a" * 40, 1), + ("workflow_dispatch", "", "refs/tags/v1", "open", "a" * 40, 1), + ("pull_request", "ContextualWisdomLab/k-csap-skills", "refs/pull/7/merge", "closed", "a" * 40, 1), + ("pull_request", "ContextualWisdomLab/k-csap-skills", "refs/pull/7/merge", "open", "INVALID", 1), + ], +) +def test_admission_rejects_non_master_closed_pr_and_invalid_sha( + event, head, ref, state, sha, expected, tmp_path +): + import os + import subprocess + + script = workflow()["jobs"]["quality"]["steps"][0]["run"] + env = {**os.environ, "TARGET_REPOSITORY": "ContextualWisdomLab/k-csap-skills", + "EVENT_NAME": event, "HEAD_REPOSITORY": head, "EVENT_REF": ref, + "PR_STATE": state, "EXPECTED_SHA": sha, + "GITHUB_OUTPUT": str(tmp_path / "output")} + proc = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", script], + env=env, capture_output=True, text=True, check=False) + assert proc.returncode == expected, proc.stderr From 6bd6fadb032a3f2a937c90cfc49c7c0e3f64aad9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 10 Oct 2026 00:18:16 +0900 Subject: [PATCH 2/9] docs: update K-CSAP central quality PR status Record PR #2615 publication, final delta review, and current remote check blockers without changing the workflow source. --- docs/doctoring/k-csap-skills-central-quality.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/k-csap-skills-central-quality.md b/docs/doctoring/k-csap-skills-central-quality.md index 80d1827b6c..b3f3603261 100644 --- a/docs/doctoring/k-csap-skills-central-quality.md +++ b/docs/doctoring/k-csap-skills-central-quality.md @@ -8,11 +8,11 @@ 독립 재리뷰는 동일 workflow/test 해시에서 `PASS_WITH_ACTIVATION_LIMITATIONS`를 반환했다. 부모가 report SHA-256 `923b518cf61b072d3fb8ca7e03e5a1825faaca3c248942dd3048d2fbc2a2a378` 및 두 파일 해시를 직접 대조했다. 리뷰의 focused pytest 21개, custom-label actionlint exit 0, 별도 harmless cleanup probe 11/11은 로컬 계약 증거다. 초기 FAIL report는 역사 기록으로 보존한다. setup-uv 도구 상태는 private cleanup 범위 밖이며 외부 action 내부 cleanup, immutable download 무결성, 실제 원격 실행/App 승인은 미검증이다. -현재 수리본은 job-level `if`를 runner 배정 전 gate로 추가하고, repo-scoped `k-csap-isolated` label을 사용한다. 기존 step admission은 checkout 전 방어층으로 유지했다. exact head, protected master ref, 일회용 source path, Python 3.11.14 및 uv/private venv/cache를 좁은 테스트로 검증했다. 초안 FAIL을 역사 기록으로 유지하며 수리본의 독립 재리뷰는 로컬 소스 한정 조건부 통과다. 수리 과정의 위임 writer 2회는 provider transport 실패로 결과 없이 종료했으며 부모가 테스트 RED를 확인하고 직접 수리했다. 수정본 로컬 `pytest` 21개 통과, scratch custom-label actionlint와 `git diff --check` exit 0; workflow SHA-256 `9c4cc914b57f919da06f434527670d34597ac4a8c1fc4c24008ce36440fdc5c9`, test SHA-256 `f053b9e139ddf3c2e566ce7966df1988af2a467d9627813404a2ee119da03f05`. 이는 실 runner에서의 실행/격리를 입증하지 않는다. +현재 수리본은 job-level `if`를 runner 배정 전 gate로 추가하고, repo-scoped `k-csap-isolated` label을 사용한다. 기존 step admission은 checkout 전 방어층으로 유지했다. exact head, protected master ref, 일회용 source path, Python 3.11.14 및 uv/private venv/cache를 좁은 테스트로 검증했다. 초안 FAIL을 역사 기록으로 유지하며 수리본의 독립 재리뷰는 로컬 소스 한정 조건부 통과다. 수리 과정의 위임 writer 2회는 provider transport 실패로 결과 없이 종료했으며 부모가 테스트 RED를 확인하고 직접 수리했다. 수정본 로컬 `pytest` 21개 통과, scratch custom-label actionlint와 `git diff --check` exit 0; workflow SHA-256 `9c4cc914b57f919da06f434527670d34597ac4a8c1fc4c24008ce36440fdc5c9`, 최종 test SHA-256 `64ae572dfee0063e85ec4850969c02018105aceeb237d20abb1dedc7fe68d2c9`, 최종 staged patch SHA-256 `03a8a6260ae18aa887aab3151dcf8848c0fbe2f3c28cd47b6808699bf579968e`다. 이는 실 runner에서의 실행/격리를 입증하지 않는다. ## 리뷰 사실관계 구분 -리뷰는 “checkout도 admission 전에 실행된다”고 기록했으나 frozen YAML은 첫 step admission, 다음 step checkout 순서다. 이 서술은 현재 파일로 지지되지 않는다. 반면 job runner 배정은 첫 step보다 앞서므로 **step admission만으로 배정 이전 신뢰 경계가 생기지 않는다는 지적은 유효하다.** 원본 FAIL을 삭제하거나 PASS로 덮어쓰지 않는다. 다음 리뷰에서 이 두 결론을 구분한다. +리뷰는 “checkout도 admission 전에 실행된다”고 기록했으나 frozen YAML은 첫 step admission, 다음 step checkout 순서다. 이 서술은 현재 파일로 지지되지 않는다. 반면 job runner 배정은 첫 step보다 앞서므로 **step admission만으로 배정 이전 신뢰 경계가 생기지 않는다는 지적은 유효하다.** 원본 FAIL을 삭제하거나 PASS로 덮어쓰지 않는다. 완료한 재리뷰 `923b518c…`에서 이 두 결론을 구분했다. PR exact head 검증은 기존 PR #30의 exact-head 증거 계약을 이어받은 의도다. GitHub merge candidate를 검증했다는 주장은 하지 않는다. push/manual 호출은 protected master로 제한하는 수리 계약이다. @@ -33,12 +33,13 @@ PR exact head 검증은 기존 PR #30의 exact-head 증거 계약을 이어받 ## 재현 가능한 테스트 환경 -새 전용 테스트 lock `requirements-k-csap-quality-tests-hashes.txt`는 `requirements-k-csap-quality-tests.in`에서 uv 0.12.5의 실제 compile로 생성했다. 공유 manifest/lock은 수정하지 않았다. private scratch CPython 3.11.16 환경에서 hash-required binary-only sync로 pytest 9.1.1, PyYAML 6.0.3 및 4개 전이 의존성을 설치하고 21개 계약 테스트를 실행해 통과했다. YAML 텍스트에서 기대값을 합성하는 대안 parser는 폐기했으며 실제 PyYAML로 구조를 읽는다. `subprocess.run(check=False)`를 명시하여 실패 exit 검증을 유지했다. 이 테스트 파일 delta는 별도 재검토 대상이다. runtime workflow의 Python pin은 3.11.14로 유지되어, 이 실행은 workflow 원격 runtime 실행 증거가 아니다. +새 전용 테스트 lock `requirements-k-csap-quality-tests-hashes.txt`는 `requirements-k-csap-quality-tests.in`에서 uv 0.12.5의 실제 compile로 생성했다. 공유 manifest/lock은 수정하지 않았다. private scratch CPython 3.11.16 환경에서 hash-required binary-only sync로 pytest 9.1.1, PyYAML 6.0.3 및 4개 전이 의존성을 설치하고 21개 계약 테스트를 실행해 통과했다. YAML 텍스트에서 기대값을 합성하는 대안 parser는 폐기했으며 실제 PyYAML로 구조를 읽는다. `subprocess.run(check=False)`를 명시하여 실패 exit 검증을 유지했다. 최종 delta 재리뷰 report SHA-256 `0d36a3d66c8d6ae435128cfaeda8d0926b3fa0721a50ddc3c7b489e4d157095d`가 이 테스트/lock/문서 범위를 조건부 통과로 바인딩했다. runtime workflow의 Python pin은 3.11.14로 유지되어, 이 실행은 workflow 원격 runtime 실행 증거가 아니다. ## 다음 gate -1. 수리 writer의 실제 테스트 결과, 파일 해시, 수정 중단을 확인한다. -2. 최종 해시에서 독립 재리뷰를 수행한다. -3. 중앙 callee를 출판하고 immutable commit을 확보한다. 소비자 caller에 가짜 SHA나 mutable branch를 넣지 않는다. +1. 로컬 소스와 최종 delta 독립 검토 완료. 운영 제한은 미수락이다. +2. 중앙 Draft PR #2615를 출판했다. 초도 head는 `9996a21600b3751491842de20ca4c87e1cc257a8`이며 아직 병합하지 않았다. +3. 실제 required checks와 formal App 검토를 확인하고 제한을 해결한 뒤 일반 병합으로 callee를 보호 브랜치에 출판한다. 소비자 caller에 가짜 SHA나 mutable branch를 넣지 않는다. 4. caller·runner ACL·capacity를 승인된 경계에서 연결하고 run ID와 job.runner_name을 검증한다. 5. 실제 Noema/OpenCode auto inference 및 조건부 App 승인을 별도 확인한다. 단독 개발자에게 가상의 다른 인간 리뷰어를 요구하지 않는다. +6. 현재 head에서 확인한 두 실패는 별개다. CodeQL `Analyze (javascript-typescript)`는 billing account lock 때문에 시작하지 않았다. Noema는 Draft라 model inference를 skip한 뒤 Artifact storage quota로 upload에 실패했다. GraphQL annotations로 원인을 확인했으며 REST log 조회 quota 403은 별도 조회 실패다. `coverage-evidence`/`coverage-source-tree` SUCCESS와 `opencode-review` SKIPPED는 실 모델 승인 증거가 아니다. 필요한 check 실패 및 운영 제한 해결 전 Draft를 유지한다. From 77fb8982e684b95aab9bfb3e6050e469e74df16c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 10 Oct 2026 22:04:22 +0900 Subject: [PATCH 3/9] docs: update authorized startup-failure retirement goal --- GOAL.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/GOAL.md b/GOAL.md index b237464a6d..3902dd7f18 100644 --- a/GOAL.md +++ b/GOAL.md @@ -1,6 +1,6 @@ # Goal — K-CSAP 중앙 self-hosted 품질 실행 -갱신: 2026-10-09 KST. 사용자 직접 지시를 따른다. +갱신: 2026-10-10 KST. 사용자 직접 지시를 따른다. 최신 지시는 원격 Startup failure를 만드는 hosted 절차도 전환 대상으로 포함하며, 전환할 수 없으면 비활성화하도록 허용한다. 병합을 위한 실행·독립 App 승인까지 진행할 권한이 있다. 기준 완화·보호 우회·승인 조작은 허용하지 않는다. ## 결과 @@ -74,4 +74,8 @@ ## Hosted-only retirement -K-CSAP 일반 품질 검증은 hosted-only가 아니다. 중앙 self-hosted가 실제 활성화되면 제품의 hosted 실행 경로를 제거한다. group access/runner가 준비되지 않은 동안 old hosted workflow를 먼저 끄고 성공으로 표시하지 않는다. 새 중앙 gate가 실제 실행되지 않으면 unavailable evidence로 남는다. +K-CSAP 일반 품질 검증은 hosted-only가 아니다. 중앙 self-hosted producer로 교체하며 hosted fallback은 제거한다. 최신 사용자는 Startup failure 경로를 전환할 수 없으면 끄도록 허용했다. disabled producer는 unavailable evidence로 남기며 통과 또는 보안 면제로 표시하지 않는다. + +2026-10-10 직접 조치: 중앙 `.github`의 동적 Code Quality 설정을 supported PATCH `state=not-configured`로 비활성화하고 GET readback을 확인했다. self-hosted labeled 모드는 API가 지원하지만 현재 CodeQL group 4의 workflow allowlist는 동적 quality를 허용하지 않는다. 승인된 경로의 활성화 전까지 이 producer를 중지한다. 중앙 advanced `codeql-pr.yml`과 `codeql-scan-dispatch.yml`은 active로 보존했다. 일반 Actions disable API는 동적 workflow에서 422를 반환했으므로 registry `active`만으로 feature 설정을 판단하지 않는다. 과거 실패 check는 삭제하거나 성공으로 바꾸지 않는다. + +제품 `k-csap-skills` Code Quality 설정은 GET `not-configured`이며 이 세션은 제품 설정을 수정하지 않았다. kelp가 제품 dynamic 설정의 단일 담당자다. #2565 shared routing 및 #2560/Remora auto/key custody는 유지한다. 다음 실행 단계는 중앙 PR #2615의 actual runner/auto App gate를 기존 경로에서 활성화하고 일반 병합까지 진행하는 것이다. From aacc387349c62b54ed96a9bd96b17c022296897b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 10 Oct 2026 22:42:35 +0900 Subject: [PATCH 4/9] fix(ci): bind K-CSAP admission and private uv tooling Actual baseline 2 FAIL/3 PASS; repair 28 tests; fresh independent actual-shell 35 controls and 4 SHA consumers conditionally accepted. No activation or formal approval claimed. --- .github/workflows/k-csap-skills-quality.yml | 38 ++++++++-- .../k-csap-skills-central-quality.md | 4 + ...k_csap_skills_quality_workflow_contract.py | 76 ++++++++++++++++++- 3 files changed, 110 insertions(+), 8 deletions(-) diff --git a/.github/workflows/k-csap-skills-quality.yml b/.github/workflows/k-csap-skills-quality.yml index 647a5169dd..b5dfadbe48 100644 --- a/.github/workflows/k-csap-skills-quality.yml +++ b/.github/workflows/k-csap-skills-quality.yml @@ -14,7 +14,10 @@ jobs: github.repository == 'ContextualWisdomLab/k-csap-skills' && ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.state == 'open') || + github.event.pull_request.state == 'open' && + github.event.pull_request.head.sha != '' && + github.event.pull_request.number > 0 && + github.ref == format('refs/pull/{0}/merge', github.event.pull_request.number)) || ((github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master')) runs-on: [self-hosted, Linux, X64, k-csap-isolated] @@ -26,9 +29,10 @@ jobs: TARGET_REPOSITORY: ${{ github.repository }} EVENT_NAME: ${{ github.event_name }} HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || '' }} - EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }} + EVENT_SHA: ${{ github.sha }} EVENT_REF: ${{ github.ref }} - PR_STATE: ${{ github.event.pull_request.state || 'open' }} + PR_STATE: ${{ github.event.pull_request.state || '' }} shell: bash --noprofile --norc -e -o pipefail {0} run: | set -euo pipefail @@ -42,12 +46,18 @@ jobs: echo "::error::K-CSAP quality only accepts master branch pushes or dispatches." exit 1 fi + EXPECTED_SHA="$EVENT_SHA" ;; pull_request) if [ "$HEAD_REPOSITORY" != "$TARGET_REPOSITORY" ] || [ "$PR_STATE" != "open" ]; then echo "::error::K-CSAP quality accepts only open same-repository pull requests." exit 1 fi + if ! [[ "$EVENT_REF" =~ ^refs/pull/[1-9][0-9]*/merge$ ]]; then + echo "::error::K-CSAP quality rejected a malformed pull request ref." + exit 1 + fi + EXPECTED_SHA="$PR_HEAD_SHA" ;; *) echo "::error::K-CSAP quality rejected an unsupported event." @@ -58,6 +68,7 @@ jobs: echo "::error::K-CSAP quality rejected a malformed source SHA." exit 1 fi + echo "expected_sha=$EXPECTED_SHA" >>"$GITHUB_OUTPUT" echo "admitted=true" >>"$GITHUB_OUTPUT" - name: Prepare run-owned source directory @@ -88,21 +99,38 @@ jobs: - name: Checkout exact caller source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ steps.admission.outputs.expected_sha }} path: ${{ steps.prepare.outputs.source_path }} persist-credentials: false clean: true + - name: Prepare private setup action storage + working-directory: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + set -euo pipefail + test ! -e .ci-tools && test ! -L .ci-tools + mkdir -p .ci-tools/tmp .ci-tools/tool-cache + - name: Install pinned uv action uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + env: + RUNNER_TOOL_CACHE: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }}/.ci-tools/tool-cache + RUNNER_TEMP: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }}/.ci-tools/tmp with: version: '0.9.7' + checksum: b26fcc8dfa1c39b5a5613445af3be3eefda45d9a39359bee271eafe34913583e enable-cache: false + restore-cache: false + save-cache: false + cache-local-path: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }}/.ci-tools/cache + tool-dir: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }}/.ci-tools/uv-tools + tool-bin-dir: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }}/.ci-tools/bin - name: Verify K-CSAP quality in a job-private environment working-directory: ${{ github.workspace }}/${{ steps.prepare.outputs.source_path }} env: - EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + EXPECTED_SHA: ${{ steps.admission.outputs.expected_sha }} shell: bash --noprofile --norc -e -o pipefail {0} run: | set -euo pipefail diff --git a/docs/doctoring/k-csap-skills-central-quality.md b/docs/doctoring/k-csap-skills-central-quality.md index b3f3603261..736f7f0ae9 100644 --- a/docs/doctoring/k-csap-skills-central-quality.md +++ b/docs/doctoring/k-csap-skills-central-quality.md @@ -4,6 +4,10 @@ ## 현재 단계 +2026-10-10 admission 수리본의 독립 fresh review는 `CONDITIONAL_SOURCE_ACCEPTANCE`다. 부모가 report SHA-256 `dbc0d58d24a098c68f33268fcd8dc1a6de00f57bfe729aa0dd0272fd4e29ff62` 및 workflow `2b14845ebf761652ca78ecdaac86c6e851ea41dc573312f41ade2695b0a719b7`, test `36bbf733ea0bec7e0cd3b03cb07d6c466cf19fffb578f16114e820a9d69c3737`를 직접 대조했다. 실제 admission shell 35개와 SHA 소비자 4개 사례가 기대대로 동작했고 고정본 계약 테스트 28개가 통과했다. PR head/event SHA 및 state를 분리하며 admission 출력 SHA를 checkout/HEAD 검증이 함께 소비한다. Flatback의 이전 head 독립 재현은 2 FAIL/3 PASS로 보존한다. 이전 아래 해시는 역사 증거이며 새 수리본의 승인으로 재사용하지 않는다. + +setup-uv 사설 저장소/checksum 수정은 실제 pinned dist의 macOS arm64 Node24 실행, 잘못된 checksum 거부, cache-disabled post 실행으로 검토했다. report SHA-256 `2159eec4f6343b82250124b78af57ed8925deaa8a63dc007a6f7e15a9670da83`를 대조했으며 admission 변경과 해당 setup/cleanup 블록의 바이트가 동일함을 fresh reviewer가 확인했다. Linux X64/Node20 실제 Actions, Python runtime 무결성, same-UID 공격 및 강제 종료 격리는 미검증이다. job expression 검증은 로컬 합성 입력이며 원격 runner 배정 증거가 아니다. scratch custom-label actionlint는 canonical CI vocabulary 수락을 대체하지 않는다. 새 소스 출판은 허용되지만 Draft 해제·운영 활성화·병합은 아직 수락하지 않았다. + 초안의 독립 리뷰가 FAIL이다. 원본 workflow SHA-256은 `1743d6c788606bdbd6caa2006a2de47a22029ed5696d2a9a705085542224fa45`, 테스트는 `29e2d825a666332c6bf8a3272359d72c743856606f5d940c3f6920869e354db6`이다. 리뷰 report SHA-256은 `a67ed03a6136c0bdc98b48201b68720489f10a68c10a7eaa8eaa5e72f5546fd9`이다. 초안의 9개 계약 테스트와 actionlint 통과는 이 FAIL을 대체하지 않는다. 독립 재리뷰는 동일 workflow/test 해시에서 `PASS_WITH_ACTIVATION_LIMITATIONS`를 반환했다. 부모가 report SHA-256 `923b518cf61b072d3fb8ca7e03e5a1825faaca3c248942dd3048d2fbc2a2a378` 및 두 파일 해시를 직접 대조했다. 리뷰의 focused pytest 21개, custom-label actionlint exit 0, 별도 harmless cleanup probe 11/11은 로컬 계약 증거다. 초기 FAIL report는 역사 기록으로 보존한다. setup-uv 도구 상태는 private cleanup 범위 밖이며 외부 action 내부 cleanup, immutable download 무결성, 실제 원격 실행/App 승인은 미검증이다. diff --git a/tests/test_k_csap_skills_quality_workflow_contract.py b/tests/test_k_csap_skills_quality_workflow_contract.py index 605e84a2c8..ba7781ef5a 100644 --- a/tests/test_k_csap_skills_quality_workflow_contract.py +++ b/tests/test_k_csap_skills_quality_workflow_contract.py @@ -54,6 +54,75 @@ def test_admission_before_checkout_and_no_credential_persistence(): assert steps[0]["env"]["HEAD_REPOSITORY"].startswith("${{") +@pytest.mark.parametrize( + ("head_sha", "ref", "state", "expected"), + [ + ("a" * 40, "refs/pull/17/merge", "open", 0), + ("", "refs/pull/17/merge", "open", 1), + ("a" * 40, "refs/heads/master", "open", 1), + ("a" * 40, "refs/pull/17/merge", "", 1), + ("a" * 40, "refs/pull/0/merge", "open", 1), + ], +) +def test_pr_admission_never_falls_back_to_event_sha(tmp_path, head_sha, ref, state, expected): + import os + import subprocess + + step = workflow()["jobs"]["quality"]["steps"][0] + values = { + "${{ github.repository }}": "ContextualWisdomLab/k-csap-skills", + "${{ github.event_name }}": "pull_request", + "${{ github.event.pull_request.head.repo.full_name || '' }}": "ContextualWisdomLab/k-csap-skills", + "${{ github.event.pull_request.head.sha || github.sha }}": head_sha or "b" * 40, + "${{ github.event.pull_request.head.sha || '' }}": head_sha, + "${{ github.sha }}": "b" * 40, + "${{ github.ref }}": ref, + "${{ github.event.pull_request.state || 'open' }}": state or "open", + "${{ github.event.pull_request.state || '' }}": state, + } + env = {"PATH": os.environ["PATH"], **{key: values[value] for key, value in step["env"].items()}, + "GITHUB_OUTPUT": str(tmp_path / "output")} + result = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", step["run"]], + env=env, capture_output=True, text=True, check=False) + assert result.returncode == expected, result.stdout + result.stderr + output = tmp_path / "output" + if expected == 0: + assert output.read_text() == f"expected_sha={head_sha}\nadmitted=true\n" + else: + assert not output.exists() + + +def test_source_consumers_use_admitted_sha(): + steps = workflow()["jobs"]["quality"]["steps"] + checkout = next(step for step in steps if step.get("uses", "").startswith("actions/checkout@")) + verify = next(step for step in steps if step.get("name", "").startswith("Verify K-CSAP")) + assert checkout["with"]["ref"] == "${{ steps.admission.outputs.expected_sha }}" + assert verify["env"]["EXPECTED_SHA"] == "${{ steps.admission.outputs.expected_sha }}" + gate = workflow()["jobs"]["quality"]["if"] + assert "github.event.pull_request.head.sha != ''" in gate + assert "format('refs/pull/{0}/merge', github.event.pull_request.number)" in gate + + +def test_setup_uv_is_checksum_pinned_and_job_owned(): + steps = workflow()["jobs"]["quality"]["steps"] + setup = next(step for step in steps if step.get("uses", "").startswith("astral-sh/setup-uv@")) + assert setup["with"]["checksum"] == ( + "b26fcc8dfa1c39b5a5613445af3be3eefda45d9a39359bee271eafe34913583e" + ) + assert setup["env"]["RUNNER_TOOL_CACHE"] == ( + "${{ github.workspace }}/${{ steps.prepare.outputs.source_path }}/.ci-tools/tool-cache" + ) + assert setup["env"]["RUNNER_TEMP"] == ( + "${{ github.workspace }}/${{ steps.prepare.outputs.source_path }}/.ci-tools/tmp" + ) + assert setup["with"]["cache-local-path"].endswith("/.ci-tools/cache") + assert setup["with"]["enable-cache"] is False + assert setup["with"]["save-cache"] is False + assert setup["with"]["restore-cache"] is False + assert setup["with"]["tool-bin-dir"].endswith("/.ci-tools/bin") + assert setup["with"]["tool-dir"].endswith("/.ci-tools/uv-tools") + + def test_locked_quality_steps_and_scope_are_executable(): steps = workflow()["jobs"]["quality"]["steps"] source = "\n".join(step.get("run", "") for step in steps) @@ -142,8 +211,9 @@ def test_actual_admission_shell_rejects_untrusted_before_checkout( script = workflow()["jobs"]["quality"]["steps"][0]["run"] env = {**os.environ, "TARGET_REPOSITORY": repository, "EVENT_NAME": event, - "HEAD_REPOSITORY": head_repository, "EXPECTED_SHA": "a" * 40, - "PR_STATE": "open", "EVENT_REF": "refs/heads/master", + "HEAD_REPOSITORY": head_repository, "PR_HEAD_SHA": "a" * 40, + "EVENT_SHA": "b" * 40, "PR_STATE": "open", + "EVENT_REF": "refs/pull/17/merge" if event == "pull_request" else "refs/heads/master", "GITHUB_OUTPUT": str(tmp_path / "output")} proc = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", script], env=env, capture_output=True, text=True, check=False) @@ -203,7 +273,7 @@ def test_admission_rejects_non_master_closed_pr_and_invalid_sha( script = workflow()["jobs"]["quality"]["steps"][0]["run"] env = {**os.environ, "TARGET_REPOSITORY": "ContextualWisdomLab/k-csap-skills", "EVENT_NAME": event, "HEAD_REPOSITORY": head, "EVENT_REF": ref, - "PR_STATE": state, "EXPECTED_SHA": sha, + "PR_STATE": state, "PR_HEAD_SHA": sha, "EVENT_SHA": sha, "GITHUB_OUTPUT": str(tmp_path / "output")} proc = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", script], env=env, capture_output=True, text=True, check=False) From c0d6235bd996159fa8c2bb2d176dce2d1983b38b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 10 Oct 2026 23:24:08 +0900 Subject: [PATCH 5/9] docs: set K-CSAP conditional merge goal and execution gates --- GOAL.md | 118 +++++++++++++++++++++++++------------------------------- 1 file changed, 53 insertions(+), 65 deletions(-) diff --git a/GOAL.md b/GOAL.md index 3902dd7f18..2d94bcf761 100644 --- a/GOAL.md +++ b/GOAL.md @@ -1,81 +1,69 @@ -# Goal — K-CSAP 중앙 self-hosted 품질 실행 +# Goal — K-CSAP 중앙 self-hosted 실행과 조건부 병합 -갱신: 2026-10-10 KST. 사용자 직접 지시를 따른다. 최신 지시는 원격 Startup failure를 만드는 hosted 절차도 전환 대상으로 포함하며, 전환할 수 없으면 비활성화하도록 허용한다. 병합을 위한 실행·독립 App 승인까지 진행할 권한이 있다. 기준 완화·보호 우회·승인 조작은 허용하지 않는다. +갱신: 2026-10-10 23:04 KST. 이 worktree의 단일 작성자는 goosefish 부모다. -## 결과 +## 완료 결과 -`ContextualWisdomLab/k-csap-skills`의 일반 품질 검증을 조직 `ContextualWisdomLab/.github`의 중앙 reusable workflow로 이동한다. GitHub-hosted fallback을 두지 않는다. 현재 제품 저장소의 `ubuntu-latest` workflow는 중앙 workflow의 immutable commit을 호출하는 얇은 caller로 교체한다. +중앙 `ContextualWisdomLab/.github` PR #2615를 검증과 독립 App 승인 후 일반 병합으로 `main`에 전달한다. 제품 `k-csap-skills`에는 검증된 중앙 commit을 호출하는 얇은 caller만 남긴다. 실제 self-hosted job과 개인 LiteLLM `auto` 리뷰를 실행하고, 작성자와 다른 Noema/OpenCode App의 현재 head 승인을 확인한다. -이 Goal은 Noema/OpenCode 공유 모델·App reviewer나 LiteLLM key를 구현하지 않는다. 그 파일과 key custody는 기존 #2560, PR #2565 및 Remora worktree 소유다. 이 increment는 중앙 품질 실행의 disjoint additive callee와 소비자 caller 계약만 소유한다. +사용자는 commit/push/조건부 merge와 Startup failure 경로의 중앙 self-hosted 전환을 승인했다. 전환 불가 hosted 절차의 비활성화도 승인했다. 이는 검증·개인정보·키 관리·보호 규칙 완화나 미실행 증거의 통과 승격 승인이 아니다. 별도의 인간 개발자를 전제로 하지 않는다. -## 검증된 현재 상태 +## 현재 직접 확인 +- PR #2615 head: `aacc387349c62b54ed96a9bd96b17c022296897b`; OPEN/Draft/BLOCKED, formal review 0. - 중앙 protected base: `7554587c2e3106a388998bcad048a3d7121de25e`. -- 제품 protected master: `0c66aec00f9f501cbbd530d8f74141d633928dc2`. -- 제품 `.github/workflows/ci.yml`은 `ubuntu-latest`를 직접 사용한다. -- 제품 repo-scoped runner `k-csap-isolated-linux` is online. Labels allow routing; its current group/ACL, running service and ID do not by themselves prove caller-context reusable-workflow admission or worktree isolation. Live check is #2560 / salmon ownership; keep group ACL changes with its operator. -- Group 13 is not the route for this candidate and all three members are offline; do not substitute the privileged control group. -- 제품 저장소는 비공개다. 2026-10-09 23:53 KST GraphQL `isPrivate=true`를 직접 확인했다. 앞선 공개 저장소 서술은 철회한다. fork 여부와 무관하게 same-repository open PR, protected master push/manual만 허용한다. -- OpenCode target allowlist와 auto LiteLLM route도 K-CSAP에 활성화되지 않았다. 품질 workflow 성공과 AI review/App approval은 별도다. -- 중앙 `LLM_GATEWAY_API_KEY` Secret metadata와 `LLM_GATEWAY_MODEL=auto`는 확인했다. 값은 읽지 않았고 새 키를 발급하거나 덮어쓰지 않는다. Secret/variable 존재는 실제 inference 또는 허용된 route의 증거가 아니다. - -## 소유 파일 - -- `.github/workflows/k-csap-skills-quality.yml` -- `tests/test_k_csap_skills_quality_workflow_contract.py` -- `requirements-k-csap-quality-tests.in` -- `requirements-k-csap-quality-tests-hashes.txt` -- `docs/doctoring/k-csap-skills-central-quality.md` -- 이 `GOAL.md` - -공유 Noema/OpenCode/runner migration workflow·relay·key·ACL은 변경하지 않는다. - -## 계약 - -- `on: workflow_call`만 사용. 실행 입력과 inherited secret 없음. -- job 이름 `k-csap-skills-quality`. -- 제품 repo-scoped labels `self-hosted`, `Linux`, `X64`, `k-csap-isolated`만 사용. hosted fallback 금지. -- first inline step가 checkout 전에 fixed caller repository, event, ref/head를 거부한다. k-csap repository의 same-repo pull_request/push/workflow_dispatch 호출만 admit한다. **첫 step는 runner 배정 이전의 방어책이 아니다.** caller job-level gate가 실행 이전 제한이고 skip은 검증 성공으로 간주하지 않는다. repo-scoped runner는 조직 group 13의 workflow allowlist에 속하지 않는다. -- PR은 exact head SHA를 검사한다. merge candidate 통합 검증은 별도 필요하며 head 통과로 대체하지 않는다. -- checkout은 중앙 source를 불변 workflow SHA에서 읽어 실행하지 않고, caller repository의 검증 대상 SHA를 credentials 비보존으로 checkout한다. -- 권한은 contents:read, timeout과 caller-owned concurrency를 명시. callee에는 동일 concurrency group을 두지 않는다. -- uv/Python 3.11 lock 기반으로 `ruff`, `pytest`, `scripts/validate.py`, `git diff --check`를 실행한다. source corpus·secret 없음. - -### 운영 수락표 - -| 환경 | 현재 runner에서 확인된 값 | 활성화 전 필요 조건 | -|---|---|---| -| source tree | uid 1001, cap-drop ALL, no-new-privileges, mounts 없음 | same-UID 격리는 아직 미증명. persistent shared checkout을 쓰지 않고 새 workspace 격리를 검증한다. | -| Python/uv | Python 3.12.3, uv 미설치 (salmon 보고) | locked uv/Python을 ephemeral runtime에 준비하고 버전과 package origin을 run receipt에 묶는다. | -| Group 13 | K-CSAP 접근 없음, 구성원 세 runner offline | 이 후보에서 사용하지 않는다. | -| repo runner id 2 | GitHub API online/idle | label, 별도 SSH gate, 또는 중앙 workflow 호출만으로 caller admission·network isolation이 증명되지 않는다. 실제 호출 job이 필요하다. | +- 공유 #2565 head: `ef0c9d4c668fac7f01609b8fbd61a4126b103f5c`, 아직 protected main에 전달되지 않았다. +- callee workflow SHA-256: `2b14845ebf761652ca78ecdaac86c6e851ea41dc573312f41ade2695b0a719b7`. +- test SHA-256: `36bbf733ea0bec7e0cd3b03cb07d6c466cf19fffb578f16114e820a9d69c3737`. +- fresh source review `dbc0d58d24a098c68f33268fcd8dc1a6de00f57bfe729aa0dd0272fd4e29ff62`: 조건부 소스 수용. actual admission 35개와 SHA 소비자 4개 통과, frozen test 28개 통과. 실행 도구 대체는 합성 fixture이며 제품 runtime 실행이 아니다. +- setup-uv review `2159eec4f6343b82250124b78af57ed8925deaa8a63dc007a6f7e15a9670da83`: 실제 pinned dist macOS arm64/Node24 설치·checksum 거부·post cleanup 확인. Linux X64/Node20 및 Python runtime 무결성은 미검증이다. +- 제품 repo runner id 2 `k-csap-isolated-linux`는 online/idle이며 labels `[self-hosted, Linux, X64, k-csap-isolated]`다. online/label은 격리·실행 증거가 아니다. Group 13은 사용하지 않는다. +- 제품은 비공개다. 원문·추출본·private 증적·키를 공개 중앙 저장소로 전송하지 않는다. -### 중앙 reusable 실행격리 +## 소유권과 기존 경로 -| 영역 | 구현 계약 | 상태 | +| 영역 | 단일 담당 | 이 Goal의 경계 | |---|---|---| -| runner 접근 | product repo runner id 2만 허용. group 13/privileged control fallback 없음. ACL/group 변경은 owner 전용 | 실행 전 admission 확인 필요 | -| 동시 작업 | shared host/container를 사용하는 한 same-UID untrusted 동시 job이 없음을 증명하거나, 운영자가 보장하는 상호배제 lease 사용 | 미증명; host process scan은 원자적 lease 대체 아님 | -| source/runtime | stale checkout reuse 금지; run-id/attempt 전용 source path, private runtime/cache, exact SHA checkout | workflow `9c4cc914…` 로컬 lifecycle 통과; hostile 동시 writer/강제 종료 격리는 미검증 | -| cleanup | 실제 격리·exclusive lease의 운영 수락이 필요 | 소스 재리뷰 PASS_WITH_ACTIVATION_LIMITATIONS; 원격 activation 미수락 | -| network | 허용/거부 egress 경계를 실제 workflow runner에서 확인 | NOT_RUN | -| toolchain | uv 없음이 보고됨. pinned uv와 Python 3.11을 job 내부 설치, locked sync, 실제 버전/모듈 origin 기록 | 중앙 job 미실행 | -| admission | caller repository/event/ref/head, fork 거부, checkout 전 trusted caller gate | 실제 Actions 미검증 | +| PR #2615 producer, 전용 tests/hash lock, 이 Goal, doctoring 문서 | goosefish 부모 | exact reviewed bytes만 수정·출판 | +| 제품 thin caller와 runner 활성화 | oscar 및 기존 운영 담당 | 승인된 새 caller/격리 packet을 받아 연결; 경쟁 편집·runner 이동 금지 | +| acceptance 조정 | salmon, 독립 admission oracle | actual packet을 확인하며 보고를 승인으로 승격하지 않음 | +| shared routing/Noema/OpenCode/canonical actionlint | #2565/Remora 담당 | accepted exact-hunk 인계 전 shared 파일 편집 금지 | +| 서비스 키·relay·effective auto route | 기존 #2560 담당 | Secret 값 읽기·중복 발급·덮어쓰기 금지 | +| 제품 dynamic Code Quality 설정 | kelp | GET not-configured 확인 완료, 이 session 제품 설정 write 0 | + +## 실행 계약 + +- `workflow_call`만 사용하고 executable inputs/inherited secrets는 없다. +- caller 신뢰 gate를 runner 배정 전 job-level `if`에 둔다. 첫 step는 checkout 전 방어층이며 host 격리를 대신하지 않는다. skip은 quality 통과가 아니다. +- same-repo open PR과 positive-number `refs/pull/N/merge`만 허용한다. PR head와 event SHA/state는 분리한다. missing head/state fallback은 금지한다. +- PR exact head, master push/manual event SHA를 admission에서 확정한다. checkout과 HEAD 검증은 동일 `expected_sha` 출력만 소비한다. merge candidate 검증은 별도다. +- 일회용 source child와 private uv tool/cache/temp/venv, archive checksum 및 정상 종료 cleanup을 유지한다. hostile same-UID 동시 작업·잔류 프로세스·강제 종료에 대한 격리는 별도 운영 gate다. +- Python 3.11.14와 uv 0.9.7로 locked ruff/pytest/validate/diff-check를 실행한다. 로컬 contract 환경 Python 3.11.16을 workflow pin 실행으로 주장하지 않는다. +- canonical actionlint의 `k-csap-isolated` vocabulary와 전용 test lock 설치 linkage를 수락해야 한다. scratch vocabulary 통과는 canonical gate 통과가 아니다. +- 모델은 literal `auto`, endpoint는 `https://litellm.poinnetworks.net`다. `orchestrator/free` 실행은 대체 수락하지 않는다. 비용·private retention 사실을 임의 attestation으로 만들지 않는다. + +## 병합 전 수락 단계 + +1. 실제 source bytes와 로컬 독립 review의 hash binding을 유지한다. unchanged suite 반복 대신 다음 미충족 gate로 진행한다. +2. exact-head 운영 packet에서 Linux/Node runtime, toolchain provenance, 격리·동시 작업·network·cleanup을 확인한다. approved runtime이 없으면 운영 담당과 좁게 수리한다. +3. 공유 auto route의 base chain을 default `main`까지 추적한다. feature-stack merge는 배포가 아니다. canonical lint label과 test dependency linkage도 해당 소유자와 결합한다. +4. 실제 self-hosted run/job ID와 runner_name, 실행된 source SHA 및 required step exits를 확인한다. +5. 실제 개인 gateway 인증 추론과 substantive verdict, author-distinct canonical App의 현재 head/base review를 확인한다. Draft skip/무료 요약/인프라 오류는 approval이 아니다. +6. live head/checks/required rules/unresolved findings를 fail-closed로 판정한다. 실패·skip·부재·다른 head 증거는 Ready/merge로 진행하지 않는다. 성공 후 일반 merge를 수행하고 state/mergedAt/mergeCommit/default branch를 읽어 확인한다. +7. 제품 caller를 immutable protected producer pin으로 전환하고 실제 caller-context 실행을 확인한다. hosted fallback 없이 교체한다. -## 수락 단계 +## 현재 수락 상태 -1. 계약 테스트 RED→GREEN, YAML parse, actionlint. -2. 정확 파일 diff 독립 리뷰. -3. 중앙 PR merge 및 immutable commit 획득. -4. 제품 caller에서 중앙 commit pin과 기존 hosted workflow 제거. -5. 제품 repo-scoped `k-csap-isolated-linux`에서 exact-head job 실행. -6. 실행 ID, runner_name, step exits를 확인. 그 전에는 configured/proposed 상태다. -7. AI review는 #2560/Remora를 통해 literal auto actual inference와 author-distinct App review를 별도 확인한다. +현재 사용자 지시에 따라 병합 작업까지 진행한다. PR #2615는 `OPEN/DRAFT/BLOCKED`, head `aacc387349c62b54ed96a9bd96b17c022296897b`, source review는 `CONDITIONAL_SOURCE_ACCEPTANCE`다. 이는 merge gate 충족이 아니다. source owner scope는 #2615의 여섯 파일이며 shared #2565, #2560, Remora, kelp 설정 파일은 소유하지 않는다. -## Hosted-only retirement +## 다음 실제 작업 -K-CSAP 일반 품질 검증은 hosted-only가 아니다. 중앙 self-hosted producer로 교체하며 hosted fallback은 제거한다. 최신 사용자는 Startup failure 경로를 전환할 수 없으면 끄도록 허용했다. disabled producer는 unavailable evidence로 남기며 통과 또는 보안 면제로 표시하지 않는다. +1. 이미 시작한 운영 진단 작업의 실제 호스트 관측과 실행 receipt를 대조하고, 격리·toolchain의 좁은 수리를 담당자와 실행한다. +2. 병렬 전달 경로 작업으로 #2565의 base chain과 canonical label의 다음 정상 통합 지점을 결정한다. 운영 작업과 불필요한 선행조건으로 묶지 않는다. +3. 개인 gateway `auto` 추론과 작성자와 다른 App의 현재 head review를 검증한다. shared/key 담당자의 source와 custody를 유지한다. +4. 모든 병합 gate가 충족되면 최신 head에 결속한 preflight와 동일 실행 경로에서 정상 Ready/merge를 수행한다. 실패·skip·부재 시 mutation을 멈추고 해당 원인을 수리한다. +5. Kelp/Flatback은 이 worktree를 편집하지 않고 독립 packet을 반환한다. 다음 checkpoint는 실행 중인 두 작업의 구조화된 결과다. -2026-10-10 직접 조치: 중앙 `.github`의 동적 Code Quality 설정을 supported PATCH `state=not-configured`로 비활성화하고 GET readback을 확인했다. self-hosted labeled 모드는 API가 지원하지만 현재 CodeQL group 4의 workflow allowlist는 동적 quality를 허용하지 않는다. 승인된 경로의 활성화 전까지 이 producer를 중지한다. 중앙 advanced `codeql-pr.yml`과 `codeql-scan-dispatch.yml`은 active로 보존했다. 일반 Actions disable API는 동적 workflow에서 422를 반환했으므로 registry `active`만으로 feature 설정을 판단하지 않는다. 과거 실패 check는 삭제하거나 성공으로 바꾸지 않는다. +## Hosted 절차 중단 상태 -제품 `k-csap-skills` Code Quality 설정은 GET `not-configured`이며 이 세션은 제품 설정을 수정하지 않았다. kelp가 제품 dynamic 설정의 단일 담당자다. #2565 shared routing 및 #2560/Remora auto/key custody는 유지한다. 다음 실행 단계는 중앙 PR #2615의 actual runner/auto App gate를 기존 경로에서 활성화하고 일반 병합까지 진행하는 것이다. +중앙 동적 Code Quality는 supported PATCH로 `not-configured`를 설정하고 GET으로 확인했다. generic workflow disable은 422여서 registry active identity와 feature 활성화를 구분한다. advanced CodeQL security workflow는 보존했다. 제품 설정은 kelp receipt `92e725eaa5422cbc0d3356a74afc749f75f4779854bcfda270572ea24e734f94`에서 not-configured이며 중복 write는 하지 않았다. 과거 billing failure와 artifact quota 실패는 역사 증거로 유지한다. disabled/unavailable 절차를 성공·보안 면제·merge 우회로 간주하지 않는다. From ad10996643a508f81ac7787ea450a442fe28dc29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 11 Oct 2026 22:49:57 +0900 Subject: [PATCH 6/9] fix(ci): create private K-CSAP directories and separate review gates --- .github/workflows/k-csap-skills-quality.yml | 2 ++ GOAL.md | 16 +++++------ .../k-csap-skills-central-quality.md | 10 ++++++- ...k_csap_skills_quality_workflow_contract.py | 28 +++++++++++++++++++ 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/.github/workflows/k-csap-skills-quality.yml b/.github/workflows/k-csap-skills-quality.yml index b5dfadbe48..9092c293fe 100644 --- a/.github/workflows/k-csap-skills-quality.yml +++ b/.github/workflows/k-csap-skills-quality.yml @@ -92,6 +92,7 @@ jobs: echo "::error::Source path already exists; refusing stale source." exit 1 fi + umask 077 mkdir -- "$GITHUB_WORKSPACE/$source_path" echo "source_path=$source_path" >>"$GITHUB_OUTPUT" @@ -110,6 +111,7 @@ jobs: run: | set -euo pipefail test ! -e .ci-tools && test ! -L .ci-tools + umask 077 mkdir -p .ci-tools/tmp .ci-tools/tool-cache - name: Install pinned uv action diff --git a/GOAL.md b/GOAL.md index 2d94bcf761..6f18a264ec 100644 --- a/GOAL.md +++ b/GOAL.md @@ -1,6 +1,6 @@ # Goal — K-CSAP 중앙 self-hosted 실행과 조건부 병합 -갱신: 2026-10-10 23:04 KST. 이 worktree의 단일 작성자는 goosefish 부모다. +갱신: 2026-10-11 KST. 이 worktree의 단일 작성자는 goosefish 부모다. ## 완료 결과 @@ -49,8 +49,8 @@ 3. 공유 auto route의 base chain을 default `main`까지 추적한다. feature-stack merge는 배포가 아니다. canonical lint label과 test dependency linkage도 해당 소유자와 결합한다. 4. 실제 self-hosted run/job ID와 runner_name, 실행된 source SHA 및 required step exits를 확인한다. 5. 실제 개인 gateway 인증 추론과 substantive verdict, author-distinct canonical App의 현재 head/base review를 확인한다. Draft skip/무료 요약/인프라 오류는 approval이 아니다. -6. live head/checks/required rules/unresolved findings를 fail-closed로 판정한다. 실패·skip·부재·다른 head 증거는 Ready/merge로 진행하지 않는다. 성공 후 일반 merge를 수행하고 state/mergedAt/mergeCommit/default branch를 읽어 확인한다. -7. 제품 caller를 immutable protected producer pin으로 전환하고 실제 caller-context 실행을 확인한다. hosted fallback 없이 교체한다. +6. 리뷰 요청 단계와 병합 수락 단계를 분리한다. Ready는 승인·검증 통과가 아니다. 소스 검토와 live head/base/필수 규칙/미해결 finding 확인 후, 실행 가능한 승인된 reviewer 경로가 있으면 Ready만 전환할 수 있다. 실제 callee 증거는 oscar가 승인된 임시 caller에서 immutable 후보 SHA를 사용해 병합 전에 확보한다. legacy/다른 모델 실행은 요구된 `auto` 증거로 세지 않는다. +7. 병합 직전 live head/checks/required rules/unresolved findings를 fail-closed로 판정한다. 실패·skip·부재·다른 head 증거가 있으면 merge하지 않는다. 모든 조건 충족 후 일반 merge를 수행하고 state/mergedAt/mergeCommit/default branch를 읽어 확인한다. 최종 제품 caller는 immutable protected producer pin으로 전환하고 실제 caller-context 실행을 확인한다. hosted fallback 없이 교체한다. ## 현재 수락 상태 @@ -58,11 +58,11 @@ ## 다음 실제 작업 -1. 이미 시작한 운영 진단 작업의 실제 호스트 관측과 실행 receipt를 대조하고, 격리·toolchain의 좁은 수리를 담당자와 실행한다. -2. 병렬 전달 경로 작업으로 #2565의 base chain과 canonical label의 다음 정상 통합 지점을 결정한다. 운영 작업과 불필요한 선행조건으로 묶지 않는다. -3. 개인 gateway `auto` 추론과 작성자와 다른 App의 현재 head review를 검증한다. shared/key 담당자의 source와 custody를 유지한다. -4. 모든 병합 gate가 충족되면 최신 head에 결속한 preflight와 동일 실행 경로에서 정상 Ready/merge를 수행한다. 실패·skip·부재 시 mutation을 멈추고 해당 원인을 수리한다. -5. Kelp/Flatback은 이 worktree를 편집하지 않고 독립 packet을 반환한다. 다음 checkpoint는 실행 중인 두 작업의 구조화된 결과다. +1. 읽기전용 운영 보고의 최종 해시를 검증했다. online/container 관측은 배타 lease나 process settlement/cleanup 통과가 아니다. oscar의 기존 운영 수리와 caller 후보를 재사용한다. +2. permissive umask에서 source와 `.ci-tools`가 0777로 만들어지는 실제 shell RED 2개를 확인했다. 각 생성 단계에 `umask 077`을 적용하고 계약 테스트 30개 통과를 확인했다. 이는 다른 UID의 접근 제한이며 같은 UID의 적대적 writer 격리 증거가 아니다. 독립 delta 리뷰 후 게시한다. +3. canonical CI lock의 PyYAML 누락은 실제 collection RED exit 2로 확인했다. dedicated PyYAML 6.0.3 hash block을 추가한 scratch 제안은 28개 통과다. shared 소유자에게 comment `6098771884`로 전달했다. 2026-10-11 remote head `b78007bfc976faaa4d416d2b6578b114e2725a82`의 canonical lock에 PyYAML pin이 추가된 사실을 확인했다. 해당 pin의 설치 검증 및 보호된 main 통합은 별도다. canonical lint의 `k-csap-isolated`는 이 head에서도 없다. +4. bootstrap plan review `5eaa615dc1f6ac143f526329ae46d68f95284452114cada4a83c77e3391586b1`의 CHANGES_REQUESTED를 채택했다. Ready와 merge를 분리하고 임시 exact-SHA caller로 실행 순환을 해소한다. 실제 `auto`/App 및 운영 수락 조건은 유지한다. +5. 기존 caller 후보는 PR32 base 위의 oscar 소유 2파일이며 아직 미게시다. source가 바뀌면 기존 후보 pin의 증거를 새 SHA로 이전하지 않는다. 독립 리뷰와 운영 승인 후 소유자가 pin을 갱신하고 실행한다. ## Hosted 절차 중단 상태 diff --git a/docs/doctoring/k-csap-skills-central-quality.md b/docs/doctoring/k-csap-skills-central-quality.md index 736f7f0ae9..cff231d81e 100644 --- a/docs/doctoring/k-csap-skills-central-quality.md +++ b/docs/doctoring/k-csap-skills-central-quality.md @@ -46,4 +46,12 @@ PR exact head 검증은 기존 PR #30의 exact-head 증거 계약을 이어받 3. 실제 required checks와 formal App 검토를 확인하고 제한을 해결한 뒤 일반 병합으로 callee를 보호 브랜치에 출판한다. 소비자 caller에 가짜 SHA나 mutable branch를 넣지 않는다. 4. caller·runner ACL·capacity를 승인된 경계에서 연결하고 run ID와 job.runner_name을 검증한다. 5. 실제 Noema/OpenCode auto inference 및 조건부 App 승인을 별도 확인한다. 단독 개발자에게 가상의 다른 인간 리뷰어를 요구하지 않는다. -6. 현재 head에서 확인한 두 실패는 별개다. CodeQL `Analyze (javascript-typescript)`는 billing account lock 때문에 시작하지 않았다. Noema는 Draft라 model inference를 skip한 뒤 Artifact storage quota로 upload에 실패했다. GraphQL annotations로 원인을 확인했으며 REST log 조회 quota 403은 별도 조회 실패다. `coverage-evidence`/`coverage-source-tree` SUCCESS와 `opencode-review` SKIPPED는 실 모델 승인 증거가 아니다. 필요한 check 실패 및 운영 제한 해결 전 Draft를 유지한다. +6. 초도 head에서 확인한 두 실패는 별개다. CodeQL `Analyze (javascript-typescript)`는 billing account lock 때문에 시작하지 않았다. Noema는 Draft라 model inference를 skip한 뒤 Artifact storage quota로 upload에 실패했다. GraphQL annotations로 원인을 확인했으며 REST log 조회 quota 403은 별도 조회 실패다. `coverage-evidence`/`coverage-source-tree` SUCCESS와 `opencode-review` SKIPPED는 실 모델 승인 증거가 아니다. 이 실패 이력은 현재 head의 검증 결과로 승계하지 않는다. + +## 2026-10-11 다음 병합 준비 단계 + +- bootstrap 독립 plan review는 `CHANGES_REQUESTED`, 보고서 해시 `5eaa615dc1f6ac143f526329ae46d68f95284452114cada4a83c77e3391586b1`다. Ready는 리뷰 요청이며 merge 승인과 분리한다. 승인된 reviewer 경로가 준비되면 소스 및 live preflight 후 Ready만 전환할 수 있다. 실제 callee 실행은 oscar가 승인된 임시 exact-SHA caller로 병합 전에 확인한다. 실행·literal auto·App 승인·필수 check가 미충족이면 병합하지 않는다. +- 실제 inline shell을 permissive `umask 000` 아래 실행하면 source와 `.ci-tools`가 mode0777로 생성되는 RED 2개를 확인했다. 각 생성 단계의 `umask 077` 수리 후 30개 계약 테스트가 통과했다. 다른 UID 접근 제한만 확인하며 같은 UID의 비협조 writer 격리나 forced-kill cleanup을 증명하지 않는다. +- 이전 scratch Python 3.11 환경과 actionlint config가 없어 최초 호출은 setup 실패했다. 새 검증은 기존 hash-locked Python 3.14.7 scratch 환경을 사용했다. workflow의 Python 3.11.14 runtime 통과가 아니다. +- canonical PyYAML 누락 제안은 #2565 comment `6098771884`로 전달했다. 최신 remote shared head `b78007bfc976faaa4d416d2b6578b114e2725a82`의 canonical lock에 PyYAML pin이 있음을 확인했다. 보호된 main 배포와 실제 설치는 별도다. 그 head의 canonical lint label에는 `k-csap-isolated`가 없다. diagnostic label config의 성공은 canonical CI 수락이 아니다. +- oscar의 PR32 caller 후보 packet을 읽었다. 미게시 2파일 변경이며 base는 `9b0b1a202ea23b96ba2d3fe71ac1d86b0ca6bc36`, 기존 producer pin은 `c0d6235bd996159fa8c2bb2d176dce2d1983b38b`다. 후보의 로컬 테스트는 소유자 보고이며 Actions 실행이 아니다. 새 producer bytes를 게시하면 caller 소유자가 pin을 다시 검토해야 한다. diff --git a/tests/test_k_csap_skills_quality_workflow_contract.py b/tests/test_k_csap_skills_quality_workflow_contract.py index ba7781ef5a..be759f7f0f 100644 --- a/tests/test_k_csap_skills_quality_workflow_contract.py +++ b/tests/test_k_csap_skills_quality_workflow_contract.py @@ -254,6 +254,34 @@ def test_real_prepare_and_cleanup_preserve_stale_and_sibling_source(tmp_path, st assert sentinel.read_text() == "unchanged" +@pytest.mark.parametrize("storage", ["source", "setup-tools"]) +def test_owned_directories_are_private_with_permissive_parent_umask(tmp_path, storage): + import os + import stat + import subprocess + + steps = workflow()["jobs"]["quality"]["steps"] + output = tmp_path / "output" + env = {**os.environ, "RUN_ID": "83", "RUN_ATTEMPT": "2", + "GITHUB_WORKSPACE": str(tmp_path), "GITHUB_OUTPUT": str(output)} + if storage == "source": + step = steps[1] + directory = tmp_path / "k-csap-quality.83.2" + else: + step = next(item for item in steps + if item.get("name") == "Prepare private setup action storage") + directory = tmp_path / ".ci-tools" + result = subprocess.run( + ["bash", "-e", "-o", "pipefail", "-c", "umask 000; " + step["run"]], + cwd=tmp_path, env=env, capture_output=True, text=True, check=False, + ) + assert result.returncode == 0, result.stderr + assert stat.S_IMODE(directory.stat().st_mode) == 0o700 + for child in directory.rglob("*"): + if child.is_dir(): + assert stat.S_IMODE(child.stat().st_mode) == 0o700 + + @pytest.mark.parametrize( ("event", "head", "ref", "state", "sha", "expected"), [ From 1fd6f9317c479edee6f1a9dc1fbad2100e3c04ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 13 Oct 2026 06:48:35 +0900 Subject: [PATCH 7/9] fix(ci): register K-CSAP canonical actionlint label --- .github/actionlint.yaml | 6 ++++++ tests/test_k_csap_actionlint_config.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 .github/actionlint.yaml create mode 100644 tests/test_k_csap_actionlint_config.py diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..4be3e85c79 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +# Source validation only; labels do not attest runtime isolation. +self-hosted-runner: + labels: + - cwlab-ci-isolated + - cwlab-control + - k-csap-isolated diff --git a/tests/test_k_csap_actionlint_config.py b/tests/test_k_csap_actionlint_config.py new file mode 100644 index 0000000000..7b467096ab --- /dev/null +++ b/tests/test_k_csap_actionlint_config.py @@ -0,0 +1,18 @@ +"""K-CSAP canonical actionlint 설정의 자동 발견과 라벨 범위를 검증한다.""" + +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] + + +def test_canonical_config_registers_only_reviewed_custom_labels(): + """공유 라벨을 보존하고 K-CSAP 라벨만 추가하며 wildcard는 거부한다.""" + config = ROOT / ".github/actionlint.yaml" + assert config.is_file(), "canonical actionlint config missing" + assert yaml.safe_load(config.read_text(encoding="utf-8")) == { + "self-hosted-runner": { + "labels": ["cwlab-ci-isolated", "cwlab-control", "k-csap-isolated"] + } + } From b9e980378741979274f98cd6e7e918538c07b9eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 13 Oct 2026 13:12:11 +0900 Subject: [PATCH 8/9] test: keep CodeQL current-time and sandbox HOME fixtures valid --- tests/test_audit_org_codeql_coverage.py | 21 ++++++++++--- tests/test_sandboxed_web_e2e.py | 41 ++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 0ffc8fa749..5afeb920ad 100644 --- a/tests/test_audit_org_codeql_coverage.py +++ b/tests/test_audit_org_codeql_coverage.py @@ -35,9 +35,11 @@ def default_setup_scanning_nothing(name: str) -> dict: } -def covered_by_recent_analysis(name: str, *, days_ago: int = 1) -> dict: +def covered_by_recent_analysis( + name: str, *, days_ago: int = 1, now: datetime = NOW +) -> dict: """Return a repository payload covered by a recent, successful CodeQL analysis.""" - created_at = (NOW - timedelta(days=days_ago)).isoformat().replace("+00:00", "Z") + created_at = (now - timedelta(days=days_ago)).isoformat().replace("+00:00", "Z") return { "name": name, "archived": False, @@ -263,7 +265,11 @@ def test_null_latest_analysis_is_not_coverage() -> None: def test_audit_codeql_coverage_defaults_now_to_current_time() -> None: - repositories = [covered_by_recent_analysis("DefaultNowRepo", days_ago=0)] + repositories = [ + covered_by_recent_analysis( + "DefaultNowRepo", days_ago=0, now=datetime.now(timezone.utc) + ) + ] assert audit.audit_codeql_coverage(repositories) == [] @@ -302,7 +308,14 @@ def test_main_fail_path_reports_gaps_from_stdin(monkeypatch, capsys) -> None: def test_main_pass_path_reports_from_file_arg(tmp_path, capsys) -> None: payload_path = tmp_path / "repositories.json" payload_path.write_text( - json.dumps([covered_by_default_setup("ELUNVERA"), covered_by_recent_analysis("Orgmetra")]), + json.dumps( + [ + covered_by_default_setup("ELUNVERA"), + covered_by_recent_analysis( + "Orgmetra", now=datetime.now(timezone.utc) + ), + ] + ), encoding="utf-8", ) diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 1b1cdf3722..96503f0b79 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -1571,7 +1571,21 @@ def test_isolated_command_rejects_executable_outside_bound_roots(monkeypatch, tm ) -def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(monkeypatch, tmp_path): +@pytest.fixture +def isolated_host_home(monkeypatch, tmp_path): + """Separate synthetic host home from scratch without relaxing production denial. + + TMPDIR may legitimately live under the caller's home on macOS. These + path-resolution fixtures model a workspace outside the denied host home; + the separate host-home rejection test continues to use the real home. + """ + home = tmp_path / "host-home" + home.mkdir() + monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + return home + + +def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(monkeypatch, tmp_path, isolated_host_home): """A ./gradlew-style repository launcher resolves against the sandboxed cwd. ``shutil.which`` resolves any command string containing a path separator @@ -1602,7 +1616,7 @@ def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(mon assert command.endswith("./gradlew build") -def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypatch, tmp_path): +def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypatch, tmp_path, isolated_host_home): """A bare command resolves when PATH itself has a relative entry. ``shutil.which`` joins a relative ``PATH`` entry with the *calling @@ -1635,7 +1649,7 @@ def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypa assert command.endswith("tool") -def test_isolated_command_translates_absolute_workspace_launcher_to_sandbox_mount(monkeypatch, tmp_path): +def test_isolated_command_translates_absolute_workspace_launcher_to_sandbox_mount(monkeypatch, tmp_path, isolated_host_home): """An absolute copied-repo launcher path is rewritten to its /workspace equivalent. ``isolated_command`` binds ``sandbox_root`` at ``SANDBOX_MOUNT`` inside @@ -1681,6 +1695,23 @@ def test_which_relative_to_cwd_returns_none_for_empty_path(tmp_path): assert result is None +def test_isolated_command_rejects_explicit_host_home_even_inside_workspace(monkeypatch, tmp_path): + """An allowed scratch workspace cannot turn a host-home executable into code.""" + monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) + sandbox = tmp_path / "sandbox" + repo = sandbox / "repo" + repo.mkdir(parents=True) + launcher = repo / "launch.sh" + launcher.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + launcher.chmod(0o755) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + with pytest.raises(RuntimeError, match="host home directory"): + sandboxed_web_e2e.isolated_command( + str(launcher), backend="/usr/bin/bwrap", cwd=repo, + sandbox_root=sandbox, env={"PATH": "/usr/bin"}, + ) + + def test_isolated_command_rejects_relative_path_entry_escaping_sandbox(monkeypatch, tmp_path): """A relative PATH entry cannot be used to search the real host filesystem outside the copy. @@ -1704,7 +1735,7 @@ def test_isolated_command_rejects_relative_path_entry_escaping_sandbox(monkeypat ) -def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_path): +def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_path, isolated_host_home): """A repo-local launcher path that lexically escapes the sandbox root is still rejected.""" monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" @@ -1724,7 +1755,7 @@ def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_pat ) -def test_isolated_command_rejects_explicit_external_path(monkeypatch, tmp_path): +def test_isolated_command_rejects_explicit_external_path(monkeypatch, tmp_path, isolated_host_home): """An explicit absolute path outside the workspace and bind roots is still rejected.""" monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" From 52976e9b29af9f21a6def135a53ed9bd3854debf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 13 Oct 2026 13:27:54 +0900 Subject: [PATCH 9/9] fix(ci): address K-CSAP quality review findings - workflow-scope concurrency coalesces superseded PR heads before runner lease - check committed whitespace against the empty tree on clean checkouts - create the job-private root inside run-owned .ci-tools/tmp - date historical GOAL/doc records --- .github/workflows/k-csap-skills-quality.yml | 12 +++- GOAL.md | 21 ++++++- .../k-csap-skills-central-quality.md | 11 +++- ...k_csap_skills_quality_workflow_contract.py | 60 ++++++++++++++++++- 4 files changed, 95 insertions(+), 9 deletions(-) diff --git a/.github/workflows/k-csap-skills-quality.yml b/.github/workflows/k-csap-skills-quality.yml index 9092c293fe..a0915b5056 100644 --- a/.github/workflows/k-csap-skills-quality.yml +++ b/.github/workflows/k-csap-skills-quality.yml @@ -6,6 +6,12 @@ on: permissions: contents: read +# Coalesce superseded heads of the same PR before runner assignment. Non-PR runs +# use the unique run id so protected-branch runs never cancel each other. +concurrency: + group: k-csap-skills-quality-${{ github.repository }}-${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} + cancel-in-progress: true + jobs: quality: name: k-csap-skills-quality @@ -136,7 +142,8 @@ jobs: shell: bash --noprofile --norc -e -o pipefail {0} run: | set -euo pipefail - job_root="$(mktemp -d "${RUNNER_TEMP:?}/k-csap-quality.XXXXXX")" + test -d .ci-tools/tmp && test ! -L .ci-tools/tmp + job_root="$(mktemp -d "$PWD/.ci-tools/tmp/k-csap-quality.XXXXXX")" cleanup() { rm -rf -- "$job_root"; } trap cleanup EXIT unset PYTHONHOME PYTHONPATH @@ -150,7 +157,8 @@ jobs: uv run --python 3.11.14 ruff check . uv run --python 3.11.14 pytest -q uv run --python 3.11.14 python scripts/validate.py - git diff --check + # A clean checkout has no working-tree diff; check committed content. + git diff --check "$(git hash-object -t tree /dev/null)" HEAD - name: Clean run-owned source after any step result if: always() diff --git a/GOAL.md b/GOAL.md index 6f18a264ec..fdcd70af0e 100644 --- a/GOAL.md +++ b/GOAL.md @@ -1,6 +1,21 @@ # Goal — K-CSAP 중앙 self-hosted 실행과 조건부 병합 -갱신: 2026-10-11 KST. 이 worktree의 단일 작성자는 goosefish 부모다. +갱신: 2026-10-13 KST. 이 worktree의 단일 작성자는 goosefish 부모다. + +## 최신 직접 권한과 실행 분해 + +사용자는 인프라 장애에 대응하기 위한 bypass merge를 직접 허용했다. 아래 이전의 일반 병합 전용·일괄 HOLD 조건은 이 권한 범위에서 대체한다. 소스 전달과 운영 활성화를 분리한다. 확인된 코드·보안 결함은 수리하고, 미실행 CI/App 검사는 PASS나 APPROVED로 만들지 않는다. 사람 팀원을 추가로 요구하지 않는다. 미래 head 실행을 그 head의 게시 선행조건으로 두지 않는다. + +| 조각 | 담당 | 다음 실제 동작 | 완료 증거 | 진짜 선행 조건 | +|---|---|---|---|---| +| 현재 PR2615 전달 가능성 | 부모 + 읽기전용 평가자 | live head/base/checks/threads와 retained source review를 대조하고 우회할 인프라 check를 특정 | exact-head 병합 사전판정, 실제 merge와 readback 또는 실제 거절 | 미해결 substantive 소스 결함이 없음 | +| 전체 gate 실패 분리 | 별도 읽기전용 평가자 | 기존 9개 실패와 coverage 미달을 환경·상속 소스·미검증으로 분류하고 기존 수리 찾아 결합 범위 제시 | 현재 bytes에 근거한 최소 수리 경로 | label/runner/App 대기 불필요 | +| canonical label | 기존 shared owner + 부모 | 기존 한 줄 patch 수락과 producer config 연결, 기본 actionlint 실행 | canonical invocation 직접 exit | shared 파일 쓰기 경계만 직렬화 | +| central exact-head runtime 연결 | 부모 producer + oscar runtime | 실제 consumer 인자·승인 계약에 맞춘 adapter 구현·시험 | runnable adapter와 승인된 Linux lifecycle 증거 | 실행 시 runner custody·credential 분리 | +| literal auto/App 복구 | Remora/#2560 | 기존 경로의 실제 inference와 독립 App 게시 복구 | 실제 verdict/review object | 키·비공개 자료 보호 | + +인프라 복구는 source delivery와 병렬로 진행한다. bypass는 exact PR/head/base와 실제 인프라 실패에 한정한다. 보호 설정 삭제·가짜 status·자기 승인·force push·키/ACL 확대는 하지 않는다. 로컬 untracked CLI의 검증은 현재 원격 PR의 검증과 구별한다. 이전 head와 일정은 아래 역사적 기록이며 live 조회로 대체한다. + ## 완료 결과 @@ -8,11 +23,11 @@ 사용자는 commit/push/조건부 merge와 Startup failure 경로의 중앙 self-hosted 전환을 승인했다. 전환 불가 hosted 절차의 비활성화도 승인했다. 이는 검증·개인정보·키 관리·보호 규칙 완화나 미실행 증거의 통과 승격 승인이 아니다. 별도의 인간 개발자를 전제로 하지 않는다. -## 현재 직접 확인 +## 2026-10-10 기준 직접 확인 (역사 기록; 현재 값은 위 표와 live 조회로 대체) - PR #2615 head: `aacc387349c62b54ed96a9bd96b17c022296897b`; OPEN/Draft/BLOCKED, formal review 0. - 중앙 protected base: `7554587c2e3106a388998bcad048a3d7121de25e`. -- 공유 #2565 head: `ef0c9d4c668fac7f01609b8fbd61a4126b103f5c`, 아직 protected main에 전달되지 않았다. +- 2026-10-10 기준 공유 #2565 head: `ef0c9d4c668fac7f01609b8fbd61a4126b103f5c`; 당시 아직 protected main에 전달되지 않았다. 이후 #2565는 main에 병합됐다. - callee workflow SHA-256: `2b14845ebf761652ca78ecdaac86c6e851ea41dc573312f41ade2695b0a719b7`. - test SHA-256: `36bbf733ea0bec7e0cd3b03cb07d6c466cf19fffb578f16114e820a9d69c3737`. - fresh source review `dbc0d58d24a098c68f33268fcd8dc1a6de00f57bfe729aa0dd0272fd4e29ff62`: 조건부 소스 수용. actual admission 35개와 SHA 소비자 4개 통과, frozen test 28개 통과. 실행 도구 대체는 합성 fixture이며 제품 runtime 실행이 아니다. diff --git a/docs/doctoring/k-csap-skills-central-quality.md b/docs/doctoring/k-csap-skills-central-quality.md index cff231d81e..f33f0e9b69 100644 --- a/docs/doctoring/k-csap-skills-central-quality.md +++ b/docs/doctoring/k-csap-skills-central-quality.md @@ -1,6 +1,15 @@ # K-CSAP 중앙 품질 실행 상태 -확인일: 2026-10-09 KST. 관련 issue: 중앙 #2612. 이 문서는 실행 성공 또는 출시 승인이 아니다. +기준일: 2026-10-09 KST. 최종 갱신: 2026-10-13 KST. 관련 issue: 중앙 #2612. 이 문서는 실행 성공 또는 출시 승인이 아니다. + +## 2026-10-13 갱신 + +- canonical `.github/actionlint.yaml`에 `k-csap-isolated`를 등록했다. 기본 actionlint 호출 exit 0이다. +- 리뷰 지적에 따라 workflow-scope `concurrency`(같은 PR의 이전 head 취소, 비PR은 run id 분리)를 추가했다. +- `git diff --check`는 깨끗한 checkout에서 빈 결과만 내므로 빈 tree 대비 HEAD 전체를 검사하도록 바꿨다. 실제 git 저장소로 커밋된 공백의 거부를 시험했다. +- job 전용 임시 디렉터리를 공유 `RUNNER_TEMP`가 아닌 run-owned `.ci-tools/tmp` 아래에 만든다. +- 아래의 workflow SHA-256 `9c4cc914…`, `2b14845e…` 등은 당시 기록이다. 현재 head의 workflow 바이트는 이후 수정되었으므로 그 리뷰를 현재 head의 승인으로 재사용하지 않는다. +- 실제 Linux runner 실행·App 승인은 여전히 미검증이다. ## 현재 단계 diff --git a/tests/test_k_csap_skills_quality_workflow_contract.py b/tests/test_k_csap_skills_quality_workflow_contract.py index be759f7f0f..a97bd2c62e 100644 --- a/tests/test_k_csap_skills_quality_workflow_contract.py +++ b/tests/test_k_csap_skills_quality_workflow_contract.py @@ -24,10 +24,21 @@ def test_reusable_only_and_no_hosted_fallback(): source = CENTRAL.read_text(encoding="utf-8") assert "ubuntu-latest" not in source and "ubuntu-24.04" not in source assert "secrets:" not in source and "continue-on-error" not in source - assert "concurrency:" not in source + assert "concurrency" not in job assert job["timeout-minutes"] <= 20 +def test_workflow_scope_concurrency_supersedes_same_pr_only(): + """AGENTS.md: PR 번호로 runner 배정 전 병합하고, 비PR 실행은 서로 취소하지 않는다.""" + concurrency = workflow()["concurrency"] + group = concurrency["group"] + assert concurrency["cancel-in-progress"] is True + assert "github.repository" in group + assert "github.event.pull_request.number" in group + assert "github.run_id" in group + assert "sha" not in group + + def test_runner_assignment_admits_only_trusted_caller_before_lease(): job = workflow()["jobs"]["quality"] gate = job["if"] @@ -134,7 +145,6 @@ def test_locked_quality_steps_and_scope_are_executable(): assert required in source assert "set -euo pipefail" in source assert "git rev-parse HEAD" in source - assert "RUNNER_TEMP" in source assert "trap " in source assert "UV_PYTHON_INSTALL_DIR" in source assert "UV_CACHE_DIR" in source @@ -163,6 +173,7 @@ def test_quality_shell_keeps_original_status_and_removes_private_runtime(tmp_pat work = tmp_path / "source" work.mkdir() (work / ".git").mkdir() + (work / ".ci-tools/tmp").mkdir(parents=True) sentinel = tmp_path / "untouched" sentinel.write_text("keep") bindir = tmp_path / "bin" @@ -173,7 +184,7 @@ def test_quality_shell_keeps_original_status_and_removes_private_runtime(tmp_pat tool.write_text( "#!/bin/sh\n" "if [ \"$1\" = rev-parse ]; then printf '%s\\n' \"$EXPECTED_SHA\"; exit 0; fi\n" - "if [ \"$1\" = diff ]; then exit 0; fi\n" + "if [ \"$1\" = diff ] || [ \"$1\" = hash-object ]; then exit 0; fi\n" "if [ \"$1\" = python ] && [ \"$2\" = install ]; then : >\"$MARKER\"; fi\n" "if [ \"$FAIL_AT\" = install ] && [ \"$1\" = python ]; then exit 37; fi\n" "if [ \"$FAIL_AT\" = test ] && [ \"$1\" = run ] && " @@ -189,9 +200,52 @@ def test_quality_shell_keeps_original_status_and_removes_private_runtime(tmp_pat assert proc.returncode == {"success": 0, "install": 37, "test": 43}[failed_at], proc.stderr assert sentinel.read_text() == "keep" assert list(tmp_path.glob("k-csap-quality.*")) == [] + assert list((work / ".ci-tools/tmp").glob("k-csap-quality.*")) == [] assert marker.exists() +@pytest.mark.parametrize("dirty", [False, True]) +def test_quality_shell_checks_committed_whitespace_in_run_owned_temp(tmp_path, dirty): + """깨끗한 checkout에서도 커밋된 공백 오류를 거부하고 공유 RUNNER_TEMP를 쓰지 않는다.""" + import os + import subprocess + + verify = next( + step for step in workflow()["jobs"]["quality"]["steps"] + if step.get("name", "").startswith("Verify K-CSAP") + ) + work = tmp_path / "source" + work.mkdir() + git = ["git", "-c", "user.name=t", "-c", "user.email=t@example.invalid"] + subprocess.run([*git, "init", "-q"], cwd=work, check=True) + (work / "a.txt").write_text("x \n" if dirty else "x\n") + subprocess.run([*git, "add", "a.txt"], cwd=work, check=True) + subprocess.run([*git, "commit", "-qm", "c"], cwd=work, check=True) + sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=work, text=True).strip() + (work / ".ci-tools/tmp").mkdir(parents=True) + shared = tmp_path / "shared-runner-temp" + shared.mkdir() + bindir = tmp_path / "bin" + bindir.mkdir() + seen = tmp_path / "seen" + uv = bindir / "uv" + uv.write_text('#!/bin/sh\nprintf "%s\\n" "$UV_PROJECT_ENVIRONMENT" >>"$SEEN"\nexit 0\n') + uv.chmod(0o755) + env = {**os.environ, "PATH": f"{bindir}:{os.environ['PATH']}", + "RUNNER_TEMP": str(shared), "EXPECTED_SHA": sha, "SEEN": str(seen)} + proc = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", verify["run"]], + cwd=work, env=env, capture_output=True, text=True, check=False) + assert (proc.returncode != 0) is dirty, proc.stdout + proc.stderr + if dirty: + assert "trailing whitespace" in proc.stdout + proc.stderr + venvs = seen.read_text().split() + assert venvs and all( + Path(v).resolve().is_relative_to((work / ".ci-tools/tmp").resolve()) for v in venvs + ) + assert list(shared.iterdir()) == [] + assert list((work / ".ci-tools/tmp").iterdir()) == [] + + @pytest.mark.parametrize( ("repository", "event", "head_repository", "expected_exit"), [