diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 0a15d1d..1c60683 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -1,4 +1,4 @@ -name: Hourly product development +name: Product development dispatch on: workflow_dispatch: @@ -8,10 +8,6 @@ on: required: false default: false type: boolean - schedule: - # Offset from the PR steward at minute 17 so protected merge evidence settles. - - cron: "41 * * * *" - concurrency: group: hourly-product-development-${{ github.repository }} cancel-in-progress: false @@ -25,13 +21,7 @@ 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 - OPENCODE_RUN_TIMEOUT_SECONDS: "2100" - NIM_PROXY_HOST: "127.0.0.1" - NIM_PROXY_PORT: "8765" + CONTEXTUAL_ORCHESTRATOR_POOL: free jobs: develop-product-gap: @@ -55,11 +45,14 @@ jobs: disable-telemetry: true allowed-endpoints: >- api.github.com:443 + api.bytez.com:443 + api.openai.com:443 cafe.github.com:443 codeload.github.com:443 github.com:443 integrate.api.nvidia.com:443 objects.githubusercontent.com:443 + openrouter.ai:443 raw.githubusercontent.com:443 registry.npmjs.org:443 release-assets.githubusercontent.com:443 @@ -305,12 +298,22 @@ jobs: - name: Check out the protected default branch without persisted credentials if: steps.gate.outputs.develop == 'true' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: main fetch-depth: 1 persist-credentials: false + - name: Check out immutable central automation source + if: steps.gate.outputs.develop == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ContextualWisdomLab/.github + ref: dcd35b7653854edb2ea26a87bac2035f12d8d903 + fetch-depth: 1 + persist-credentials: false + path: trusted-organization-source + - name: Set up Python if: steps.gate.outputs.develop == 'true' uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -407,7 +410,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 provider credential. Before finishing, run focused tests and the available local quality gates with the preinstalled environment. Update CHANGELOG.md [Unreleased], beginner- @@ -421,87 +424,76 @@ jobs: required checks. Do not publish a release. PROMPT - - name: Start the loopback-only NIM credential broker + - name: Provision the pinned contextual-orchestrator sidecar if: steps.gate.outputs.develop == 'true' - id: nim_broker + id: orchestrator 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." - 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 secret_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + secret = os.environ.get(secret_name, "").encode("utf-8") + if not secret: + continue + for value in ( + secret, + base64.b64encode(secret), + base64.urlsafe_b64encode(secret), + secret.hex().encode("ascii"), + ): + fingerprints.append( + f"{len(value)}:{hashlib.sha256(value).hexdigest()}" + ) + print(",".join(fingerprints)) PY )" printf 'secret_fingerprint=%s\n' "$secret_fingerprint" >>"$GITHUB_OUTPUT" - 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 - - ready=false - for _attempt in $(seq 1 30); do - if curl -fsS \ - "http://${NIM_PROXY_HOST}:${NIM_PROXY_PORT}/healthz" \ - >/dev/null; then - ready=true - break - fi - sleep 1 - done - if [ "$ready" != "true" ]; then - cat "$proxy_log" >&2 - echo "::error::The loopback NIM credential broker did not become ready." - exit 1 - fi + bash "$GITHUB_WORKSPACE/trusted-organization-source/scripts/ci/contextual_orchestrator_review_sidecar.sh" - - name: Run the NVIDIA NIM development agent in a disposable workspace + - name: Run OpenCode through contextual-orchestrator in a disposable workspace if: steps.gate.outputs.develop == 'true' id: agent shell: bash + env: + MODEL: contextual-orchestrator/orchestrator/free run: | set -euo pipefail trap 'sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true' EXIT prompt="$(cat "${RUNNER_TEMP}/keyverse-agent-prompt.md")" - 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" - - cat >"${agent_workspace}/opencode.json" <<'CONFIG' + 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" + install -m 0644 \ + "$GITHUB_WORKSPACE/trusted-organization-source/scripts/ci/load_contextual_orchestrator_token.sh" \ + "$agent_home/load_contextual_orchestrator_token.sh" + + cat >"${agent_workspace}/opencode.json" <<'CONFIG' { "$schema": "https://opencode.ai/config.json", - "enabled_providers": ["nvidia-nim"], + "model": "contextual-orchestrator/orchestrator/free", + "small_model": "contextual-orchestrator/orchestrator/free", + "enabled_providers": ["contextual-orchestrator"], "lsp": false, "mcp": {}, "permission": { @@ -518,28 +510,19 @@ jobs: "external_directory": "deny" }, "provider": { - "nvidia-nim": { + "contextual-orchestrator": { "npm": "@ai-sdk/openai-compatible", - "name": "NVIDIA NIM through local Keyverse broker", + "name": "Contextual Orchestrator", "options": { - "baseURL": "http://127.0.0.1:8765/v1", - "apiKey": "{env:NVIDIA_API_KEY}" + "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", + "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": "Orchestrator Free", "tool_call": true, - "limit": {"context": 131072, "output": 8192} + "reasoning": true, + "limit": {"context": 200000, "output": 32768} } } } @@ -547,61 +530,57 @@ 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 pkill -KILL -u 65532 >/dev/null 2>&1 || true - echo "::endgroup::" - echo "::warning::Model $model failed; its disposable workspace was discarded." - done - - if [ -z "$successful_workspace" ]; then - echo "::error::Every NVIDIA NIM model candidate failed." + if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || \ + [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then + echo "::error::The contextual-orchestrator sidecar did not export its connection files." exit 1 fi - sudo chown -R "$(id -u):$(id -g)" "$successful_workspace" - rm -f "$successful_workspace/opencode.json" - echo "workspace=$successful_workspace" >>"$GITHUB_OUTPUT" + sudo chown -R 65532:65532 "$agent_workspace" "$agent_home" + sudo chown 65532:65532 "$CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE" + 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 \ + OPENCODE_DISABLE_AUTOUPDATE=1 \ + MODEL="$MODEL" \ + CONTEXTUAL_ORCHESTRATOR_BASE_URL="$CONTEXTUAL_ORCHESTRATOR_BASE_URL" \ + CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE="$CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE" \ + /bin/bash -c \ + 'set -euo pipefail; source "$HOME/load_contextual_orchestrator_token.sh"; ulimit -u 256; ulimit -n 1024; cd "$1"; exec opencode run "$2" --model "$MODEL"' \ + bash "$agent_workspace" "$prompt" + sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true + sudo chown -R "$(id -u):$(id -g)" "$agent_workspace" + rm -f "$agent_workspace/opencode.json" + echo "workspace=$agent_workspace" >>"$GITHUB_OUTPUT" - - name: Stop the credential broker and all model descendants + - name: Stop contextual-orchestrator and remove credential material 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 - fi + sidecar_pattern="${RUNNER_TEMP}/contextual-orchestrator-review/launch_sidecar.py" + sanitizer_pattern="$GITHUB_WORKSPACE/trusted-organization-source/scripts/ci/sanitize_contextual_orchestrator_sidecar_stream.py" + pkill -KILL -f "$sidecar_pattern" >/dev/null 2>&1 || true + pkill -KILL -f "$sanitizer_pattern" >/dev/null 2>&1 || true + sudo rm -f -- "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-/nonexistent}" + rm -rf -- \ + "$GITHUB_WORKSPACE/trusted-organization-source" \ + "$GITHUB_WORKSPACE/strix_runs" \ + "$RUNNER_TEMP/contextual-orchestrator" \ + "$RUNNER_TEMP/contextual-orchestrator-review" - name: Capture the bounded credential-free patch if: steps.gate.outputs.develop == 'true' id: package shell: bash env: - KEYVERSE_FORBIDDEN_SECRET_FINGERPRINT: ${{ steps.nim_broker.outputs.secret_fingerprint }} + KEYVERSE_FORBIDDEN_SECRET_FINGERPRINT: ${{ steps.orchestrator.outputs.secret_fingerprint }} run: | set -euo pipefail artifact_dir="${RUNNER_TEMP}/hourly-product-change" @@ -882,7 +861,7 @@ jobs: stream.write(body + "\n") PY - branch="nim-agent/product-dev-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + branch="opencode-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/CHANGELOG.md b/CHANGELOG.md index 4639f1a..7a3a582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ 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, +- A centrally dispatched fail-closed OpenCode loop using the immutable contextual-orchestrator `orchestrator/free` sidecar, isolating provider credentials, requires a production-code/test/changelog vertical, independently verifies the sealed patch, and opens one draft PR through a dedicated publication token. diff --git a/docs/TRD.md b/docs/TRD.md index a252230..21e71ea 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -83,7 +83,7 @@ Readiness is component/lifecycle specific. Preflight success does not imply Keyc ## 12. Automation boundary -Autonomous development uses NVIDIA NIM/OpenCode through an isolated model phase. Model execution has no publication/reviewer/release authority. PR #74 is integrated in protected main; its operational boundary must still be proven by a protected-main scheduled or manual run. +Autonomous development uses OpenCode through the central contextual-orchestrator `orchestrator/free` sidecar in an isolated model phase. Model execution has no publication/reviewer/release authority. PR #74 is integrated in protected main; its operational boundary must still be proven by a protected-main centrally dispatched or manual run. ## 13. Change control diff --git a/docs/operations/hourly-product-development.md b/docs/operations/hourly-product-development.md index abb366a..eeebfc6 100644 --- a/docs/operations/hourly-product-development.md +++ b/docs/operations/hourly-product-development.md @@ -1,245 +1,130 @@ -# Hourly product-development loop +# Centrally dispatched product development 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. The organization readiness loop owns the hourly cadence and +dispatches this repository workflow only when Keyverse has no open pull request +or active writer. The repository workflow exposes `workflow_dispatch` only; it +does not maintain a second timer or organization queue sweep. -| 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. | - -The development scheduler never approves or merges its own work and never -publishes a release. The existing review-agent workflows and their credentials -remain unchanged. Review, repair, revalidation, and merge stay owned by the -normal protected PR path. +The development workflow never approves or merges its own work and never +publishes a release. Review, repair, exact-head revalidation, and merge remain +owned by the normal protected PR path. ## Architecture 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. +1. **Discover and package.** OpenCode runs as an unprivileged Unix user in a + disposable archive of `main`. A central, commit-pinned + contextual-orchestrator sidecar selects the `orchestrator/free` route and + keeps upstream provider credentials outside the model process. 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. -3. **Publish.** A second fresh checkout verifies the sealed patch hash and base - SHA again, then uses a dedicated publication credential to create one branch - and one draft PR. +3. **Publish.** Another fresh checkout verifies the sealed patch hash and base + SHA, then uses a dedicated publication credential to create one branch and + one draft PR. 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 provider credential. ## 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. - -### `OPENCODE_PRODUCT_DEVELOPMENT_TOKEN` - -This dedicated fine-grained token is used only in the final publication step. -It must be scoped to `ContextualWisdomLab/keyverse` with the minimum permissions -needed to create a branch and draft pull request: - -- **Contents:** read and write; -- **Pull requests:** read and write; -- **Metadata:** read. - -Do not reuse `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, CodeRabbit -credentials, or any review-agent secret. Development publication and review -approval are separate trust domains. - -The normal `GITHUB_TOKEN` remains read-only throughout the workflow. The -publication token is supplied through `GIT_ASKPASS` and `GH_TOKEN`; it is not -embedded in a remote URL or written to the repository. +The sidecar bootstrap step may receive configured Bytez, NVIDIA NIM, +OpenRouter, and OpenAI repository secrets. The pinned central script chooses an +available upstream for `orchestrator/free`, starts a loopback-only proxy, and +writes a short-lived token to a mode-0600 file. OpenCode receives only the +loopback URL and token-file path; an isolated loader reads the token after the +process drops privileges. Raw upstream credentials never enter its environment, +command line, workspace, patch, or PR metadata. + +The bootstrap step derives one-way fingerprints for every configured provider +credential and passes only those fingerprints to the patch scanner. The guard +rejects raw and commonly encoded credential forms from changed files, the +generated patch, and PR metadata. Cleanup kills sidecar descendants and removes +the token file and temporary central checkout. + +`OPENCODE_PRODUCT_DEVELOPMENT_TOKEN` is a dedicated fine-grained token used +only by the final publication job. Scope it to +`ContextualWisdomLab/keyverse` with Contents read/write, Pull requests +read/write, and Metadata read. Do not reuse review or approval credentials. ## Eligibility gate -A run proceeds only when all of these statements are true. - -- `NVIDIA_NIM_API_KEY` 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. -- The latest check run for every observed app/name pair is complete with a - `success` conclusion. Optional neutral/skipped checks are not treated as - required evidence by this gate. -- The workflow is not a `dry_run` invocation. +A run proceeds only when all of these statements are true: -Failure to list or parse any required GitHub response stops development. The -scheduler does not infer health from older commits or from a similarly named -check on another SHA. +- the central readiness loop selected Keyverse, or an operator dispatched it; +- no open pull request exists, including drafts and dependency updates; +- the current `main` SHA resolves unambiguously; +- the exact `main` SHA has successful `ci` and `CodeQL` push runs; +- the latest observed check for every app/name pair completed successfully; +- the invocation is not `dry_run`. -`Security Scan` and `SAST Semgrep` remain protected exact-head PR merge gates. -They do not currently emit a second push run for the squash-generated `main` -commit, so the scheduler does not fabricate nonexistent post-merge evidence. +Failure to list or parse required GitHub evidence stops development. The +workflow does not infer health from older commits or similarly named checks on +another SHA. ## 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 -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. - -The model receives a repository-specific contract requiring: - -- exactly one highest-impact buyer-visible product gap; -- Superpowers design, test-driven development, systematic debugging, and - verification-before-completion; -- a real failing regression before production implementation; -- realistic OIDC, SAML, SCIM, merge, outage, concurrency, migration, or - deployment cases where applicable; -- 100% production docstring, statement, and branch coverage; -- standalone and CWL/Naruon module compatibility; -- current authoritative standards or primary research documented in APA 7th - style; -- two-word-or-longer snake_case database object names; -- `contextual-orchestrator` only when a model is genuinely necessary; -- Figma or Product Design only for an actual user-interface slice; -- no merge, approval, release, dependency change, workflow change, or secret - disclosure. - -Repository prose, issues, comments, fixtures, provider metadata, generated -files, payloads, and fetched references are treated as untrusted data rather -than instructions. +OpenCode is installed from a versioned archive with a pinned SHA-256 digest. +Its local configuration enables only the central +`contextual-orchestrator/orchestrator/free` model. Provider discovery and +fallback stay inside contextual-orchestrator instead of repository YAML. There +is no local model-candidate loop or per-model timeout. -## Patch boundary +The agent runs under UID and GID `65532` with `env -i`. It may read, edit, +search, and run bounded local commands, but cannot use subagents, web access, +LSP, external directories, GitHub credentials, or publication authority. -The autonomous workspace may change only: +## Patch boundary -- `services/account_unification/app/**`; -- `services/account_unification/tests/**`; -- `services/account_unification/tools/**`; -- `deploy/templates/**`; -- `docs/**`; -- `README.md`; -- `CHANGELOG.md`. +The autonomous workspace may change only product source, tests, tools, +deployment templates, documentation, `README.md`, and `CHANGELOG.md`. The guard +rejects workflow and policy changes, dependencies, locks, secrets, deletions, +renames, links, executables, binary data, mode changes, unsafe paths, more than +12 files, more than 1,500 changed lines, files above 512 KiB, or a patch above +2 MiB. -The guard rejects: +The receipt records the exact base SHA, changed paths, title, body, and SHA-256 +digest. Verification and publication compare that receipt with the downloaded +artifact before applying it. -- deletions and renames; -- `.github`, `.git`, scripts, locks, dependency manifests, realm source, - Docker/Helm configuration, security policy, and release configuration; -- symlinks, hard links, executables, binary files, NUL bytes, mode changes, and - 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. +## Independent verification -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 -the downloaded artifact before applying it. +The fresh verification job confirms that the PR queue is still empty and that +`main` still equals the measured base SHA. It then runs the patch-guard +self-test, locked dependency sync, Ruff, Interrogate, compileall, complete +statement and branch coverage, package build, realm validation, Compose +validation, deployment-template JSON validation, and `git diff --check`. -## Independent verification +No model or publication credential is present in this job. -The fresh verification job requires that the PR queue is still empty and that -`main` still equals the measured base SHA. It then runs: - -```bash -python scripts/ci/hourly_product_guard.py self-test -cd services/account_unification -uv sync --locked --extra dev -uv run ruff check app tests tools -uv run interrogate . -uv run python -m compileall -q app tests tools -uv run coverage run --branch --source=app -m pytest -q -uv run coverage report --show-missing --fail-under=100 -uv build --out-dir dist -cd ../.. -python scripts/validate_realm.py deploy/keycloak/realm-cwl.json -docker compose -f docker-compose.yml config -python -m json.tool deploy/templates/.json -git diff --check -``` - -No model credential or publication credential is present in this job. - -## Publication and race handling +## Publication and incidents 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. - -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 -final network interval, branch protection and the subsequent hourly steward -remain authoritative. During a duplicate-publication incident, revoke -`OPENCODE_PRODUCT_DEVELOPMENT_TOKEN`, close all but one draft, preserve the -Actions logs and artifacts, add a reproducing contract test, and only then -restore the token. - -## First activation - -1. Merge the workflow through the normal protected PR path. -2. Configure `NVIDIA_NIM_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. -4. After the PR queue is empty and exact `main` is green, run a dry run and - confirm the health gate succeeds without invoking a model. -5. Run a normal dispatch and confirm that one draft PR is created. -6. Dispatch again while that draft is open and confirm that no second agent - session begins. - -The hourly schedule is active only after the workflow exists on the default -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. +zero-open-PR checks. It creates one run-unique +`opencode-agent/product-dev--` branch and one draft PR. + +Repository concurrency serializes dispatches, but GitHub does not provide an +atomic compare-base-and-create-PR operation. If another actor opens a PR during +the final network interval, branch protection remains authoritative. Revoke the +publication token, preserve logs and artifacts, close duplicate drafts, add a +reproducing contract test, and restore dispatch only after exact-head +verification. + +For first activation, merge through the protected path, configure at least one +central sidecar provider credential plus the publication token, verify the open +PR and dry-run gates, then perform one normal central or manual dispatch. A +missing provider route stops sidecar bootstrap; a missing publication token +prevents branch and PR creation. ## Release boundary -An autonomous draft may update `[Unreleased]`, but task creation and PR creation -are not release authorization. A release may occur only after protected merge, -complete `main` verification, version and changelog reconciliation, signed -artifacts, SBOM, provenance, rollback evidence, and an explicit release decision. +An autonomous draft may update `[Unreleased]`, but PR creation is not release +authorization. Release requires protected merge, complete `main` verification, +version and changelog reconciliation, signed artifacts, SBOM, provenance, +rollback evidence, and an explicit release decision. ## References @@ -258,8 +143,5 @@ profile* (NIST AI 600-1). https://doi.org/10.6028/NIST.AI.600-1 OpenCode. (2026). *Providers, permissions, and non-interactive run command*. https://opencode.ai/docs/ -NVIDIA. (2026). *NVIDIA NIM APIs: OpenAI-compatible inference endpoints*. -https://docs.nvidia.com/nim/ - Supply-chain Levels for Software Artifacts. (2025). *SLSA specification, version 1.2*. https://slsa.dev/spec/v1.2/ diff --git a/docs/operations/ldap-directory-reconciliation.md b/docs/operations/ldap-directory-reconciliation.md index 3948102..6a8c458 100644 --- a/docs/operations/ldap-directory-reconciliation.md +++ b/docs/operations/ldap-directory-reconciliation.md @@ -97,8 +97,8 @@ mutations remain serialized by the convergence lock. redacted values. - Never enable cleartext LDAP, writable/unsynced mode, Kerberos, or trusted email as an incident workaround. -- Existing review-agent tokens and the hourly NVIDIA NIM OpenCode development - credential are unrelated to runtime directory operations. +- Existing review-agent tokens and centrally dispatched OpenCode development + credentials are unrelated to runtime directory operations. ## Escalation evidence diff --git a/scripts/ci/hourly_product_guard.py b/scripts/ci/hourly_product_guard.py index c0ab167..6cf701f 100644 --- a/scripts/ci/hourly_product_guard.py +++ b/scripts/ci/hourly_product_guard.py @@ -219,7 +219,7 @@ def _read_proposal(workspace: Path) -> tuple[str, str]: proposal_path = workspace / PROPOSAL_FILENAME default_title = "Keyverse autonomous product increment" default_body = ( - "Autonomous NVIDIA NIM increment; see the bounded diff, tests, and " + "Autonomous product increment; see the bounded diff, tests, and " "CHANGELOG.md for evidence." ) if not proposal_path.exists(): 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_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 diff --git a/services/account_unification/tests/test_hourly_product_development.py b/services/account_unification/tests/test_hourly_product_development.py index eb50768..de07b00 100644 --- a/services/account_unification/tests/test_hourly_product_development.py +++ b/services/account_unification/tests/test_hourly_product_development.py @@ -1,4 +1,4 @@ -"""Static contracts for the hourly NVIDIA NIM product-development workflow.""" +"""Static contracts for the centrally dispatched product-development workflow.""" from __future__ import annotations from pathlib import Path @@ -75,11 +75,16 @@ def _permissions_block(source: str, marker: str, terminator: str) -> str: return source[block_start:block_end] -def test_product_development_runs_hourly_without_cancelling_a_decision() -> None: - """The product loop is hourly, serialized, bounded, and non-cancelling.""" +def test_product_development_is_dispatched_by_the_central_cadence() -> None: + """Only the central organization loop decides when this workflow runs.""" workflow = _workflow_source() + document = _workflow_document() + triggers = document.get(True) - assert 'cron: "41 * * * *"' in workflow + assert isinstance(triggers, dict) + assert set(triggers) == {"workflow_dispatch"} + assert "schedule:" not in workflow + assert "cron:" not in workflow assert "hourly-product-development-${{ github.repository }}" in workflow assert "cancel-in-progress: false" in workflow assert "timeout-minutes: 180" in workflow @@ -102,21 +107,30 @@ 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_the_pinned_central_orchestrator_sidecar() -> None: + """OpenCode uses the immutable central sidecar and its free virtual model.""" 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 '"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 "dcd35b7653854edb2ea26a87bac2035f12d8d903" in workflow + assert "repository: ContextualWisdomLab/.github" in workflow + assert "contextual_orchestrator_review_sidecar.sh" in workflow + assert '"enabled_providers": ["contextual-orchestrator"]' in workflow + assert '"model": "contextual-orchestrator/orchestrator/free"' in workflow + assert '"small_model": "contextual-orchestrator/orchestrator/free"' in workflow + assert "MODEL: contextual-orchestrator/orchestrator/free" in workflow + assert "CONTEXTUAL_ORCHESTRATOR_BASE_URL" in workflow + assert "CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE" in workflow + for endpoint in ( + "api.bytez.com:443", + "api.openai.com:443", + "openrouter.ai:443", + "integrate.api.nvidia.com:443", + ): + assert endpoint in develop_endpoints assert "COPILOT_GITHUB_TOKEN" not in workflow assert "/agents/repos/" not in workflow assert "create_pull_request: true" not in workflow @@ -135,16 +149,42 @@ 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_credentials_bootstrap_only_the_sidecar() -> None: + """Raw provider credentials never enter OpenCode's generated workspace or process.""" 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 + provision = _step_by_name( + "develop-product-gap", + "Provision the pinned contextual-orchestrator sidecar", + ) + agent = _step_by_name( + "develop-product-gap", + "Run OpenCode through contextual-orchestrator in a disposable workspace", + ) + provision_env = provision.get("env") + agent_env = agent.get("env") + agent_run = agent.get("run") + + assert isinstance(provision_env, dict) + assert isinstance(agent_env, dict) + assert isinstance(agent_run, str) + assert set(provision_env) == { + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + } + assert not any("secrets." in str(value) for value in agent_env.values()) assert "env -i" in workflow - assert "NVIDIA_API_KEY=${{ secrets.NVIDIA_NIM_API_KEY }}" not in workflow + assert "load_contextual_orchestrator_token.sh" in agent_run + assert "CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE=" in agent_run + assert "CONTEXTUAL_ORCHESTRATOR_TOKEN=" not in agent_run + assert 'source "$HOME/load_contextual_orchestrator_token.sh"' in agent_run + assert "NVIDIA_API_KEY=" not in agent_run + assert "GH_TOKEN=" not in agent_run + assert "GITHUB_TOKEN=" not in agent_run + assert "nim_proxy.py" not in workflow + assert "127.0.0.1:8765" not in workflow def test_product_development_does_not_reuse_review_agent_credentials() -> None: @@ -160,20 +200,17 @@ 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( + sidecar = _step_by_name( "develop-product-gap", - "Start the loopback-only NIM credential broker", + "Provision the pinned contextual-orchestrator sidecar", ) - 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 }}" - ) - assert "NVIDIA_NIM_API_KEY is required only for model-backed development" in broker_run + sidecar_env = sidecar.get("env") + sidecar_run = sidecar.get("run") + + assert isinstance(sidecar_env, dict) + assert isinstance(sidecar_run, str) + assert sidecar.get("if") == "steps.gate.outputs.develop == 'true'" + assert "contextual_orchestrator_review_sidecar.sh" in sidecar_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 +224,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 OpenCode through contextual-orchestrator") + agent_end = workflow.index("Stop contextual-orchestrator", agent_start) agent_block = workflow[agent_start:agent_end] assert "git archive HEAD | tar -x" in workflow @@ -200,6 +237,14 @@ def test_agent_runs_in_a_disposable_credential_free_workspace() -> None: assert "GH_TOKEN=" not in agent_block assert "GITHUB_TOKEN=" not in agent_block assert "ACTIONS_ID_TOKEN_REQUEST_TOKEN" not in agent_block + for secret_name in ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert secret_name not in agent_block def test_product_development_uses_generate_reverify_publish_separation() -> None: @@ -265,7 +310,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 "opencode-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_guard.py b/services/account_unification/tests/test_hourly_product_guard.py index 8467fbb..163b31e 100644 --- a/services/account_unification/tests/test_hourly_product_guard.py +++ b/services/account_unification/tests/test_hourly_product_guard.py @@ -116,7 +116,7 @@ def test_guard_rejects_deletion_binary_mode_and_control_plane_patches(tmp_path) def test_guard_rejects_model_secret_in_raw_and_encoded_forms( monkeypatch, tmp_path ) -> None: - """The generated patch and PR metadata cannot exfiltrate the NIM credential.""" + """The generated patch and PR metadata cannot exfiltrate a provider credential.""" guard = _load_guard() monkeypatch.setenv("KEYVERSE_FORBIDDEN_SECRET", "nim-sensitive-value") 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..5b4031c 100644 --- a/services/account_unification/tests/test_hourly_product_incident_contract.py +++ b/services/account_unification/tests/test_hourly_product_incident_contract.py @@ -9,11 +9,14 @@ EXPECTED_ENDPOINTS = { "develop-product-gap": ( "api.github.com:443", + "api.bytez.com:443", + "api.openai.com:443", "cafe.github.com:443", "codeload.github.com:443", "github.com:443", "integrate.api.nvidia.com:443", "objects.githubusercontent.com:443", + "openrouter.ai:443", "raw.githubusercontent.com:443", "registry.npmjs.org:443", "release-assets.githubusercontent.com:443", @@ -230,77 +233,80 @@ 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.""" - 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"))) - timeout_minutes = int(str(_job("develop-product-gap").get("timeout-minutes", 0))) +def test_opencode_uses_one_unbounded_orchestrator_free_route() -> None: + """The workflow delegates discovery and fallback without a local model loop.""" + workflow = _workflow_source() - assert candidates - setup_and_packaging_reserve_seconds = 15 * 60 - assert timeout_minutes * 60 >= ( - len(candidates) * per_model_seconds + setup_and_packaging_reserve_seconds - ) + assert "OPENCODE_MODEL_CANDIDATES" not in workflow + assert "OPENCODE_RUN_TIMEOUT_SECONDS" not in workflow + assert "for model in" not in workflow + assert "timeout --kill-after" not in workflow + assert "contextual-orchestrator/orchestrator/free" in workflow -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] = [] +def test_provider_secrets_are_materialized_only_by_sidecar_bootstrap() -> None: + """Every raw provider secret is scoped to the central sidecar bootstrap step.""" + materializing_steps: dict[str, list[str]] = {} + secret_expressions = ( + "${{ secrets.BYTEZ_API_KEY }}", + "${{ secrets.NVIDIA_NIM_API_KEY }}", + "${{ secrets.NVIDIA_NIM_API_KEY_SUB }}", + "${{ secrets.OPENROUTER_API_KEY }}", + "${{ secrets.OPENAI_API_KEY }}", + ) + for secret_expression in secret_expressions: + materializing_steps[secret_expression] = [] for step in _steps("develop-product-gap"): env = step.get("env") - if not isinstance(env, dict) or secret_expression not in env.values(): + if not isinstance(env, dict): continue name = step.get("name") assert isinstance(name, str) - materializing_steps.append(name) + for secret_expression in secret_expressions: + if secret_expression in env.values(): + materializing_steps[secret_expression].append(name) - assert materializing_steps == ["Start the loopback-only NIM credential broker"] + assert all( + steps == ["Provision the pinned contextual-orchestrator sidecar"] + for steps in materializing_steps.values() + ) -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_secret_fingerprints_cross_the_sidecar_boundary() -> None: + """Packaging receives only one-way fingerprints for leak scanning.""" + sidecar = _step_by_name( "develop-product-gap", - "Start the loopback-only NIM credential broker", + "Provision the pinned contextual-orchestrator sidecar", ) package = _step_by_name( "develop-product-gap", "Capture the bounded credential-free patch", ) - broker_run = broker.get("run") + sidecar_run = sidecar.get("run") package_env = package.get("env") - assert isinstance(broker_run, str) + assert isinstance(sidecar_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 sidecar.get("id") == "orchestrator" + assert "sha256" in sidecar_run + assert "GITHUB_OUTPUT" in sidecar_run assert package_env.get("KEYVERSE_FORBIDDEN_SECRET_FINGERPRINT") == ( - "${{ steps.nim_broker.outputs.secret_fingerprint }}" + "${{ steps.orchestrator.outputs.secret_fingerprint }}" ) assert "KEYVERSE_FORBIDDEN_SECRET: ${{ secrets.NVIDIA_NIM_API_KEY }}" 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_sidecar_bootstrap_runs_only_on_the_model_backed_path() -> None: + """Provider bootstrap happens only after deterministic gates select development.""" + sidecar = _step_by_name( "develop-product-gap", - "Start the loopback-only NIM credential broker", + "Provision the pinned contextual-orchestrator sidecar", ) - 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 + sidecar_env = sidecar.get("env") + sidecar_run = sidecar.get("run") + assert isinstance(sidecar_env, dict) + assert isinstance(sidecar_run, str) + + assert sidecar.get("if") == "steps.gate.outputs.develop == 'true'" + assert "contextual_orchestrator_review_sidecar.sh" in sidecar_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"