From fa9bd9491c13ef652b6eb1a42747f504c8756202 Mon Sep 17 00:00:00 2001 From: Will Griffin Date: Sat, 25 Jul 2026 19:41:00 -0600 Subject: [PATCH] ci(agent-policy): select central channel --- .agents/project.yaml | 2 + .github/workflows/agent-policy.yml | 139 ++++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 11 deletions(-) diff --git a/.agents/project.yaml b/.agents/project.yaml index 2828040..a83f745 100644 --- a/.agents/project.yaml +++ b/.agents/project.yaml @@ -16,6 +16,8 @@ "claude": "dispatch: claude", "codex": "dispatch: codex", "hermes": "dispatch: hermes", + "jcode": "dispatch: jcode", + "kimi": "dispatch: kimi", "zcode": "dispatch: zcode" } }, diff --git a/.github/workflows/agent-policy.yml b/.github/workflows/agent-policy.yml index fc13203..f201717 100644 --- a/.github/workflows/agent-policy.yml +++ b/.github/workflows/agent-policy.yml @@ -16,6 +16,7 @@ permissions: contents: read issues: read pull-requests: read + statuses: read packages: read jobs: @@ -26,19 +27,101 @@ jobs: steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1 - - name: Load pinned policy runtime + # cosign-installer v4 shells out to envsubst to expand its install path. + # The compatibility workflow must also run on the contract's ci-linux-x64 + # pool, where envsubst is intentionally not required. Download the same + # publisher-compatible cosign v3 binary directly and verify its pinned + # SHA-256 so lifecycle enforcement does not depend on an undeclared tool. + - name: Install signature verifier env: - POLICY_ARTIFACT: ${{ vars.AGENT_POLICY_ARTIFACT }} + COSIGN_VERSION: v3.0.6 + COSIGN_SHA256_AMD64: c956e5dfcac53d52bcf058360d579472f0c1d2d9b69f55209e256fe7783f4c74 + COSIGN_SHA256_ARM64: bedac92e8c3729864e13d4a17048007cfafa79d5deca993a43a90ffe018ef2b8 + run: | + set -euo pipefail + case "$(uname -m)" in + x86_64) asset=cosign-linux-amd64; expected="$COSIGN_SHA256_AMD64" ;; + aarch64 | arm64) asset=cosign-linux-arm64; expected="$COSIGN_SHA256_ARM64" ;; + *) echo "unsupported runner architecture: $(uname -m)" >&2; exit 1 ;; + esac + install_dir="$RUNNER_TEMP/hv-cosign" + mkdir -p "$install_dir" + curl -fsSL -o "$install_dir/cosign" \ + "https://github.com/sigstore/cosign/releases/download/$COSIGN_VERSION/$asset" + actual=$(python3 - "$install_dir/cosign" <<'PY' + import hashlib + import pathlib + import sys + + print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest()) + PY + ) + test "$actual" = "$expected" || { + echo "cosign checksum mismatch: expected $expected, got $actual" >&2 + exit 1 + } + chmod +x "$install_dir/cosign" + echo "$install_dir" >> "$GITHUB_PATH" + "$install_dir/cosign" version + - name: Select source-owned policy channel + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TARGET_REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + source_repository=$(gh api repos/happyvertical/.github --jq '.id') + test "$source_repository" = 1129270614 || { + echo "trusted policy source repository id is $source_repository, expected 1129270614" >&2 + exit 1 + } + channel_state="$RUNNER_TEMP/hv-agent-policy-channels.json" + gh api 'repos/happyvertical/.github/contents/.github/agent-policy-channels.json?ref=main' \ + --jq '.content' | base64 --decode > "$channel_state" + protected_selector="$RUNNER_TEMP/hv-agent-policy-channel-select" + gh api 'repos/happyvertical/.github/contents/scripts/hv-agent-policy-channel-select?ref=main' \ + --jq '.content' | base64 --decode > "$protected_selector" + chmod +x "$protected_selector" + selected_lock="$RUNNER_TEMP/hv-agent-policy-selected-lock.json" + target_repository_id=$(gh api "repos/$TARGET_REPOSITORY" --jq '.node_id') + channel=$(python3 "$protected_selector" \ + "$channel_state" "$selected_lock" "$target_repository_id") + artifact=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["artifact"])' "$selected_lock") + { + echo "HV_POLICY_ARTIFACT=$artifact" + echo "HV_POLICY_CHANNEL=$channel" + echo "HV_POLICY_LOCK=$selected_lock" + } >> "$GITHUB_ENV" + - name: Load selected policy runtime + env: + POLICY_ARTIFACT: ${{ env.HV_POLICY_ARTIFACT }} + POLICY_PUBLISHER_IDENTITY: ${{ vars.AGENT_POLICY_PUBLISHER_IDENTITY }} run: | case "$POLICY_ARTIFACT" in - ghcr.io/*@sha256:*) ;; - *) echo 'AGENT_POLICY_ARTIFACT must be an immutable GHCR digest' >&2; exit 1 ;; + ghcr.io/happyvertical/agent-policy@sha256:*) ;; + *) echo 'source-owned policy channel must select the trusted HappyVertical policy image by digest' >&2; exit 1 ;; esac + expected_publisher_identity_sha256=86a917ce537a9e923ce02103068c76eb72629247ab584f7d368ac238a0209857 + publisher_identity_sha256=$(python3 -c 'import hashlib, os; print(hashlib.sha256(os.environ["POLICY_PUBLISHER_IDENTITY"].encode()).hexdigest())') + test "$publisher_identity_sha256" = "$expected_publisher_identity_sha256" || { + echo 'AGENT_POLICY_PUBLISHER_IDENTITY must match the exact protected-main publisher identity' >&2 + exit 1 + } + publisher_workflow="${POLICY_PUBLISHER_IDENTITY%@refs/heads/main}" + test "$publisher_workflow" != "$POLICY_PUBLISHER_IDENTITY" || { + echo 'AGENT_POLICY_PUBLISHER_IDENTITY must name the protected-main publisher workflow' >&2 + exit 1 + } + publisher_workflow_regexp=$(printf '%s' "$publisher_workflow" | sed 's/[][\.*^()+?{}|$]/\\&/g') + cosign verify "$POLICY_ARTIFACT" \ + --certificate-identity-regexp "^${publisher_workflow_regexp}@refs/(heads/main|tags/agent-policy-v[0-9]+\.[0-9]+\.[0-9]+)$" \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' + policy_download_dir="$RUNNER_TEMP/hv-policy-download" policy_dir="$RUNNER_TEMP/hv-policy" - rm -rf "$policy_dir" - mkdir -p "$policy_dir" - oras pull "$POLICY_ARTIFACT" -o "$policy_dir" - archive="$policy_dir/agent-policy.tar.gz" + rm -rf "$policy_download_dir" "$policy_dir" + mkdir -p "$policy_download_dir" "$policy_dir" + oras pull "$POLICY_ARTIFACT" -o "$policy_download_dir" + archive="$policy_download_dir/agent-policy.tar.gz" test -f "$archive" || { echo 'policy artifact is missing agent-policy.tar.gz' >&2; exit 1; } python3 - "$archive" <<'PY' import pathlib @@ -52,16 +135,50 @@ jobs: raise SystemExit(f"unsafe policy archive member: {member.name}") PY tar -xzf "$archive" -C "$policy_dir" + identity=$(python3 "$policy_dir/scripts/verify-policy-artifact" "$policy_dir") + python3 - "$HV_POLICY_LOCK" "$policy_dir/policy-artifact.json" "$POLICY_ARTIFACT" <<'PY' + import json + import sys + + lock = json.load(open(sys.argv[1], encoding="utf-8")) + manifest = json.load(open(sys.argv[2], encoding="utf-8")) + expected = { + "artifact": sys.argv[3], + "generation": manifest.get("generation"), + "policy_revision": manifest.get("policy_revision"), + "source_commit": manifest.get("source_commit"), + "source_tree_sha256": manifest.get("source_tree_sha256"), + } + for field, value in expected.items(): + if lock.get(field) != value: + raise SystemExit( + f"selected policy lock {field} does not match signed artifact manifest" + ) + PY + echo "Loaded $identity from $HV_POLICY_CHANNEL channel $POLICY_ARTIFACT" echo "HV_POLICY_DIR=$policy_dir" >> "$GITHUB_ENV" + echo "HV_POLICY_IDENTITY=$identity" >> "$GITHUB_ENV" - name: Validate lifecycle shell: bash env: GH_TOKEN: ${{ github.token }} + POLICY_ARTIFACT: ${{ env.HV_POLICY_ARTIFACT }} EVENT_NAME: ${{ github.event_name }} + EVENT_ACTION: ${{ github.event.action }} PR_NUMBER: ${{ inputs.pr_number || github.event.pull_request.number }} MERGE_GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }} MERGE_GROUP_HEAD_REF: ${{ github.event.merge_group.head_ref }} run: | - "$HV_POLICY_DIR/scripts/hv-agent-lifecycle-prs" | while IFS= read -r number; do - python3 "$HV_POLICY_DIR/scripts/hv-agent" check-pr "$number" - done + set -euo pipefail + if test "$EVENT_NAME" = workflow_dispatch && test "${PR_NUMBER:-0}" = 0; then + python3 "$HV_POLICY_DIR/scripts/hv-agent" audit . + exit + fi + numbers_file="$RUNNER_TEMP/hv-agent-lifecycle-prs" + "$HV_POLICY_DIR/scripts/hv-agent-lifecycle-prs" > "$numbers_file" + while IFS= read -r number; do + if ! python3 "$HV_POLICY_DIR/scripts/hv-agent" check-pr "$number"; then + echo "ERROR $HV_POLICY_IDENTITY from $POLICY_ARTIFACT rejected PR #$number; correct canonical claim/PR state, then rerun the organization-required workflow execution; a repository-local lifecycle-only dispatch is not protected merge authority and draft toggles are invalid" >&2 + exit 1 + fi + done < "$numbers_file"