From 742ed3dddd8ff85e3caa983753f87a01a24fcc87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:19:52 +0900 Subject: [PATCH 1/6] fix(hourly-loop): route product-development agent through orchestrator/free hourly-product-development.yml called integrate.api.nvidia.com directly through a repository-local credential broker (scripts/ci/nim_proxy.py), bypassing the org's governed contextual-orchestrator gateway entirely -- even though the workflow's own agent prompt told the AGENT to route any new product integration through contextual-orchestrator. Vendor the gateway at the same pinned commit ContextualWisdomLab/.github's central review sidecar already trusts, register the five org provider secrets into its process-local KV, and point OpenCode at the fail-closed zero-cost orchestrator/free pool instead of a fixed three-model NVIDIA NIM candidate list -- the gateway's own routing now supplies provider fallback, so the workflow no longer needs its own per-model retry loop. Extend the patch guard's credential-fingerprint scanning to cover all five provider secrets (previously only NVIDIA_NIM_API_KEY), remove the now-fully-redundant nim_proxy.py broker and its test, and update the two workflow contract-test files plus the operator docs, doctoring record, AGENTS.md, and CHANGELOG.md to match. Fixes #131. Co-Authored-By: Claude Sonnet 5 --- .../workflows/hourly-product-development.yml | 254 +++++++----- AGENTS.md | 6 +- CHANGELOG.md | 9 +- .../hourly-opencode-product-development.md | 15 +- docs/operations/hourly-product-development.md | 128 ++++--- scripts/ci/nim_proxy.py | 361 ------------------ .../tests/test_hourly_product_development.py | 85 +++-- .../test_hourly_product_incident_contract.py | 141 ++++--- .../tests/test_nim_proxy.py | 95 ----- 9 files changed, 400 insertions(+), 694 deletions(-) delete mode 100644 scripts/ci/nim_proxy.py delete mode 100644 services/account_unification/tests/test_nim_proxy.py diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 0a15d1d..4318210 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -25,13 +25,21 @@ env: CORE_WORKFLOWS: '["ci","CodeQL"]' OPENCODE_VERSION: "1.17.13" OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348 - OPENCODE_MODEL_CANDIDATES: >- - nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 - nvidia-nim/nvidia/nemotron-3-super-120b-a12b - nvidia-nim/deepseek-ai/deepseek-v4-pro + # A single fail-closed zero-cost virtual pool, not a fixed model list: the + # gateway's own routing picks a live candidate from whichever of the five + # provider secrets are registered, so there is no per-model retry loop here + # any more (see ContextualWisdomLab/keyverse#131 and the identical two-call-site + # change in ContextualWisdomLab/contextual-orchestrator's own + # .github/workflows/opencode-hourly-loop.yml, PR #1013). + OPENCODE_MODEL: "contextual_orchestrator_gateway/orchestrator/free" OPENCODE_RUN_TIMEOUT_SECONDS: "2100" - NIM_PROXY_HOST: "127.0.0.1" - NIM_PROXY_PORT: "8765" + ORCHESTRATOR_HOST: "127.0.0.1" + ORCHESTRATOR_PORT: "8765" + ORCHESTRATOR_GIT_URL: "https://github.com/ContextualWisdomLab/contextual-orchestrator.git" + # Pinned to the exact commit ContextualWisdomLab/.github's central review + # sidecar (scripts/ci/contextual_orchestrator_review_sidecar.sh) already + # vendors and trusts. + ORCHESTRATOR_PIN_SHA: "045d17da5e2aea56a97e241ee158ab1628d78660" jobs: develop-product-gap: @@ -54,12 +62,16 @@ jobs: egress-policy: block disable-telemetry: true allowed-endpoints: >- + api.bytez.com:443 api.github.com:443 + api.openai.com:443 cafe.github.com:443 codeload.github.com:443 github.com:443 integrate.api.nvidia.com:443 + models.dev:443 objects.githubusercontent.com:443 + openrouter.ai:443 raw.githubusercontent.com:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 @@ -407,7 +419,7 @@ jobs: or touch more than 12 files or 1,500 changed lines. - Do not stage, commit, push, open or merge a pull request, approve work, tag, or publish a release. This workspace contains no .git directory and the - model process receives no GitHub, OIDC, or upstream NVIDIA credential. + model process receives no GitHub, OIDC, or upstream model-provider credential. Before finishing, run focused tests and the available local quality gates with the preinstalled environment. Update CHANGELOG.md [Unreleased], beginner- @@ -421,65 +433,129 @@ jobs: required checks. Do not publish a release. PROMPT - - name: Start the loopback-only NIM credential broker + - name: Vendor and start the contextual-orchestrator gateway if: steps.gate.outputs.develop == 'true' - id: nim_broker + id: orchestrator_gateway shell: bash env: - NIM_UPSTREAM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} + NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | set -euo pipefail - if [ -z "${NIM_UPSTREAM_API_KEY:-}" ]; then - echo "::error::NVIDIA_NIM_API_KEY is required only for model-backed development." + # Missing individual provider secrets are allowed (the gateway's own + # auto-discovery skips an unregistered provider); at least one of + # the five is required so the free pool is never empty. + provider_secret_count=0 + for secret_name in BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB OPENROUTER_API_KEY OPENAI_API_KEY; do + if [ -n "${!secret_name:-}" ]; then + provider_secret_count=$((provider_secret_count + 1)) + fi + done + if [ "$provider_secret_count" -lt 1 ]; then + echo "::error::At least one of BYTEZ_API_KEY, NVIDIA_NIM_API_KEY, NVIDIA_NIM_API_KEY_SUB, OPENROUTER_API_KEY, or OPENAI_API_KEY is required only for model-backed development." exit 1 fi + secret_fingerprint="$( python3 - <<'PY' import base64 import hashlib import os - secret = os.environ["NIM_UPSTREAM_API_KEY"].encode("utf-8") - representations = ( - secret, - base64.b64encode(secret), - base64.urlsafe_b64encode(secret), - secret.hex().encode("ascii"), - ) - print(",".join( - f"{len(value)}:{hashlib.sha256(value).hexdigest()}" - for value in representations - )) + fingerprints = [] + for name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + secret = os.environ.get(name, "").encode("utf-8") + if not secret: + continue + representations = ( + secret, + base64.b64encode(secret), + base64.urlsafe_b64encode(secret), + secret.hex().encode("ascii"), + ) + fingerprints.extend( + f"{len(value)}:{hashlib.sha256(value).hexdigest()}" + for value in representations + ) + print(",".join(fingerprints)) PY )" printf 'secret_fingerprint=%s\n' "$secret_fingerprint" >>"$GITHUB_OUTPUT" + + orchestrator_source="${RUNNER_TEMP}/contextual-orchestrator" + rm -rf "$orchestrator_source" + git clone --quiet --filter=blob:none --no-checkout \ + "$ORCHESTRATOR_GIT_URL" "$orchestrator_source" + git -C "$orchestrator_source" -c advice.detachedHead=false \ + checkout --quiet "$ORCHESTRATOR_PIN_SHA" + checked_out="$(git -C "$orchestrator_source" rev-parse HEAD)" + if [ "$checked_out" != "$ORCHESTRATOR_PIN_SHA" ]; then + echo "::error::Vendored contextual-orchestrator HEAD $checked_out != pin $ORCHESTRATOR_PIN_SHA" + exit 1 + fi + + python3 -m pip install --quiet --disable-pip-version-check --no-cache-dir \ + --require-hashes --no-deps \ + -r "$orchestrator_source/requirements.lock" + + gateway_token="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" + echo "::add-mask::$gateway_token" umask 077 - proxy_log="${RUNNER_TEMP}/keyverse-nim-proxy.log" - proxy_pid="${RUNNER_TEMP}/keyverse-nim-proxy.pid" - python scripts/ci/nim_proxy.py \ - --host "$NIM_PROXY_HOST" \ - --port "$NIM_PROXY_PORT" \ - >"$proxy_log" 2>&1 & - printf '%s\n' "$!" >"$proxy_pid" - unset NIM_UPSTREAM_API_KEY + token_file="${RUNNER_TEMP}/keyverse-orchestrator-gateway.token" + printf '%s' "$gateway_token" >"$token_file" + chmod 600 "$token_file" + # Export only the file path, never the raw bearer: GitHub renders + # $GITHUB_ENV before a later step's own add-mask can protect a value + # placed there directly. + printf 'ORCHESTRATOR_GATEWAY_TOKEN_FILE=%s\n' "$token_file" >>"$GITHUB_ENV" + + gateway_log="${RUNNER_TEMP}/keyverse-orchestrator-gateway.log" + gateway_pid_file="${RUNNER_TEMP}/keyverse-orchestrator-gateway.pid" + ( + cd "$orchestrator_source" + CONTEXTUAL_ORCHESTRATOR_TOKEN="$gateway_token" \ + BYTEZ_API_KEY="${BYTEZ_API_KEY:-}" \ + NVIDIA_NIM_API_KEY="${NVIDIA_NIM_API_KEY:-}" \ + NVIDIA_NIM_API_KEY_SUB="${NVIDIA_NIM_API_KEY_SUB:-}" \ + OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}" \ + OPENAI_API_KEY="${OPENAI_API_KEY:-}" \ + PYTHONPATH="$orchestrator_source" \ + nohup python3 -m scripts.ci.serve_seeded_gateway \ + --serve \ + --auto-discover-model-agents \ + --auth-token-key CONTEXTUAL_ORCHESTRATOR_TOKEN \ + --host "$ORCHESTRATOR_HOST" --port "$ORCHESTRATOR_PORT" \ + >"$gateway_log" 2>&1 & + echo "$!" >"$gateway_pid_file" + ) + unset gateway_token BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB OPENROUTER_API_KEY OPENAI_API_KEY ready=false - for _attempt in $(seq 1 30); do + for _attempt in $(seq 1 60); do if curl -fsS \ - "http://${NIM_PROXY_HOST}:${NIM_PROXY_PORT}/healthz" \ + "http://${ORCHESTRATOR_HOST}:${ORCHESTRATOR_PORT}/healthz" \ >/dev/null; then ready=true break fi - sleep 1 + sleep 2 done if [ "$ready" != "true" ]; then - cat "$proxy_log" >&2 - echo "::error::The loopback NIM credential broker did not become ready." + cat "$gateway_log" >&2 + echo "::error::The contextual-orchestrator gateway did not become ready." exit 1 fi - - name: Run the NVIDIA NIM development agent in a disposable workspace + - name: Run the orchestrator/free development agent in a disposable workspace if: steps.gate.outputs.develop == 'true' id: agent shell: bash @@ -487,21 +563,20 @@ jobs: set -euo pipefail trap 'sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true' EXIT prompt="$(cat "${RUNNER_TEMP}/keyverse-agent-prompt.md")" + gateway_token="$(cat "$ORCHESTRATOR_GATEWAY_TOKEN_FILE")" successful_workspace="" trusted_venv="${GITHUB_WORKSPACE}/services/account_unification/.venv" - for model in $OPENCODE_MODEL_CANDIDATES; do - sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true - agent_workspace="${RUNNER_TEMP}/keyverse-agent" - agent_home="${RUNNER_TEMP}/keyverse-agent-home" - rm -rf "$agent_workspace" "$agent_home" - install -d -m 0750 "$agent_workspace" "$agent_home" "$agent_home/tmp" - git archive HEAD | tar -x -C "$agent_workspace" + agent_workspace="${RUNNER_TEMP}/keyverse-agent" + agent_home="${RUNNER_TEMP}/keyverse-agent-home" + rm -rf "$agent_workspace" "$agent_home" + install -d -m 0750 "$agent_workspace" "$agent_home" "$agent_home/tmp" + git archive HEAD | tar -x -C "$agent_workspace" - cat >"${agent_workspace}/opencode.json" <<'CONFIG' + cat >"${agent_workspace}/opencode.json" <<'CONFIG' { "$schema": "https://opencode.ai/config.json", - "enabled_providers": ["nvidia-nim"], + "enabled_providers": ["contextual_orchestrator_gateway"], "lsp": false, "mcp": {}, "permission": { @@ -518,26 +593,16 @@ jobs: "external_directory": "deny" }, "provider": { - "nvidia-nim": { + "contextual_orchestrator_gateway": { "npm": "@ai-sdk/openai-compatible", - "name": "NVIDIA NIM through local Keyverse broker", + "name": "Contextual Orchestrator Gateway through the local Keyverse sidecar", "options": { "baseURL": "http://127.0.0.1:8765/v1", - "apiKey": "{env:NVIDIA_API_KEY}" + "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}" }, "models": { - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - "name": "NVIDIA Llama 3.3 Nemotron Super 49B v1.5", - "tool_call": true, - "limit": {"context": 131072, "output": 8192} - }, - "nvidia/nemotron-3-super-120b-a12b": { - "name": "NVIDIA Nemotron 3 Super 120B", - "tool_call": true, - "limit": {"context": 131072, "output": 8192} - }, - "deepseek-ai/deepseek-v4-pro": { - "name": "DeepSeek V4 Pro (NIM)", + "orchestrator/free": { + "name": "fail-closed zero-cost orchestration", "tool_call": true, "limit": {"context": 131072, "output": 8192} } @@ -547,53 +612,52 @@ jobs: } CONFIG - sudo chown -R 65532:65532 "$agent_workspace" "$agent_home" - echo "::group::opencode $model" - if timeout --kill-after=30s "${OPENCODE_RUN_TIMEOUT_SECONDS}s" \ - sudo -u '#65532' -g '#65532' env -i \ - PATH="${trusted_venv}/bin:/usr/local/bin:/usr/bin:/bin" \ - HOME="$agent_home" \ - TMPDIR="$agent_home/tmp" \ - PYTHONPATH="$agent_workspace/services/account_unification" \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_INDEX=1 \ - LANG=C.UTF-8 \ - LC_ALL=C.UTF-8 \ - NVIDIA_API_KEY=keyverse-local-broker \ - OPENCODE_DISABLE_AUTOUPDATE=1 \ - /bin/bash -c \ - 'ulimit -u 256; ulimit -n 1024; cd "$1"; exec opencode run "$2" --model "$3"' \ - bash "$agent_workspace" "$prompt" "$model"; then - sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true - successful_workspace="$agent_workspace" - echo "::endgroup::" - echo "Agent session completed with \`$model\`." >>"$GITHUB_STEP_SUMMARY" - break - fi + sudo chown -R 65532:65532 "$agent_workspace" "$agent_home" + echo "::group::opencode $OPENCODE_MODEL" + if timeout --kill-after=30s "${OPENCODE_RUN_TIMEOUT_SECONDS}s" \ + sudo -u '#65532' -g '#65532' env -i \ + PATH="${trusted_venv}/bin:/usr/local/bin:/usr/bin:/bin" \ + HOME="$agent_home" \ + TMPDIR="$agent_home/tmp" \ + PYTHONPATH="$agent_workspace/services/account_unification" \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_INDEX=1 \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + CONTEXTUAL_ORCHESTRATOR_TOKEN="$gateway_token" \ + OPENCODE_DISABLE_AUTOUPDATE=1 \ + /bin/bash -c \ + 'ulimit -u 256; ulimit -n 1024; cd "$1"; exec opencode run "$2" --model "$3"' \ + bash "$agent_workspace" "$prompt" "$OPENCODE_MODEL"; then sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true + successful_workspace="$agent_workspace" echo "::endgroup::" - echo "::warning::Model $model failed; its disposable workspace was discarded." - done + echo "Agent session completed with \`$OPENCODE_MODEL\`." >>"$GITHUB_STEP_SUMMARY" + else + sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true + echo "::endgroup::" + echo "::error::The orchestrator/free development agent run failed." + fi if [ -z "$successful_workspace" ]; then - echo "::error::Every NVIDIA NIM model candidate failed." + echo "::error::The orchestrator/free development agent run failed." exit 1 fi sudo chown -R "$(id -u):$(id -g)" "$successful_workspace" rm -f "$successful_workspace/opencode.json" echo "workspace=$successful_workspace" >>"$GITHUB_OUTPUT" - - name: Stop the credential broker and all model descendants + - name: Stop the gateway and all model descendants if: always() && steps.gate.outputs.develop == 'true' shell: bash run: | set -euo pipefail sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true - proxy_pid_file="${RUNNER_TEMP}/keyverse-nim-proxy.pid" - if [ -s "$proxy_pid_file" ]; then - proxy_pid="$(cat "$proxy_pid_file")" - kill "$proxy_pid" >/dev/null 2>&1 || true - wait "$proxy_pid" >/dev/null 2>&1 || true + gateway_pid_file="${RUNNER_TEMP}/keyverse-orchestrator-gateway.pid" + if [ -s "$gateway_pid_file" ]; then + gateway_pid="$(cat "$gateway_pid_file")" + kill "$gateway_pid" >/dev/null 2>&1 || true + wait "$gateway_pid" >/dev/null 2>&1 || true fi - name: Capture the bounded credential-free patch @@ -601,7 +665,7 @@ jobs: id: package shell: bash env: - KEYVERSE_FORBIDDEN_SECRET_FINGERPRINT: ${{ steps.nim_broker.outputs.secret_fingerprint }} + KEYVERSE_FORBIDDEN_SECRET_FINGERPRINT: ${{ steps.orchestrator_gateway.outputs.secret_fingerprint }} run: | set -euo pipefail artifact_dir="${RUNNER_TEMP}/hourly-product-change" @@ -882,7 +946,7 @@ jobs: stream.write(body + "\n") PY - branch="nim-agent/product-dev-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + branch="orchestrator-agent/product-dev-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" git config user.name "opencode-agent[bot]" git config user.email "219766164+opencode-agent[bot]@users.noreply.github.com" git config core.hooksPath /dev/null diff --git a/AGENTS.md b/AGENTS.md index 7620bd2..de8859e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,8 +73,10 @@ ablation evidence. Never use `COPILOT_GITHUB_TOKEN`. ## Automation trust boundary -- The hourly development scheduler uses OpenCode through - `NVIDIA_NIM_API_KEY`; it does not use Copilot Agent Tasks. +- The hourly development scheduler uses OpenCode through a vendored, + pinned-SHA `contextual-orchestrator` gateway pointed at the fail-closed + `orchestrator/free` pool (not a direct provider call); it does not use + Copilot Agent Tasks. - Existing review agents keep their current credential system. Do not repurpose, rename, or broaden those credentials while changing product-development automation. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4639f1a..a690ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,11 @@ Keep a Changelog, and releases use semantic versioning. - Authenticated, side-effect-free LDAP and Active Directory component preflight with LDAPS-only transport, RFC 4514 distinguished-name validation, closed read-only policy, bounded timeouts, and bind-secret redaction. -- An hourly fail-closed NVIDIA NIM OpenCode loop that isolates model credentials, - requires a production-code/test/changelog vertical, independently verifies - the sealed patch, and opens one draft PR through a dedicated publication - token. +- An hourly fail-closed OpenCode loop, routed through the org's vendored, + pinned-SHA `contextual-orchestrator` gateway and its fail-closed zero-cost + `orchestrator/free` pool, that isolates model credentials, requires a + production-code/test/changelog vertical, independently verifies the sealed + patch, and opens one draft PR through a dedicated publication token. - Fail-closed OIDC and Keycloak-OIDC federation preflight with pinned HTTPS endpoints, JWKS signature validation, PKCE `S256`, confidential-client authentication, and RFC 6749 scope validation before desired-state writes. diff --git a/docs/doctoring/hourly-opencode-product-development.md b/docs/doctoring/hourly-opencode-product-development.md index 25dbb16..e843f45 100644 --- a/docs/doctoring/hourly-opencode-product-development.md +++ b/docs/doctoring/hourly-opencode-product-development.md @@ -12,7 +12,7 @@ patch across jobs, and independently re-runs the repository acceptance suite. | Control area | Repository implementation | | --- | --- | -| Least privilege | Read-only default `GITHUB_TOKEN`; upstream NIM and draft-PR publication use separate, step-scoped credentials, and only broker-derived fingerprints cross the patch-scanning boundary. | +| Least privilege | Read-only default `GITHUB_TOKEN`; the five org provider secrets (routed through the vendored, pinned-SHA `contextual-orchestrator` gateway's `orchestrator/free` pool) and draft-PR publication use separate, step-scoped credentials, and only gateway-derived fingerprints cross the patch-scanning boundary. | | Untrusted AI output | No `.git` or GitHub/OIDC credentials in the model workspace; bounded path and patch validation; secrets and common encodings rejected. | | Supply-chain integrity | OpenCode and GitHub Actions are commit/digest pinned; generated patches are SHA-256 sealed and reverified on fresh checkouts. | | Verification | Realistic regression tests, 100% production docstrings, 100% statement and branch coverage, package/deployment validation, and exact-base race checks. | @@ -45,8 +45,17 @@ require a separately scoped assessment and evidence package. configuration remain operational dependencies. - Scheduling and draft-PR creation are not release evidence. - The post-model patch scanner intentionally receives only bounded - `length:sha256` fingerprints for the raw/common encoded NIM credential; it - must never be given the credential again merely to perform leak detection. + `length:sha256` fingerprints for the raw/common encoded forms of whichever + of the five provider secrets are present; it must never be given a + credential again merely to perform leak detection. +- 2026-09-02 update: migrated from a repository-local NVIDIA NIM credential + broker (`scripts/ci/nim_proxy.py`, now removed) to the org's governed + `contextual-orchestrator` gateway, vendored at a pinned commit SHA and + pointed at the fail-closed zero-cost `orchestrator/free` pool, matching the + same pattern already landed in `ContextualWisdomLab/.github`'s central + review workflows and `ContextualWisdomLab/contextual-orchestrator`'s own + hourly maintenance loop. See + `ContextualWisdomLab/keyverse#131`. ## References — APA 7th diff --git a/docs/operations/hourly-product-development.md b/docs/operations/hourly-product-development.md index abb366a..16be869 100644 --- a/docs/operations/hourly-product-development.md +++ b/docs/operations/hourly-product-development.md @@ -7,7 +7,7 @@ repository before a new product slice is considered. | Minute (UTC) | Workflow | Responsibility | | --- | --- | --- | | `17 * * * *` | `hourly-pr-steward.yml` | Update trusted PR branches, require approval and required Checks, then arm exact-head auto-merge. | -| `41 * * * *` | `hourly-product-development.yml` | When the PR queue is empty and exact `main` is healthy, use OpenCode with NVIDIA NIM to produce one bounded buyer-visible draft PR. | +| `41 * * * *` | `hourly-product-development.yml` | When the PR queue is empty and exact `main` is healthy, use OpenCode through the vendored contextual-orchestrator gateway (`orchestrator/free`) to produce one bounded buyer-visible draft PR. | The development scheduler never approves or merges its own work and never publishes a release. The existing review-agent workflows and their credentials @@ -20,8 +20,11 @@ The workflow uses three jobs with different trust levels. 1. **Discover and package.** A model runs as an unprivileged Unix user inside a disposable, credential-free archive of `main`. It may edit only the bounded - product paths. A local broker injects the real NVIDIA credential into a fixed - upstream host; OpenCode receives only a non-secret placeholder key. + product paths. A vendored, pinned-SHA `contextual-orchestrator` gateway holds + the org's five provider credentials in its own process-local KV and serves + the fail-closed zero-cost `orchestrator/free` pool over loopback; OpenCode + receives only an ephemeral, job-scoped local bearer token, never a real + upstream provider key. 2. **Independently reverify.** A fresh checkout validates the textual patch, applies it to the exact base SHA, and runs the complete Keyverse quality, coverage, package, realm, Compose, and template gates without a model. @@ -31,38 +34,50 @@ The workflow uses three jobs with different trust levels. Only a sanitized text patch and bounded PR metadata cross job boundaries. The model workspace has no `.git` directory, GitHub token, Actions OIDC token, -publication token, or upstream NVIDIA key. +publication token, or upstream model-provider key. ## Credentials -### `NVIDIA_NIM_API_KEY` - -This repository secret is available only to the local credential broker. The -broker derives one-way fingerprints for the raw and common encoded forms, -publishes only those fingerprints to the later patch scanner, and then removes -the raw value from its process environment. It is not placed in the OpenCode -process environment. The model process receives -`NVIDIA_API_KEY=keyverse-local-broker` and sends requests to -`http://127.0.0.1:8765/v1`. - -The broker: - -- binds only to IPv4 loopback; -- forwards only bounded GET and POST requests under `/v1`; -- rejects absolute URLs, traversal, nested encoding, encoded separators, and - controls; -- uses a fixed upstream host, `integrate.api.nvidia.com`; -- creates a verified TLS client with TLS 1.2 or newer; -- strips caller-controlled authorization and injects the real key itself; -- suppresses request logging so prompts and responses do not enter Actions - logs; -- limits request size, response size, and concurrent upstream requests. - -The patch guard rejects the raw key and common Base64, URL-safe Base64, and hex -representations from changed files, the generated patch, and PR metadata when -the trusted broker can hold the raw key. The post-model scanner receives only -the broker-derived `length:sha256` fingerprints and hashes candidate -non-whitespace tokens; it never receives the raw key. +### The five org provider secrets + +`BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`, +`OPENROUTER_API_KEY`, and `OPENAI_API_KEY` are available only to the vendored +gateway process, matching the same vendoring pattern already landed in +`ContextualWisdomLab/.github`'s central review workflows and +`ContextualWisdomLab/contextual-orchestrator`'s own hourly maintenance loop. At +least one of the five is required; a missing individual secret is not an +error, and the gateway's own auto-discovery simply skips that provider. The +workflow derives one-way fingerprints for the raw and common encoded forms of +whichever secrets are present, publishes only those fingerprints to the later +patch scanner, and never places any of the five in the OpenCode process +environment. The model process receives only a fresh, job-scoped +`CONTEXTUAL_ORCHESTRATOR_TOKEN` bearer and sends requests to +`http://127.0.0.1:8765/v1`; that bearer authenticates only to this one local +gateway instance for the lifetime of the job and cannot reach any upstream +provider directly. + +The gateway step: + +- clones `contextual-orchestrator` at a pinned commit SHA (the same one + `ContextualWisdomLab/.github`'s central review sidecar already vendors and + trusts) and verifies the checked-out `HEAD` against that pin before + installing anything; +- installs its dependencies with `pip install --require-hashes --no-deps`; +- registers each present provider secret into the gateway's process-local KV + as bootstrap transport only, never read back from the environment again; +- serves the OpenAI-compatible gateway on IPv4 loopback, auto-discovering + live model candidates for the `orchestrator/free` fail-closed zero-cost + pool; +- requires a fresh, per-run bearer token to authenticate any caller, written + to a `chmod 600` file and never exported through `$GITHUB_ENV` directly + (only its file path is), so the raw token cannot land in the workflow's + rendered environment before it is masked. + +The patch guard rejects the raw value and common Base64, URL-safe Base64, and +hex representations of every present provider secret from changed files, the +generated patch, and PR metadata. The post-model scanner receives only the +gateway-derived `length:sha256` fingerprints for each present secret and +hashes candidate non-whitespace tokens; it never receives any raw key. ### `OPENCODE_PRODUCT_DEVELOPMENT_TOKEN` @@ -86,7 +101,7 @@ embedded in a remote URL or written to the repository. A run proceeds only when all of these statements are true. -- `NVIDIA_NIM_API_KEY` is configured. +- At least one of the five org provider secrets is configured. - No open pull request exists, including drafts and dependency updates. - The current `main` SHA can be resolved unambiguously. - The exact `main` SHA has completed successful `ci` and `CodeQL` push runs. @@ -106,16 +121,19 @@ commit, so the scheduler does not fabricate nonexistent post-merge evidence. ## OpenCode isolation OpenCode is installed from a versioned release archive whose SHA-256 digest is -pinned in the workflow. The configured model pool is limited to NVIDIA NIM -models. The project-local `opencode.json` allows reading, editing, searching, -and bounded shell use while denying subagents, web search, web fetch, LSP, and +pinned in the workflow. The configured model is the single fail-closed +zero-cost `contextual_orchestrator_gateway/orchestrator/free` virtual pool, +not a fixed provider model list; the gateway's own auto-discovery and routing +select a live candidate from whichever provider secrets are registered. The +project-local `opencode.json` allows reading, editing, searching, and bounded +shell use while denying subagents, web search, web fetch, LSP, and external-directory access. The agent runs under UID and GID `65532` with `env -i`. Its environment contains only a minimal executable path, an isolated home and temporary directory, the -workspace-local Python import path, deterministic locale settings, the local -broker placeholder key, and OpenCode update suppression. GitHub, Actions OIDC, -and publication credentials are absent. +workspace-local Python import path, deterministic locale settings, the +ephemeral local gateway bearer token, and OpenCode update suppression. GitHub, +Actions OIDC, and publication credentials are absent. The model receives a repository-specific contract requiring: @@ -160,7 +178,8 @@ The guard rejects: unsafe paths; - more than 12 files, 1,500 changed lines, 512 KiB per file, or 2 MiB in total; - malformed or duplicate patch paths; -- a patch or PR message containing the NVIDIA credential or common encodings. +- a patch or PR message containing a present provider credential or common + encodings. The patch receipt records the exact base SHA, changed paths, title, body, and SHA-256 digest. The verification and publication jobs compare this receipt to @@ -195,7 +214,7 @@ No model credential or publication credential is present in this job. Immediately before publication, the workflow repeats the exact-base and zero-open-PR checks, validates the sealed patch digest, and applies the patch to a fresh checkout. It creates one run-unique branch named -`nim-agent/product-dev--` and one draft PR. +`orchestrator-agent/product-dev--` and one draft PR. Workflow concurrency serializes scheduled runs, but GitHub does not provide an atomic compare-base-and-create-PR operation. If another actor opens a PR in the @@ -208,7 +227,9 @@ restore the token. ## First activation 1. Merge the workflow through the normal protected PR path. -2. Configure `NVIDIA_NIM_API_KEY` and the dedicated +2. Configure at least one of the five org provider secrets + (`BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`, + `OPENROUTER_API_KEY`, `OPENAI_API_KEY`) and the dedicated `OPENCODE_PRODUCT_DEVELOPMENT_TOKEN`. 3. While a PR is open, manually dispatch the workflow and confirm that it exits at the queue gate without starting OpenCode. @@ -223,16 +244,19 @@ branch. ## Rotation, revocation, and incident response -Rotate both credentials according to the organization policy and never rotate -review-agent credentials as part of this workflow. A missing or revoked NIM key -stops before authoring. A missing publication token allows no branch or PR -creation and fails the publication step visibly. - -If the NIM broker, OpenCode provider integration, patch guard, or independent -verification behaves unexpectedly, disable this workflow only; do not weaken -branch protection or the existing review system. Preserve the failed run, -identify the trust boundary where the invariant broke, add a regression, and -restore the schedule after exact-head verification. +Rotate credentials according to the organization policy and never rotate +review-agent credentials as part of this workflow. Losing every one of the +five provider secrets stops development before authoring (at least one is +required); revoking or rotating a single provider secret only narrows the +`orchestrator/free` pool's candidates. A missing publication token allows no +branch or PR creation and fails the publication step visibly. + +If the contextual-orchestrator gateway, OpenCode provider integration, patch +guard, or independent verification behaves unexpectedly, disable this +workflow only; do not weaken branch protection or the existing review system. +Preserve the failed run, identify the trust boundary where the invariant +broke, add a regression, and restore the schedule after exact-head +verification. ## Release boundary diff --git a/scripts/ci/nim_proxy.py b/scripts/ci/nim_proxy.py deleted file mode 100644 index f7ba4e3..0000000 --- a/scripts/ci/nim_proxy.py +++ /dev/null @@ -1,361 +0,0 @@ -"""Loopback-only credential broker for NVIDIA NIM agent requests. - -The autonomous model receives a non-secret placeholder key and talks only to -this local server. The broker injects the real NIM credential into a fixed -upstream host, strips caller-controlled authorization, bounds request and -response sizes, and never logs prompt or response content. -""" - -from __future__ import annotations - -import argparse -import http.client -import json -import os -import re -import ssl -import sys -import threading -from collections.abc import Mapping, Sequence -from dataclasses import dataclass -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from typing import Final -from urllib.parse import unquote - -UPSTREAM_HOST: Final = "integrate.api.nvidia.com" -DEFAULT_HOST: Final = "127.0.0.1" -DEFAULT_PORT: Final = 8765 -MAX_REQUEST_BYTES: Final = 16 * 1024 * 1024 -MAX_RESPONSE_BYTES: Final = 32 * 1024 * 1024 -MAX_PATH_CHARACTERS: Final = 4096 -_PATH_RE = re.compile(r"^/v1(?:/[A-Za-z0-9._~!$&'()*+,;=:@%/?-]*)?$") -_INVALID_PERCENT_ESCAPE_RE = re.compile(r"%(?![0-9A-Fa-f]{2})") -_PATH_CONTROL_RE = re.compile(r"[\x00-\x1f\x7f]") -_SAFE_HEADER_RE = re.compile(r"^[\x20-\x7e]{1,512}$") -_REAL_HTTPS_CONNECTION = http.client.HTTPSConnection - - -class ProxyConfigurationError(ValueError): - """Raised when the local proxy cannot enforce its fixed trust boundary.""" - - -class UpstreamProxyError(RuntimeError): - """Raised when the fixed NIM upstream cannot return a bounded response.""" - - -@dataclass(frozen=True, slots=True) -class UpstreamResult: - """A bounded upstream response ready for the loopback HTTP handler.""" - - status: int - reason: str - content_type: str - cache_control: str | None - body: bytes - - -def _safe_header(value: str | None, default: str) -> str: - """Return one bounded visible-ASCII header value or a safe default.""" - if value is None or _SAFE_HEADER_RE.fullmatch(value) is None: - return default - return value - - -def _validate_path(path: str) -> str: - """Return one unambiguous fixed-upstream API target or reject it.""" - if len(path) > MAX_PATH_CHARACTERS or _PATH_RE.fullmatch(path) is None: - raise ProxyConfigurationError("request path is outside the NVIDIA NIM v1 API") - if _INVALID_PERCENT_ESCAPE_RE.search(path) is not None: - raise ProxyConfigurationError("request path contains malformed percent encoding") - - path_component = path.partition("?")[0] - for segment in path_component.split("/"): - try: - decoded = unquote(segment, errors="strict") - except UnicodeDecodeError as exc: - raise ProxyConfigurationError( - "request path contains invalid percent-encoded UTF-8" - ) from exc - routing_segment = decoded.partition(";")[0] - if routing_segment in {".", ".."}: - raise ProxyConfigurationError("request path contains a dot segment") - if any(separator in decoded for separator in ("/", "\\", "%")): - raise ProxyConfigurationError( - "request path contains an encoded separator or nested escape" - ) - if _PATH_CONTROL_RE.search(decoded) is not None: - raise ProxyConfigurationError("request path contains an encoded control") - return path - - -def _open_https_connection(context: ssl.SSLContext) -> http.client.HTTPSConnection: - """Create the verified fixed-host connection or an injected test transport.""" - factory = http.client.HTTPSConnection - if factory is _REAL_HTTPS_CONNECTION: - return factory(UPSTREAM_HOST, 443, timeout=180, context=context) - return factory(UPSTREAM_HOST, 443, timeout=180) - - -class NimUpstreamClient: - """Forward bounded requests to the one configured NVIDIA NIM endpoint.""" - - def __init__(self, api_key: str) -> None: - """Store one non-empty credential without exposing it through repr output.""" - invalid_character = any( - ord(character) < 33 or ord(character) == 127 for character in api_key - ) - if not api_key or invalid_character: - raise ProxyConfigurationError( - "NIM API key is missing or contains unsafe characters" - ) - self._api_key = api_key - - def request( - self, - method: str, - path: str, - body: bytes, - request_headers: Mapping[str, str], - ) -> UpstreamResult: - """Forward one GET or POST and buffer a bounded upstream response.""" - if method not in {"GET", "POST"}: - raise ProxyConfigurationError("only GET and POST requests are supported") - safe_path = _validate_path(path) - if len(body) > MAX_REQUEST_BYTES: - raise ProxyConfigurationError("request body exceeded the proxy byte limit") - - content_type = _safe_header( - request_headers.get("Content-Type"), "application/json" - ) - accept = _safe_header(request_headers.get("Accept"), "application/json") - tls_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) - tls_context.minimum_version = ssl.TLSVersion.TLSv1_2 - connection = _open_https_connection(tls_context) - try: - connection.request( - method, - safe_path, - body=body if method == "POST" else None, - headers={ - "Accept": accept, - "Authorization": f"Bearer {self._api_key}", - "Content-Type": content_type, - "User-Agent": "Keyverse-NIM-Broker/1", - }, - ) - response = connection.getresponse() - response_body = response.read(MAX_RESPONSE_BYTES + 1) - if len(response_body) > MAX_RESPONSE_BYTES: - raise UpstreamProxyError("NIM response exceeded the proxy byte limit") - return UpstreamResult( - status=response.status, - reason=_safe_header(response.reason, "NIM response"), - content_type=_safe_header( - response.getheader("Content-Type"), "application/json" - ), - cache_control=( - _safe_header(response.getheader("Cache-Control"), "no-store") - if response.getheader("Cache-Control") is not None - else None - ), - body=response_body, - ) - except (OSError, http.client.HTTPException) as exc: - raise UpstreamProxyError("NVIDIA NIM upstream request failed") from exc - finally: - connection.close() - - -class NimProxyServer(ThreadingHTTPServer): - """A loopback HTTP server carrying one fixed-upstream NIM client.""" - - daemon_threads = True - allow_reuse_address = True - - def __init__( - self, - address: tuple[str, int], - client: NimUpstreamClient, - max_concurrency: int = 4, - ) -> None: - """Bind only to loopback and initialize a bounded request semaphore.""" - host, _port = address - if host != DEFAULT_HOST: - raise ProxyConfigurationError("NIM broker must bind to IPv4 loopback") - invalid_concurrency = ( - isinstance(max_concurrency, bool) - or not isinstance(max_concurrency, int) - or max_concurrency <= 0 - ) - if invalid_concurrency: - raise ProxyConfigurationError( - "max_concurrency must be a positive integer" - ) - self.client = client - self.request_slots = threading.BoundedSemaphore(max_concurrency) - super().__init__(address, NimProxyHandler) - - -class NimProxyHandler(BaseHTTPRequestHandler): - """Handle loopback health, GET, and POST requests without content logging.""" - - protocol_version = "HTTP/1.1" - server_version = "KeyverseNimBroker/1" - sys_version = "" - - @property - def nim_server(self) -> NimProxyServer: - """Return the typed server instance for this handler.""" - if not isinstance(self.server, NimProxyServer): - raise ProxyConfigurationError("handler is attached to an invalid server") - return self.server - - def log_message(self, _format: str, *_args: object) -> None: - """Suppress default request logging so prompts never enter Actions logs.""" - - def _send(self, status: int, body: bytes, content_type: str) -> None: - """Send one bounded response with explicit anti-cache and framing headers.""" - self.send_response(status) - self.send_header("Content-Type", content_type) - self.send_header("Content-Length", str(len(body))) - self.send_header("Cache-Control", "no-store") - self.send_header("Connection", "close") - self.end_headers() - if self.command != "HEAD": - self.wfile.write(body) - self.close_connection = True - - def _send_error_json(self, status: int, message: str) -> None: - """Send a fixed-shape JSON error without upstream or credential details.""" - payload = json.dumps({"error": message}, separators=(",", ":")).encode() - self._send(status, payload, "application/json") - - def _read_body(self) -> bytes: - """Read a non-chunked body while enforcing the configured byte limit.""" - if self.headers.get("Transfer-Encoding") is not None: - raise ProxyConfigurationError("chunked request bodies are not accepted") - raw_length = self.headers.get("Content-Length") - if self.command == "GET" and raw_length is None: - return b"" - if raw_length is None: - raise ProxyConfigurationError("Content-Length is required") - try: - length = int(raw_length) - except ValueError as exc: - raise ProxyConfigurationError("Content-Length is invalid") from exc - if length < 0 or length > MAX_REQUEST_BYTES: - raise ProxyConfigurationError("request body exceeded the proxy byte limit") - body = self.rfile.read(length) - if len(body) != length: - raise ProxyConfigurationError("request body ended before Content-Length") - return body - - def _forward(self) -> None: - """Forward one bounded request while limiting concurrent upstream calls.""" - try: - path = _validate_path(self.path) - body = self._read_body() - except ProxyConfigurationError as exc: - self._send_error_json(400, str(exc)) - return - - if not self.nim_server.request_slots.acquire(blocking=False): - self._send_error_json(429, "NIM broker concurrency limit reached") - return - try: - result = self.nim_server.client.request( - self.command, - path, - body, - {key: value for key, value in self.headers.items()}, - ) - except (ProxyConfigurationError, UpstreamProxyError): - self._send_error_json(502, "NVIDIA NIM upstream request failed") - return - finally: - self.nim_server.request_slots.release() - - self.send_response(result.status, result.reason) - self.send_header("Content-Type", result.content_type) - self.send_header("Content-Length", str(len(result.body))) - self.send_header("Cache-Control", result.cache_control or "no-store") - self.send_header("Connection", "close") - self.end_headers() - self.wfile.write(result.body) - self.close_connection = True - - def do_GET(self) -> None: - """Serve health locally or forward one bounded NIM GET request.""" - if self.path == "/healthz": - self._send(200, b"ok\n", "text/plain; charset=utf-8") - return - self._forward() - - def do_POST(self) -> None: - """Forward one bounded NIM POST request.""" - self._forward() - - def do_HEAD(self) -> None: - """Return health metadata without a response body.""" - if self.path == "/healthz": - self._send(200, b"ok\n", "text/plain; charset=utf-8") - return - self._send_error_json(405, "method not allowed") - - def do_PUT(self) -> None: - """Reject unsupported mutation methods.""" - self._send_error_json(405, "method not allowed") - - do_PATCH = do_PUT - do_DELETE = do_PUT - do_OPTIONS = do_PUT - - -def create_server( - api_key: str, - *, - host: str = DEFAULT_HOST, - port: int = DEFAULT_PORT, - max_concurrency: int = 4, -) -> NimProxyServer: - """Create a loopback broker with validated address and concurrency settings.""" - if isinstance(port, bool) or not isinstance(port, int) or not 0 <= port <= 65_535: - raise ProxyConfigurationError("port must be an integer from 0 through 65535") - return NimProxyServer( - (host, port), NimUpstreamClient(api_key), max_concurrency=max_concurrency - ) - - -def _parser() -> argparse.ArgumentParser: - """Build the command-line parser for the loopback broker.""" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--host", default=DEFAULT_HOST) - parser.add_argument("--port", type=int, default=DEFAULT_PORT) - parser.add_argument("--api-key-env", default="NIM_UPSTREAM_API_KEY") - parser.add_argument("--check", action="store_true") - return parser - - -def main(argv: Sequence[str] | None = None) -> int: - """Validate configuration and optionally serve until the process is stopped.""" - args = _parser().parse_args(argv) - api_key = os.environ.get(args.api_key_env, "") - try: - server = create_server(api_key, host=args.host, port=args.port) - except (OSError, ProxyConfigurationError) as exc: - print(f"nim proxy: {exc}", file=sys.stderr) - return 2 - if args.check: - server.server_close() - return 0 - try: - server.serve_forever(poll_interval=0.25) - except KeyboardInterrupt: - pass - finally: - server.server_close() - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/services/account_unification/tests/test_hourly_product_development.py b/services/account_unification/tests/test_hourly_product_development.py index eb50768..71791e9 100644 --- a/services/account_unification/tests/test_hourly_product_development.py +++ b/services/account_unification/tests/test_hourly_product_development.py @@ -102,21 +102,34 @@ def test_product_development_keeps_default_repository_permissions_read_only() -> assert "permissions: write-all" not in workflow -def test_product_development_uses_opencode_and_nvidia_nim_not_copilot() -> None: - """Scheduled implementation runs OpenCode against NVIDIA NIM only.""" +def test_product_development_uses_opencode_and_orchestrator_free_not_copilot() -> None: + """Scheduled implementation runs OpenCode against orchestrator/free only.""" workflow = _workflow_source() develop_endpoints = _harden_runner_endpoints("develop-product-gap") assert "OPENCODE_VERSION" in workflow assert "OPENCODE_SHA256" in workflow assert "opencode run" in workflow - assert '"enabled_providers": ["nvidia-nim"]' in workflow + assert '"enabled_providers": ["contextual_orchestrator_gateway"]' in workflow assert '"baseURL": "http://127.0.0.1:8765/v1"' in workflow - assert any( - endpoint == "integrate.api.nvidia.com:443" - for endpoint in develop_endpoints - ) - assert "secrets.NVIDIA_NIM_API_KEY" in workflow + assert 'OPENCODE_MODEL: "contextual_orchestrator_gateway/orchestrator/free"' in workflow + assert '"orchestrator/free":' in workflow + for expected_endpoint in ( + "integrate.api.nvidia.com:443", + "api.openai.com:443", + "openrouter.ai:443", + "api.bytez.com:443", + "models.dev:443", + ): + assert any(endpoint == expected_endpoint for endpoint in develop_endpoints) + for credential_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert f"secrets.{credential_name}" in workflow assert "COPILOT_GITHUB_TOKEN" not in workflow assert "/agents/repos/" not in workflow assert "create_pull_request: true" not in workflow @@ -135,16 +148,23 @@ def test_dependency_install_jobs_allow_exact_python_package_endpoints() -> None: assert any(endpoint == expected_endpoint for endpoint in endpoints) -def test_nim_credential_is_brokered_outside_the_agent_environment() -> None: - """The model receives a placeholder while the real secret stays in the broker.""" +def test_provider_secrets_stay_in_the_gateway_kv_outside_the_agent_environment() -> None: + """The model receives only an ephemeral local bearer; provider secrets stay in the gateway KV.""" workflow = _workflow_source() - assert "Start the loopback-only NIM credential broker" in workflow - assert "NIM_UPSTREAM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow - assert workflow.count("${{ secrets.NVIDIA_NIM_API_KEY }}") == 1 - assert "NVIDIA_API_KEY=keyverse-local-broker" in workflow + assert "Vendor and start the contextual-orchestrator gateway" in workflow + for credential_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert f"{credential_name}: ${{{{ secrets.{credential_name} }}}}" in workflow + assert workflow.count(f"${{{{ secrets.{credential_name} }}}}") == 1 + assert f"{credential_name}=${{{{ secrets.{credential_name} }}}}" not in workflow + assert "CONTEXTUAL_ORCHESTRATOR_TOKEN=\"$gateway_token\"" in workflow assert "env -i" in workflow - assert "NVIDIA_API_KEY=${{ secrets.NVIDIA_NIM_API_KEY }}" not in workflow def test_product_development_does_not_reuse_review_agent_credentials() -> None: @@ -160,20 +180,25 @@ def test_product_development_does_not_reuse_review_agent_credentials() -> None: def test_product_development_fails_closed_without_queue_ownership() -> None: """Unhealthy main or open work stops before entering the model-backed path.""" workflow = _workflow_source() - broker = _step_by_name( + gateway = _step_by_name( "develop-product-gap", - "Start the loopback-only NIM credential broker", - ) - broker_env = broker.get("env") - broker_run = broker.get("run") - - assert isinstance(broker_env, dict) - assert isinstance(broker_run, str) - assert broker.get("if") == "steps.gate.outputs.develop == 'true'" - assert broker_env.get("NIM_UPSTREAM_API_KEY") == ( - "${{ secrets.NVIDIA_NIM_API_KEY }}" + "Vendor and start the contextual-orchestrator gateway", ) - assert "NVIDIA_NIM_API_KEY is required only for model-backed development" in broker_run + gateway_env = gateway.get("env") + gateway_run = gateway.get("run") + + assert isinstance(gateway_env, dict) + assert isinstance(gateway_run, str) + assert gateway.get("if") == "steps.gate.outputs.develop == 'true'" + for credential_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert gateway_env.get(credential_name) == f"${{{{ secrets.{credential_name} }}}}" + assert "is required only for model-backed development" in gateway_run assert "pulls?state=open&per_page=1" in workflow assert "An open pull request exists" in workflow assert "CORE_WORKFLOWS" in workflow @@ -187,8 +212,8 @@ def test_product_development_fails_closed_without_queue_ownership() -> None: def test_agent_runs_in_a_disposable_credential_free_workspace() -> None: """The untrusted model cannot reach GitHub, task tools, or external paths.""" workflow = _workflow_source() - agent_start = workflow.index("Run the NVIDIA NIM development agent") - agent_end = workflow.index("Stop the credential broker", agent_start) + agent_start = workflow.index("Run the orchestrator/free development agent") + agent_end = workflow.index("Stop the gateway", agent_start) agent_block = workflow[agent_start:agent_end] assert "git archive HEAD | tar -x" in workflow @@ -265,7 +290,7 @@ def test_product_workflow_opens_one_draft_pr_without_merge_authority() -> None: assert workflow.count("gh pr create") == 1 assert "--draft" in workflow - assert "nim-agent/product-dev-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" in workflow + assert "orchestrator-agent/product-dev-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" in workflow assert "secrets.OPENCODE_PRODUCT_DEVELOPMENT_TOKEN" in workflow assert "gh pr merge" not in workflow assert "--admin" not in workflow diff --git a/services/account_unification/tests/test_hourly_product_incident_contract.py b/services/account_unification/tests/test_hourly_product_incident_contract.py index bb1bb2f..d5b533e 100644 --- a/services/account_unification/tests/test_hourly_product_incident_contract.py +++ b/services/account_unification/tests/test_hourly_product_incident_contract.py @@ -8,12 +8,16 @@ EXPECTED_ENDPOINTS = { "develop-product-gap": ( + "api.bytez.com:443", "api.github.com:443", + "api.openai.com:443", "cafe.github.com:443", "codeload.github.com:443", "github.com:443", "integrate.api.nvidia.com:443", + "models.dev:443", "objects.githubusercontent.com:443", + "openrouter.ai:443", "raw.githubusercontent.com:443", "registry.npmjs.org:443", "release-assets.githubusercontent.com:443", @@ -160,9 +164,15 @@ def test_deterministic_repository_gates_precede_optional_model_credential() -> N ) positions = tuple(gate_run.index(marker) for marker in ordered_markers) assert positions == tuple(sorted(positions)) - assert "NIM_UPSTREAM_API_KEY" not in gate_env - assert "NIM_UPSTREAM_API_KEY" not in gate_run - assert "NVIDIA_NIM_API_KEY" not in gate_run + for credential_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert credential_name not in gate_env + assert credential_name not in gate_run def test_github_inventory_transport_failures_are_not_false_green() -> None: @@ -230,77 +240,104 @@ def test_default_branch_check_evidence_requires_success() -> None: assert '"skipped"' not in accepted_block -def test_model_fallback_budget_fits_outer_job_timeout() -> None: - """All sequential model candidates plus setup reserve fit the job deadline.""" +def test_single_gateway_attempt_budget_fits_outer_job_timeout() -> None: + """The one orchestrator/free attempt plus setup reserve fits the job deadline. + + Unlike the retired per-model retry loop, `orchestrator/free` is a single + virtual pool id: the gateway's own routing picks a live candidate from + whichever provider secrets are registered, so only one `opencode run` + attempt budget needs to fit, not N sequential model attempts. + """ document = _workflow_document() env = document.get("env") assert isinstance(env, dict) - candidates = str(env.get("OPENCODE_MODEL_CANDIDATES", "")).split() - per_model_seconds = int(str(env.get("OPENCODE_RUN_TIMEOUT_SECONDS", "0"))) + assert env.get("OPENCODE_MODEL") == "contextual_orchestrator_gateway/orchestrator/free" + run_seconds = int(str(env.get("OPENCODE_RUN_TIMEOUT_SECONDS", "0"))) timeout_minutes = int(str(_job("develop-product-gap").get("timeout-minutes", 0))) - assert candidates + assert run_seconds > 0 setup_and_packaging_reserve_seconds = 15 * 60 assert timeout_minutes * 60 >= ( - len(candidates) * per_model_seconds + setup_and_packaging_reserve_seconds + run_seconds + setup_and_packaging_reserve_seconds ) -def test_nvidia_secret_is_materialized_only_by_broker() -> None: - """The raw NVIDIA secret exists only in the conditional loopback broker step.""" - secret_expression = "${{ secrets.NVIDIA_NIM_API_KEY }}" - materializing_steps: list[str] = [] - for step in _steps("develop-product-gap"): - env = step.get("env") - if not isinstance(env, dict) or secret_expression not in env.values(): - continue - name = step.get("name") - assert isinstance(name, str) - materializing_steps.append(name) - - assert materializing_steps == ["Start the loopback-only NIM credential broker"] - - -def test_nvidia_secret_fingerprint_crosses_the_broker_boundary() -> None: - """Packaging receives only broker-derived fingerprints for leak scanning.""" - broker = _step_by_name( +def test_provider_secrets_are_materialized_only_by_the_gateway_step() -> None: + """Each raw provider secret exists only in the conditional gateway step.""" + gateway_step_name = "Vendor and start the contextual-orchestrator gateway" + for credential_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + secret_expression = f"${{{{ secrets.{credential_name} }}}}" + materializing_steps: list[str] = [] + for step in _steps("develop-product-gap"): + env = step.get("env") + if not isinstance(env, dict) or secret_expression not in env.values(): + continue + name = step.get("name") + assert isinstance(name, str) + materializing_steps.append(name) + assert materializing_steps == [gateway_step_name] + + +def test_provider_secret_fingerprints_cross_the_gateway_boundary() -> None: + """Packaging receives only gateway-derived fingerprints for leak scanning.""" + gateway = _step_by_name( "develop-product-gap", - "Start the loopback-only NIM credential broker", + "Vendor and start the contextual-orchestrator gateway", ) package = _step_by_name( "develop-product-gap", "Capture the bounded credential-free patch", ) - broker_run = broker.get("run") + gateway_run = gateway.get("run") package_env = package.get("env") - assert isinstance(broker_run, str) + assert isinstance(gateway_run, str) assert isinstance(package_env, dict) - assert broker.get("id") == "nim_broker" - assert "sha256" in broker_run - assert "GITHUB_OUTPUT" in broker_run - assert broker_run.index("unset NIM_UPSTREAM_API_KEY") > broker_run.index( - "python scripts/ci/nim_proxy.py" + assert gateway.get("id") == "orchestrator_gateway" + assert "sha256" in gateway_run + assert "GITHUB_OUTPUT" in gateway_run + assert gateway_run.index("unset gateway_token") > gateway_run.index( + "scripts.ci.serve_seeded_gateway" ) assert package_env.get("KEYVERSE_FORBIDDEN_SECRET_FINGERPRINT") == ( - "${{ steps.nim_broker.outputs.secret_fingerprint }}" - ) - assert "KEYVERSE_FORBIDDEN_SECRET: ${{ secrets.NVIDIA_NIM_API_KEY }}" not in ( - _workflow_source() + "${{ steps.orchestrator_gateway.outputs.secret_fingerprint }}" ) + for credential_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert f"KEYVERSE_FORBIDDEN_SECRET: ${{{{ secrets.{credential_name} }}}}" not in ( + _workflow_source() + ) -def test_nvidia_secret_is_required_only_on_the_model_backed_path() -> None: - """The NVIDIA secret is checked only after deterministic gates select development.""" - broker = _step_by_name( +def test_provider_secrets_are_required_only_on_the_model_backed_path() -> None: + """Provider secrets are checked only after deterministic gates select development.""" + gateway = _step_by_name( "develop-product-gap", - "Start the loopback-only NIM credential broker", + "Vendor and start the contextual-orchestrator gateway", ) - broker_env = broker.get("env") - broker_run = broker.get("run") - assert isinstance(broker_env, dict) - assert isinstance(broker_run, str) - - assert broker_env.get("NIM_UPSTREAM_API_KEY") == "${{ secrets.NVIDIA_NIM_API_KEY }}" - assert 'if [ -z "${NIM_UPSTREAM_API_KEY:-}" ]; then' in broker_run - assert "NVIDIA_NIM_API_KEY is required only for model-backed development" in broker_run - assert "exit 1" in broker_run + gateway_env = gateway.get("env") + gateway_run = gateway.get("run") + assert isinstance(gateway_env, dict) + assert isinstance(gateway_run, str) + + for credential_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert gateway_env.get(credential_name) == f"${{{{ secrets.{credential_name} }}}}" + assert 'if [ "$provider_secret_count" -lt 1 ]; then' in gateway_run + assert "is required only for model-backed development" in gateway_run + assert "exit 1" in gateway_run diff --git a/services/account_unification/tests/test_nim_proxy.py b/services/account_unification/tests/test_nim_proxy.py deleted file mode 100644 index 17880e8..0000000 --- a/services/account_unification/tests/test_nim_proxy.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Security-boundary tests for the loopback NVIDIA NIM credential broker.""" -from __future__ import annotations - -import importlib.util -import sys -from pathlib import Path -from types import ModuleType - -import pytest - - -def _repository_root() -> Path: - """Return the Keyverse repository root from this test module.""" - return Path(__file__).resolve().parents[3] - - -def _load_proxy() -> ModuleType: - """Load the repository-local proxy without making scripts a package.""" - path = _repository_root() / "scripts" / "ci" / "nim_proxy.py" - spec = importlib.util.spec_from_file_location("keyverse_nim_proxy", path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - try: - spec.loader.exec_module(module) - finally: - sys.modules.pop(spec.name, None) - return module - - -def test_proxy_accepts_only_unambiguous_nim_v1_paths() -> None: - """Absolute URLs, traversal, nested escapes, and controls never reach NIM.""" - proxy = _load_proxy() - - assert proxy._validate_path("/v1/chat/completions") == "/v1/chat/completions" - assert proxy._validate_path("/v1/models?limit=10") == "/v1/models?limit=10" - - unsafe_paths = ( - "https://attacker.example/v1/chat/completions", - "/v1/../secrets", - "/v1/%2e%2e/secrets", - "/v1/%252fsecrets", - "/v1/%2fsecrets", - "/v1/%00", - "/v2/chat/completions", - "/healthz?forward=true", - ) - for path in unsafe_paths: - with pytest.raises(proxy.ProxyConfigurationError): - proxy._validate_path(path) - - -def test_proxy_rejects_missing_or_unsafe_credentials() -> None: - """The broker refuses empty, whitespace-bearing, or control-bearing keys.""" - proxy = _load_proxy() - - for api_key in ("", "contains space", "line\nbreak", "tab\tvalue", "del\x7f"): - with pytest.raises(proxy.ProxyConfigurationError): - proxy.NimUpstreamClient(api_key) - - client = proxy.NimUpstreamClient("valid-nim-key") - assert "valid-nim-key" not in repr(client) - - -def test_proxy_binds_only_to_ipv4_loopback_and_bounds_concurrency() -> None: - """The credential broker cannot listen on an externally reachable address.""" - proxy = _load_proxy() - - server = proxy.create_server("valid-nim-key", host="127.0.0.1", port=0) - try: - assert server.server_address[0] == "127.0.0.1" - finally: - server.server_close() - - for host in ("0.0.0.0", "::1", "localhost"): - with pytest.raises(proxy.ProxyConfigurationError): - proxy.create_server("valid-nim-key", host=host, port=0) - for concurrency in (0, -1, True): - with pytest.raises(proxy.ProxyConfigurationError): - proxy.create_server( - "valid-nim-key", - host="127.0.0.1", - port=0, - max_concurrency=concurrency, - ) - - -def test_proxy_sanitizes_forwarded_header_values() -> None: - """Untrusted upstream and caller header text cannot inject response headers.""" - proxy = _load_proxy() - - assert proxy._safe_header("application/json", "fallback") == "application/json" - assert proxy._safe_header("bad\r\nInjected: yes", "fallback") == "fallback" - assert proxy._safe_header(None, "fallback") == "fallback" - assert proxy._safe_header("x" * 513, "fallback") == "fallback" From cfb2fe23d0b6487f2815f21c1dbfcefd23ccfb6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:19:50 +0900 Subject: [PATCH 2/6] fix(workflow): refresh contextual-orchestrator pin --- .github/workflows/hourly-product-development.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 4318210..57432bf 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -39,7 +39,7 @@ env: # Pinned to the exact commit ContextualWisdomLab/.github's central review # sidecar (scripts/ci/contextual_orchestrator_review_sidecar.sh) already # vendors and trusts. - ORCHESTRATOR_PIN_SHA: "045d17da5e2aea56a97e241ee158ab1628d78660" + ORCHESTRATOR_PIN_SHA: "464da4715b495b5eaaa593eba3796e2d976ee0c9" jobs: develop-product-gap: From 0ce1ef140b6720f50826ea17c4ed45c5bb0b9813 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:19:52 +0900 Subject: [PATCH 3/6] test(hourly): bind all provider keys to leak fingerprints --- ...rly_gateway_secret_fingerprint_contract.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 services/account_unification/tests/test_hourly_gateway_secret_fingerprint_contract.py diff --git a/services/account_unification/tests/test_hourly_gateway_secret_fingerprint_contract.py b/services/account_unification/tests/test_hourly_gateway_secret_fingerprint_contract.py new file mode 100644 index 0000000..e33deaa --- /dev/null +++ b/services/account_unification/tests/test_hourly_gateway_secret_fingerprint_contract.py @@ -0,0 +1,32 @@ +"""Regression contracts for multi-provider secret leak fingerprints.""" + +from __future__ import annotations + +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +WORKFLOW_PATH = REPOSITORY_ROOT / ".github" / "workflows" / "hourly-product-development.yml" +PROVIDER_SECRET_NAMES = ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", +) + + +def test_gateway_fingerprints_every_registered_provider_secret() -> None: + """Keep every provider credential inside the patch-leak denylist input.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + start = " secret_fingerprint=\"$(\n" + end = " printf 'secret_fingerprint=%s\\n' \"$secret_fingerprint\" >>\"$GITHUB_OUTPUT\"\n" + assert workflow.count(start) == 1 + assert workflow.count(end) == 1 + fingerprint_block = workflow.split(start, 1)[1].split(end, 1)[0] + + for secret_name in PROVIDER_SECRET_NAMES: + assert f' \"{secret_name}\",' in fingerprint_block + assert "hashlib.sha256(value).hexdigest()" in fingerprint_block + assert "base64.b64encode(secret)" in fingerprint_block + assert "base64.urlsafe_b64encode(secret)" in fingerprint_block + assert "secret.hex().encode(\"ascii\")" in fingerprint_block From e862cffa4f82f09bc4ab18296cffb482eacc40de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 03:20:31 +0900 Subject: [PATCH 4/6] docs(doctoring): trace governed gateway revision --- docs/doctoring/hourly-opencode-product-development.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/doctoring/hourly-opencode-product-development.md b/docs/doctoring/hourly-opencode-product-development.md index e843f45..a21fd16 100644 --- a/docs/doctoring/hourly-opencode-product-development.md +++ b/docs/doctoring/hourly-opencode-product-development.md @@ -75,6 +75,10 @@ mitigating the risk of software vulnerabilities* (NIST SP 800-218 Rev. 1, Initial Public Draft). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218r1.ipd +ContextualWisdomLab. (2026). *contextual-orchestrator* (Revision +464da4715b495b5eaaa593eba3796e2d976ee0c9) [Computer software]. GitHub. +https://github.com/ContextualWisdomLab/contextual-orchestrator/tree/464da4715b495b5eaaa593eba3796e2d976ee0c9 + GitHub. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 5, 2026, from https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions From 601ab3dfa1adba03003202b9ad0be3267212ff03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 10:34:07 +0900 Subject: [PATCH 5/6] test(hourly): enforce single bounded agent run Signed-off-by: Seongho Bae --- .../tests/test_hourly_product_incident_contract.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/account_unification/tests/test_hourly_product_incident_contract.py b/services/account_unification/tests/test_hourly_product_incident_contract.py index d5b533e..3ddc6ed 100644 --- a/services/account_unification/tests/test_hourly_product_incident_contract.py +++ b/services/account_unification/tests/test_hourly_product_incident_contract.py @@ -254,8 +254,12 @@ def test_single_gateway_attempt_budget_fits_outer_job_timeout() -> None: assert env.get("OPENCODE_MODEL") == "contextual_orchestrator_gateway/orchestrator/free" run_seconds = int(str(env.get("OPENCODE_RUN_TIMEOUT_SECONDS", "0"))) timeout_minutes = int(str(_job("develop-product-gap").get("timeout-minutes", 0))) + agent_run = _step_by_id("develop-product-gap", "agent").get("run") + assert isinstance(agent_run, str) assert run_seconds > 0 + assert agent_run.count("opencode run") == 1 + assert 'timeout --kill-after=30s "${OPENCODE_RUN_TIMEOUT_SECONDS}s"' in agent_run setup_and_packaging_reserve_seconds = 15 * 60 assert timeout_minutes * 60 >= ( run_seconds + setup_and_packaging_reserve_seconds From bae115eab000fa8a5460fbf4eb8156a08905df1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 06:46:11 +0900 Subject: [PATCH 6/6] fix(ci): remove stale hourly-pr-steward test left behind by #140 the org-wide pr-review-merge-scheduler.yml, which already dispatches in real time on every PR event) but did not remove its own static contract test, which asserts on that workflow file's now-nonexistent content. The test fails closed with FileNotFoundError, breaking the required account-unification-tests check on main and on every PR -- including ones with no relation to the removal -- since GitHub's pull_request checkout tests against the current base branch, which already lacks the file even when a PR's own branch still has it. Also updates docs/operations/hourly-product-development.md, which still described the removed hourly steward alongside the surviving hourly-product-development.yml as if both ran on offset schedules. Verified: full account-unification suite passes (coverage 100%, ruff, interrogate, compileall, and the repository documentation contract test all clean). Co-Authored-By: Claude Sonnet 5 --- docs/operations/hourly-product-development.md | 9 ++- .../tests/test_hourly_pr_steward.py | 78 ------------------- 2 files changed, 6 insertions(+), 81 deletions(-) delete mode 100644 services/account_unification/tests/test_hourly_pr_steward.py diff --git a/docs/operations/hourly-product-development.md b/docs/operations/hourly-product-development.md index 16be869..f7d1df1 100644 --- a/docs/operations/hourly-product-development.md +++ b/docs/operations/hourly-product-development.md @@ -1,12 +1,15 @@ # Hourly product-development loop Keyverse separates protected pull-request maintenance from autonomous product -development. The schedules are offset so the merge loop has time to settle the -repository before a new product slice is considered. +development. Protected PR maintenance (updating trusted PR branches, requiring +approval and required Checks, then arming exact-head auto-merge) is owned by +the organization's central `pr-review-merge-scheduler.yml`, which dispatches +in real time on every PR event rather than on an hourly schedule — Keyverse's +own former hourly steward workflow provided no security boundary beyond that +already-required central scheduler and was removed (#140). | Minute (UTC) | Workflow | Responsibility | | --- | --- | --- | -| `17 * * * *` | `hourly-pr-steward.yml` | Update trusted PR branches, require approval and required Checks, then arm exact-head auto-merge. | | `41 * * * *` | `hourly-product-development.yml` | When the PR queue is empty and exact `main` is healthy, use OpenCode through the vendored contextual-orchestrator gateway (`orchestrator/free`) to produce one bounded buyer-visible draft PR. | The development scheduler never approves or merges its own work and never diff --git a/services/account_unification/tests/test_hourly_pr_steward.py b/services/account_unification/tests/test_hourly_pr_steward.py deleted file mode 100644 index 910133e..0000000 --- a/services/account_unification/tests/test_hourly_pr_steward.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Static contract tests for the hourly protected PR steward.""" -from __future__ import annotations - -from pathlib import Path - - -def _workflow_source() -> str: - """Return the repository's hourly PR stewardship workflow source.""" - repository_root = Path(__file__).resolve().parents[3] - return ( - repository_root / ".github" / "workflows" / "hourly-pr-steward.yml" - ).read_text(encoding="utf-8") - - -def _permissions_block(source: str, marker: str, terminator: str) -> str: - """Return one indentation-sensitive workflow permissions block.""" - block_start = source.index(marker) - block_end = source.index(terminator, block_start) - return source[block_start:block_end] - - -def test_hourly_steward_runs_once_per_hour_with_bounded_concurrency() -> None: - """The schedule is hourly and overlapping steward runs are serialized.""" - workflow = _workflow_source() - assert 'cron: "17 * * * *"' in workflow - assert "group: hourly-pr-steward" in workflow - assert "cancel-in-progress: false" in workflow - assert "timeout-minutes: 10" in workflow - - -def test_hourly_steward_uses_read_only_workflow_token_defaults() -> None: - """Only the steward job receives its narrowly required write scopes.""" - workflow = _workflow_source() - top_level_permissions = _permissions_block( - workflow, - "permissions:\n", - "\nconcurrency:", - ) - job_permissions = _permissions_block( - workflow, - " permissions:\n", - " steps:", - ) - - assert "contents: read" in top_level_permissions - assert "write" not in top_level_permissions - assert "contents: write" in job_permissions - assert "pull-requests: write" in job_permissions - assert "checks: read" in job_permissions - assert "security-events: write" not in workflow - assert "actions: write" not in workflow - - -def test_hourly_steward_is_fail_closed_on_trust_review_and_checks() -> None: - """Untrusted, unapproved, pending, or failed pull requests remain untouched.""" - workflow = _workflow_source() - assert 'head_owner" != "ContextualWisdomLab"' in workflow - assert 'trusted_author" != "true"' in workflow - assert 'review_decision" != "APPROVED"' in workflow - assert 'gh pr checks "$number" --repo "$REPOSITORY" --required' in workflow - assert "--admin" not in workflow - - -def test_hourly_steward_invalidates_old_evidence_after_branch_update() -> None: - """A branch update exits the current iteration before merging stale evidence.""" - workflow = _workflow_source() - update_position = workflow.index("gh pr update-branch") - continue_position = workflow.index("continue", update_position) - approval_position = workflow.index('review_decision" != "APPROVED"') - assert update_position < continue_position < approval_position - - -def test_hourly_steward_binds_auto_merge_to_the_checked_head() -> None: - """GitHub auto-merge is armed only for the enumerated exact head SHA.""" - workflow = _workflow_source() - assert '--auto \\' in workflow - assert '--squash \\' in workflow - assert '--match-head-commit "$head_sha"' in workflow