diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 65e1a85e..152a9b90 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -28,17 +28,25 @@ jobs: changed: ${{ steps.package.outputs.changed }} steps: - name: Harden runner + # audit, not block: the model call now routes through the vendored + # contextual-orchestrator gateway's live multi-provider discovery + # (BYTEZ_API_KEY, NVIDIA_NIM_API_KEY[_SUB], OPENROUTER_API_KEY, + # OPENAI_API_KEY -- see "Provision the contextual-orchestrator gateway + # sidecar" below), which resolves its own outbound routes at runtime + # (including https://models.dev for free-tier pricing metadata) and + # has no fixed host set to pin. This is not a downgrade invented for + # this repo: it is the exact egress posture ContextualWisdomLab/.github + # already uses in production for the same sidecar in + # pr-review-autofix.yml and strix.yml -- no consumer of this sidecar + # anywhere in the org runs it under block-mode with a static + # allowlist, because that combination is not yet solved. The actual + # behavioral boundary against a prompt-injected model stays the + # deny-by-default OpenCode permission block below (webfetch, websearch, + # task, skill, lsp, and every bash command but three read-only ones all + # stay "deny"), which is unaffected by this runner-network setting. uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 with: - egress-policy: block - allowed-endpoints: > - api.github.com:443 - files.pythonhosted.org:443 - github.com:443 - integrate.api.nvidia.com:443 - objects.githubusercontent.com:443 - pypi.org:443 - release-assets.githubusercontent.com:443 + egress-policy: audit - name: Check out the protected default branch uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -66,17 +74,28 @@ jobs: echo "::notice::Skipping product development because ${open_prs} pull request(s) remain open." fi - - name: Require the autonomous maintainer model credential + - name: Require the autonomous maintainer gateway credentials if: steps.queue.outputs.develop == 'true' env: - NVIDIA_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 "${NVIDIA_API_KEY:-}" ]; then - echo "::error::NVIDIA_NIM_API_KEY is required for hourly autonomous product development." + present=0 + for secret_name in BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB OPENROUTER_API_KEY OPENAI_API_KEY; do + value="${!secret_name:-}" + if [ -n "$value" ]; then + echo "::add-mask::$value" + present=$((present + 1)) + fi + done + if [ "$present" -eq 0 ]; then + echo "::error::At least one of BYTEZ_API_KEY, NVIDIA_NIM_API_KEY, NVIDIA_NIM_API_KEY_SUB, OPENROUTER_API_KEY, OPENAI_API_KEY is required for hourly autonomous product development." exit 1 fi - echo "::add-mask::$NVIDIA_API_KEY" - name: Set up Python if: steps.queue.outputs.develop == 'true' @@ -133,12 +152,49 @@ jobs: ;; esac + - name: Checkout the trusted contextual-orchestrator gateway sidecar source + if: steps.queue.outputs.develop == 'true' + env: + TRUSTED_GATEWAY_SOURCE_SHA: "6958918beaad96d0a67ce264706c828bb7f3f000" + run: | + set -euo pipefail + # Cloned outside $GITHUB_WORKSPACE (not via actions/checkout's + # workspace-relative `path:`) so this vendored tree never lands + # inside the git repository that "Enforce the autonomous change + # boundary" below diffs against the pristine baseline -- an + # untracked copy of an entire second repository there would be + # swept into the model's own patch. + source_dir="${RUNNER_TEMP}/trusted-gateway-source" + git clone --quiet https://github.com/ContextualWisdomLab/.github.git "$source_dir" + git -C "$source_dir" -c advice.detachedHead=false checkout --quiet "$TRUSTED_GATEWAY_SOURCE_SHA" + checked_out="$(git -C "$source_dir" rev-parse HEAD)" + if [ "$checked_out" != "$TRUSTED_GATEWAY_SOURCE_SHA" ]; then + echo "::error::vendored ContextualWisdomLab/.github HEAD ${checked_out} != pin ${TRUSTED_GATEWAY_SOURCE_SHA}" + exit 1 + fi + echo "TRUSTED_GATEWAY_SOURCE=$source_dir" >>"$GITHUB_ENV" + + - name: Provision the contextual-orchestrator gateway sidecar + if: steps.queue.outputs.develop == 'true' + env: + 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 + # strix_runs/ is this sidecar's own evidence directory, hardcoded + # (by the vendored script) to land under $GITHUB_WORKSPACE; it is + # excluded via .gitignore for the same reason the vendored source + # checkout above is cloned outside the workspace. + bash "${TRUSTED_GATEWAY_SOURCE}/scripts/ci/contextual_orchestrator_review_sidecar.sh" + - name: Run the bounded OpenCode autonomous maintainer if: steps.queue.outputs.develop == 'true' id: opencode env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - OPENCODE_MODEL: "nvidia/nemotron-3-super-120b-a12b" + OPENCODE_MODEL: "contextual-orchestrator/orchestrator/free" HOME: "${{ runner.temp }}/opencode-home" XDG_CONFIG_HOME: "${{ runner.temp }}/opencode-home/config" XDG_DATA_HOME: "${{ runner.temp }}/opencode-home/data" @@ -151,10 +207,17 @@ jobs: OPENCODE_DISABLE_CLAUDE_CODE: "true" OPENCODE_AUTO_SHARE: "false" OPENCODE_CONFIG_CONTENT: >- - {"$schema":"https://opencode.ai/config.json","model":"nvidia/nemotron-3-super-120b-a12b","small_model":"nvidia/nemotron-3-super-120b-a12b","share":"disabled","autoupdate":false,"provider":{"nvidia":{"options":{"baseURL":"https://integrate.api.nvidia.com/v1","apiKey":"{env:NVIDIA_API_KEY}"},"models":{"nemotron-3-super-120b-a12b":{"name":"NVIDIA Nemotron 3 Super 120B A12B","limit":{"context":1000000,"output":32768}}}}},"permission":{"*":"deny","read":{"*":"allow",".git/**":"deny","*.env":"deny","*.env.*":"deny"},"edit":{"*":"deny","src/egressweave/**":"allow","tests/**":"allow","docs/**":"allow","README.md":"allow","CHANGELOG.md":"allow"},"glob":"allow","grep":"allow","bash":{"*":"deny","git diff --check":"allow","git diff --stat":"allow","git status --short":"allow"},"external_directory":"deny","webfetch":"deny","websearch":"deny","question":"deny","task":"deny","skill":"deny"}} + {"$schema":"https://opencode.ai/config.json","model":"contextual-orchestrator/orchestrator/free","small_model":"contextual-orchestrator/orchestrator/free","share":"disabled","autoupdate":false,"provider":{"contextual-orchestrator":{"npm":"@ai-sdk/openai-compatible","name":"Contextual Orchestrator Gateway","options":{"baseURL":"{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}","apiKey":"{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"},"models":{"orchestrator/free":{"name":"Orchestrator Free (ZDR-first zero-cost pool)","tool_call":true,"reasoning":true,"limit":{"context":200000,"output":32768}}}}},"permission":{"*":"deny","read":{"*":"allow",".git/**":"deny","*.env":"deny","*.env.*":"deny"},"edit":{"*":"deny","src/egressweave/**":"allow","tests/**":"allow","docs/**":"allow","README.md":"allow","CHANGELOG.md":"allow"},"glob":"allow","grep":"allow","bash":{"*":"deny","git diff --check":"allow","git diff --stat":"allow","git status --short":"allow"},"external_directory":"deny","webfetch":"deny","websearch":"deny","question":"deny","task":"deny","skill":"deny"}} run: | set -euo pipefail - echo "::add-mask::$NVIDIA_API_KEY" + if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then + echo "::error::The contextual-orchestrator gateway sidecar must be provisioned before the autonomous maintainer runs." + exit 1 + fi + # Loads and masks CONTEXTUAL_ORCHESTRATOR_TOKEN from the sidecar's + # owner/mode-validated token file into THIS step's shell only -- the + # raw bearer never crosses a step boundary via $GITHUB_ENV. + source "${TRUSTED_GATEWAY_SOURCE}/scripts/ci/load_contextual_orchestrator_token.sh" mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" chmod 0700 "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" prompt_file="${RUNNER_TEMP}/opencode-prompt.md" @@ -181,16 +244,35 @@ jobs: - name: Reject model credential disclosure if: steps.queue.outputs.develop == 'true' env: - NVIDIA_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 + # None of the five real provider keys are ever this job's OpenCode + # process's own environment (only the gateway sidecar process ever + # holds them, seeded once into its in-memory KV); this scan is + # defense in depth in case the gateway ever echoed one back. The + # ephemeral per-run CONTEXTUAL_ORCHESTRATOR_TOKEN, which IS in that + # environment, is scanned for the same reason the old NVIDIA_API_KEY + # value was. + source "${TRUSTED_GATEWAY_SOURCE}/scripts/ci/load_contextual_orchestrator_token.sh" result_file="${RUNNER_TEMP}/opencode-result.ndjson" disclosure_file="${RUNNER_TEMP}/credential-disclosure-paths" - if grep -R -F -l -- "$NVIDIA_API_KEY" \ - src/egressweave tests docs README.md CHANGELOG.md "$result_file" \ - >"$disclosure_file"; then - echo "::error::The model disclosed its credential in the following path(s):" - sed 's/^/::error::/' "$disclosure_file" + : >"$disclosure_file" + for secret_name in BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB \ + OPENROUTER_API_KEY OPENAI_API_KEY CONTEXTUAL_ORCHESTRATOR_TOKEN; do + value="${!secret_name:-}" + [ -n "$value" ] || continue + grep -R -F -l -- "$value" \ + src/egressweave tests docs README.md CHANGELOG.md "$result_file" \ + >>"$disclosure_file" || true + done + if [ -s "$disclosure_file" ]; then + echo "::error::The model disclosed a credential in the following path(s):" + sort -u -- "$disclosure_file" | sed 's/^/::error::/' exit 1 fi diff --git a/.gitignore b/.gitignore index 2b448f75..62917f33 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,8 @@ venv/ .mypy_cache/ .coverage htmlcov/ + +# Vendored contextual-orchestrator gateway sidecar evidence (hourly-product-development.yml). +# Written under $GITHUB_WORKSPACE by scripts/ci/contextual_orchestrator_review_sidecar.sh; +# excluded so it is never swept into the autonomous maintainer's captured patch. +strix_runs/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e7ea4b5a..61c0e715 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -279,7 +279,8 @@ publication before the GitHub Release is made public. and guarded direct or automatic merge. The existing review-agent identity and inherited secret contract are preserved. - At minute `37` each hour, product development runs only when no pull request is - open. It uses a pinned OpenCode CLI with `NVIDIA_NIM_API_KEY`, not + open. It uses a pinned OpenCode CLI through the org's governed + contextual-orchestrator gateway, pinned to `orchestrator/free`, not `COPILOT_GITHUB_TOKEN`. Model execution and credential-free reverification use separate runners and permissions. The verifier emits only a short-lived digest-bound handoff; no repository-local product-development job obtains diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dfaecbc..6fc5de72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). constructors accept positive integers or ASCII decimal strings and reject ambiguous or non-positive configuration before network I/O. +### Changed +- Migrate the hourly product-development workflow off a direct, hardcoded + NVIDIA NIM API call and onto the org's governed contextual-orchestrator + gateway, pinned to `orchestrator/free`, following the same vendored-sidecar + pattern already shipped for `ContextualWisdomLab/.github`'s OpenCode Review + and Strix jobs (`scripts/ci/contextual_orchestrator_review_sidecar.sh` + vendored at a pinned immutable commit). OpenCode's provider credential is + now an ephemeral, per-run, loopback-scoped bearer token; none of the five + bootstrap-only provider secrets (`BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, + `NVIDIA_NIM_API_KEY_SUB`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY`) ever + reach the model-consuming process's own environment. The `develop` job's + runner egress policy moves from block-mode with a static host allowlist to + audit-mode, matching the only production precedent for this sidecar + anywhere in the org, because the sidecar's live multi-provider discovery + has no fixed host set to pin; the deny-by-default OpenCode permission + boundary (no webfetch, websearch, task, skill, lsp, or unlisted bash) is + unaffected and remains the actual control against a prompt-injected model. + See ContextualWisdomLab/EgressWeave#234. + ### Fixed - Restore the hourly PR-maintenance calls to the reviewed immutable `ContextualWisdomLab/.github` revision diff --git a/CLAUDE.md b/CLAUDE.md index e89c4376..18186e57 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,9 +32,12 @@ architecture, and planned behavior rather than treating every design as shipped. model, product/technical contract, ADRs, and architecture documentation when a durable governed boundary changes. - Do not use `COPILOT_GITHUB_TOKEN` for autonomous product development. The - product-development workflow uses pinned OpenCode with - `NVIDIA_NIM_API_KEY`; the existing organization-owned review-agent identity - and inherited secret contract must not be repurposed. + product-development workflow uses pinned OpenCode through the org's governed + contextual-orchestrator gateway, pinned to `orchestrator/free` + (bootstrap-only `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, + `NVIDIA_NIM_API_KEY_SUB`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY`); the + existing organization-owned review-agent write identity and inherited + secret contract must not be repurposed. - Do not treat queued, pending, cancelled, stale-head, or previous-head checks as successful evidence. diff --git a/README.md b/README.md index aac81085..a682b8d5 100644 --- a/README.md +++ b/README.md @@ -344,9 +344,11 @@ product roadmap moving without bypassing normal governance: - at minute `07`, the repository calls the organization-owned review-fix and merge schedulers to inspect feedback, recheck current-head evidence, and update eligible branches; final merges remain operator-controlled; -- at minute `37`, a bounded OpenCode maintainer backed by - `NVIDIA_NIM_API_KEY` runs only when there are zero open pull requests and - implements one test-driven improvement. +- at minute `37`, a bounded OpenCode maintainer backed by the org's governed + contextual-orchestrator gateway (pinned to `orchestrator/free`, provisioned + from `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`, + `OPENROUTER_API_KEY`, and `OPENAI_API_KEY`) runs only when there are zero + open pull requests and implements one test-driven improvement. The product workflow uses two fresh runners. The model job has read-only GitHub permissions, no direct network access, and can emit only a guard-checked patch. diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index f13e1e7e..763c916c 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -105,7 +105,7 @@ EgressWeave does not solve privacy by transforming or blanket-masking applicatio Repository automation is a separate authority boundary from runtime egress policy. - Automated model output is untrusted proposed change material, not review, security-scan, merge or release authority. -- Product-development model access uses the reviewed OpenCode/NVIDIA path; reviewer identity and credentials remain separate. +- Product-development model access uses the reviewed OpenCode/contextual-orchestrator gateway path (`orchestrator/free`); reviewer identity and credentials remain separate. - Exact-head CI/security evidence is not transferable after a head/base change. - A green aggregate workflow does not prove a required inner security action executed when that action was skipped. - Repository-local product development must not regain publisher credentials merely to make autonomous output easier to merge. diff --git a/docs/architecture/SYSTEM_ARCHITECTURE.md b/docs/architecture/SYSTEM_ARCHITECTURE.md index e9023b5c..f159dc46 100644 --- a/docs/architecture/SYSTEM_ARCHITECTURE.md +++ b/docs/architecture/SYSTEM_ARCHITECTURE.md @@ -122,7 +122,7 @@ flowchart LR checkout[Exact protected-main checkout] --> pr_gate[Paginated zero-open-PR gate] pr_gate --> prompt[Canonical maintainer prompt] prompt --> prompt_guard[Regular file + non-symlink + 12 KiB validation] - prompt_guard --> opencode[OpenCode with NVIDIA_NIM_API_KEY] + prompt_guard --> opencode[OpenCode via contextual-orchestrator gateway] opencode --> untrusted_patch[Bounded untrusted patch + NDJSON result] untrusted_patch --> handoff_guard[Exact-base and allowlist guard] handoff_guard --> verifier[Credential-free verifier] diff --git a/docs/architecture/UML.md b/docs/architecture/UML.md index a4009eef..073f72ca 100644 --- a/docs/architecture/UML.md +++ b/docs/architecture/UML.md @@ -228,7 +228,7 @@ The repository has separate governance and product-development paths. Current pr sequenceDiagram participant Repo as EgressWeave repository participant Central as Organization PR maintenance - participant Model as OpenCode + NVIDIA NIM + participant Model as OpenCode + contextual-orchestrator gateway participant Verify as Credential-free verifier participant Review as Independent review / repository gates @@ -329,7 +329,7 @@ sequenceDiagram Workflow->>Prompt: validate canonical prompt is regular, non-symlink and <= 12 KiB Prompt-->>Workflow: reviewed bounded policy bytes - Workflow->>Model: execute with NVIDIA credential and deny-by-default tools + Workflow->>Model: execute with gateway bearer and deny-by-default tools Model-->>Workflow: bounded patch + auditable NDJSON Workflow->>Verify: exact-base patch handoff without model credential Verify-->>Workflow: sealed digest-bound patch evidence diff --git a/docs/hourly-autonomous-maintenance.md b/docs/hourly-autonomous-maintenance.md index 64d47d48..4c6d0fb7 100644 --- a/docs/hourly-autonomous-maintenance.md +++ b/docs/hourly-autonomous-maintenance.md @@ -72,14 +72,44 @@ request is open. It installs the trusted base toolchain, creates a root-owned read-only baseline outside the model workspace, and then runs OpenCode 1.18.13 from the official Linux x64 release asset only after verifying SHA-256 `8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937`. -The repository secret `NVIDIA_NIM_API_KEY` is exposed only to that process -through OpenCode's documented `NVIDIA_API_KEY` provider variable. The explicit -model is `nvidia/nemotron-3-super-120b-a12b`. + +Model access is not a direct provider call. The job vendors +`scripts/ci/contextual_orchestrator_review_sidecar.sh` from +`ContextualWisdomLab/.github` at the pinned immutable commit +`6958918beaad96d0a67ce264706c828bb7f3f000` (cloned outside `$GITHUB_WORKSPACE` +so the vendored tree cannot be swept into the model's own patch), which starts +the org's governed contextual-orchestrator gateway as a loopback sidecar. The +bootstrap-only provider secrets `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, +`NVIDIA_NIM_API_KEY_SUB`, `OPENROUTER_API_KEY`, and `OPENAI_API_KEY` (each +optional; at least one is required) are registered once into the sidecar +process's own in-memory KV and are never read again from the environment. +OpenCode itself never sees any of those five keys: its provider `apiKey` is an +ephemeral, per-run `CONTEXTUAL_ORCHESTRATOR_TOKEN` bearer scoped to the +sidecar's loopback port, loaded from an owner/mode-validated file +(`scripts/ci/load_contextual_orchestrator_token.sh`) so the raw value never +crosses a step boundary in the runner's rendered environment. The explicit +model is `contextual-orchestrator/orchestrator/free`, the same fail-closed, +ZDR-first, zero-cost pool identifier +[ContextualWisdomLab/.github](https://github.com/ContextualWisdomLab/.github) +already pins for its own OpenCode Review and Strix jobs — which of the five +registered providers actually serves a given hourly run is decided by the +gateway's own live discovery and routing, not fixed in this workflow. +As with the PR-maintenance loop's own reusable-workflow pin, refresh +`TRUSTED_GATEWAY_SOURCE_SHA` to a later reviewed `ContextualWisdomLab/.github` +commit through an ordinary reviewed repository change whenever the sidecar or +token-loader scripts materially change there; nothing here re-resolves that +pin automatically. The model execution boundary provides: -- block-mode runner egress restricted to reviewed package sources, GitHub, and - `integrate.api.nvidia.com:443`; +- audit-mode (not block-mode) runner egress: the gateway sidecar's live + multi-provider discovery (including a `https://models.dev` metadata fetch to + determine free-tier pricing) has no fixed host set to pin, and no consumer of + this exact sidecar anywhere in the org runs it under a block-mode allowlist + today — this matches the only production precedent (`.github`'s + `pr-review-autofix.yml` and `strix.yml`), not a downgrade invented here. The + behavioral boundary against a prompt-injected model is the deny-by-default + OpenCode permission block below, which this setting does not affect; - deny-by-default OpenCode permissions, with edits limited to the bounded source, test, documentation, README, and CHANGELOG paths; - an isolated empty `HOME` and XDG configuration/data/cache roots, plus @@ -198,16 +228,22 @@ larger than 1,000 changed lines. The scheduled product-development workflow requires: -- `NVIDIA_NIM_API_KEY`, mapped only to OpenCode's `NVIDIA_API_KEY` - environment variable for the NVIDIA NIM endpoint; +- at least one of `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, + `NVIDIA_NIM_API_KEY_SUB`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY` — the + bootstrap-only credentials the contextual-orchestrator gateway sidecar + registers into its own in-memory KV before OpenCode ever starts; a missing + individual secret only narrows the sidecar's discovered pool, and OpenCode + itself receives none of them (only the sidecar's own ephemeral + `CONTEXTUAL_ORCHESTRATOR_TOKEN` bearer); - the standard Docker installation available on GitHub-hosted Ubuntu runners; - one explicitly reviewed Docker Official Image `python@sha256:<64-hex>` verifier base committed in the workflow. -The workflow fails closed when the model credential, protected base identity, -reviewed verifier-base digest, immutable built verifier image, container -isolation, or patch identity is unavailable. It has no fallback repository-write -identity and does not reuse review-agent, release, package, attestation, or ref +The workflow fails closed when every gateway provider credential, the vendored +sidecar's pinned-commit checkout, protected base identity, reviewed +verifier-base digest, immutable built verifier image, container isolation, or +patch identity is unavailable. It has no fallback repository-write identity +and does not reuse review-agent, release, package, attestation, or ref credentials. ## Manual operation @@ -223,9 +259,12 @@ condition or turn the verified handoff into a repository write. Anomaly. (2026). *OpenCode CLI documentation*. https://opencode.ai/docs/cli/ -Anomaly. (2026). *OpenCode providers: NVIDIA*. +Anomaly. (2026). *OpenCode providers: OpenAI-compatible*. https://opencode.ai/docs/providers/ +ContextualWisdomLab. (2026). *contextual-orchestrator: the org's governed LLM +gateway*. https://github.com/ContextualWisdomLab/contextual-orchestrator + Docker, Inc. (n.d.). *Building best practices*. Docker Docs. https://docs.docker.com/build/building/best-practices/ @@ -234,6 +273,3 @@ https://docs.docker.com/dhi/explore/security-concepts/digests/ Docker, Inc. (n.d.). *Validating image inputs*. Docker Docs. https://docs.docker.com/build/policies/validate-images/ - -NVIDIA Corporation. (2026). *NVIDIA Nemotron 3 Super 120B A12B model card*. -https://build.nvidia.com/nvidia/nemotron-3-super-120b-a12b/modelcard diff --git a/docs/product/TRD.md b/docs/product/TRD.md index 24691a71..0381f74f 100644 --- a/docs/product/TRD.md +++ b/docs/product/TRD.md @@ -127,7 +127,7 @@ The public API contract is documented in [`API_CONTRACT.md`](API_CONTRACT.md). H The **ACTIVE-PR** repository-local scheduler must satisfy all of the following: 1. Check out the exact protected branch and reject product development while any open PR exists. -2. Install one SHA-256-verified OpenCode release and use the existing `NVIDIA_NIM_API_KEY` through the documented `NVIDIA_API_KEY` mapping. +2. Install one SHA-256-verified OpenCode release and route model access through the vendored, pinned-commit contextual-orchestrator gateway sidecar (`orchestrator/free`); OpenCode itself never receives any of the sidecar's bootstrap-only provider secrets. 3. Validate and copy `.github/prompts/hourly-product-maintainer.md` under the 12 KiB limit before the credential-bearing model step. 4. Keep model tools deny-by-default and prohibit repository-code execution in that credential-bearing step. 5. Package only an allowlisted, bounded patch tied to the exact base SHA. diff --git a/tests/test_hourly_gateway_credential_boundary.py b/tests/test_hourly_gateway_credential_boundary.py new file mode 100644 index 00000000..ae4b69ed --- /dev/null +++ b/tests/test_hourly_gateway_credential_boundary.py @@ -0,0 +1,54 @@ +"""Regression contracts for the hourly gateway credential boundary.""" + +from __future__ import annotations + +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = REPOSITORY_ROOT / ".github" / "workflows" / "hourly-product-development.yml" +PROVIDER_SECRETS = ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", +) + + +def _workflow_step(workflow: str, start: str, end: str) -> str: + """Return one named workflow-step slice between exact reviewed markers.""" + assert workflow.count(start) == 1 + assert workflow.count(end) == 1 + return workflow.split(start, 1)[1].split(end, 1)[0] + + +def test_opencode_receives_only_loopback_gateway_credentials() -> None: + """Keep provider secrets outside the model-running OpenCode process.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + opencode_step = _workflow_step( + workflow, + " - name: Run the bounded OpenCode autonomous maintainer\n", + " - name: Reject model credential disclosure\n", + ) + + for secret_name in PROVIDER_SECRETS: + assert secret_name not in opencode_step + assert '"baseURL":"{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}"' in opencode_step + assert '"apiKey":"{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"' in opencode_step + + +def test_disclosure_scan_includes_gateway_bearer_and_provider_secrets() -> None: + """Fail closed if either bootstrap credentials or the bearer reach output.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + disclosure_step = _workflow_step( + workflow, + " - name: Reject model credential disclosure\n", + " - name: Preserve the OpenCode result outside the source tree\n", + ) + + for secret_name in (*PROVIDER_SECRETS, "CONTEXTUAL_ORCHESTRATOR_TOKEN"): + assert secret_name in disclosure_step + assert ( + "OPENROUTER_API_KEY OPENAI_API_KEY CONTEXTUAL_ORCHESTRATOR_TOKEN; do" + in disclosure_step + ) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index a5df254b..94c7e1c3 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -1,4 +1,4 @@ -"""Contracts for the NVIDIA-backed OpenCode autonomous development scheduler.""" +"""Contracts for the gateway-backed OpenCode autonomous development scheduler.""" from __future__ import annotations @@ -19,9 +19,15 @@ OPENCODE_LINUX_X64_SHA256 = ( "8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937" ) -NVIDIA_MODEL = "nvidia/nemotron-3-super-120b-a12b" -NVIDIA_API_HOST_LABELS = ("integrate", "api", "nvidia", "com") -NVIDIA_API_ENDPOINT = f"{'.'.join(NVIDIA_API_HOST_LABELS)}:443" +GATEWAY_MODEL = "contextual-orchestrator/orchestrator/free" +GATEWAY_PROVIDER_SECRETS = ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", +) +TRUSTED_GATEWAY_SOURCE_SHA = "6958918beaad96d0a67ce264706c828bb7f3f000" def _read(path: Path) -> str: @@ -29,13 +35,14 @@ def _read(path: Path) -> str: return path.read_text(encoding="utf-8") -def test_product_scheduler_uses_pinned_opencode_with_nvidia_nim() -> None: +def test_product_scheduler_uses_pinned_opencode_through_the_governed_gateway() -> None: """Replace the Codex scheduler model step without mutable agent tooling.""" workflow = _read(PRODUCT_WORKFLOW_PATH) assert "openai/codex-action@" not in workflow - assert "OPENAI_API_KEY" not in workflow - assert "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow + assert "codex run" not in workflow + for secret_name in GATEWAY_PROVIDER_SECRETS: + assert f"{secret_name}: ${{{{ secrets.{secret_name} }}}}" in workflow assert f'OPENCODE_VERSION: "{OPENCODE_VERSION}"' in workflow assert f'OPENCODE_SHA256: "{OPENCODE_LINUX_X64_SHA256}"' in workflow assert ( @@ -44,15 +51,52 @@ def test_product_scheduler_uses_pinned_opencode_with_nvidia_nim() -> None: ) in workflow assert "sha256sum --check" in workflow assert "opencode run --auto" in workflow - assert f'OPENCODE_MODEL: "{NVIDIA_MODEL}"' in workflow + assert f'OPENCODE_MODEL: "{GATEWAY_MODEL}"' in workflow + assert f'"model":"{GATEWAY_MODEL}"' in workflow + assert f'"small_model":"{GATEWAY_MODEL}"' in workflow + + +def test_gateway_sidecar_is_vendored_at_a_pinned_immutable_commit() -> None: + """Fetch the reviewed org sidecar by exact SHA, never a floating ref.""" + workflow = _read(PRODUCT_WORKFLOW_PATH) + + assert f'TRUSTED_GATEWAY_SOURCE_SHA: "{TRUSTED_GATEWAY_SOURCE_SHA}"' in workflow + assert ( + "git clone --quiet https://github.com/ContextualWisdomLab/.github.git" + in workflow + ) + assert ( + 'git -C "$source_dir" -c advice.detachedHead=false checkout --quiet ' + '"$TRUSTED_GATEWAY_SOURCE_SHA"' + ) in workflow + assert 'checked_out="$(git -C "$source_dir" rev-parse HEAD)"' in workflow + assert '[ "$checked_out" != "$TRUSTED_GATEWAY_SOURCE_SHA" ]' in workflow + assert ( + 'bash "${TRUSTED_GATEWAY_SOURCE}/scripts/ci/contextual_orchestrator_review_sidecar.sh"' + in workflow + ) + assert ( + 'source "${TRUSTED_GATEWAY_SOURCE}/scripts/ci/load_contextual_orchestrator_token.sh"' + in workflow + ) + # Vendored outside $GITHUB_WORKSPACE: the sidecar's own checkout must never + # land inside the git repository the patch-capture guard diffs against the + # pristine baseline. + assert 'source_dir="${RUNNER_TEMP}/trusted-gateway-source"' in workflow + assert 'echo "TRUSTED_GATEWAY_SOURCE=$source_dir" >>"$GITHUB_ENV"' in workflow def test_model_execution_keeps_a_fail_closed_permission_and_secret_boundary() -> None: - """Deny unneeded tools and reject model output containing its credential.""" + """Deny unneeded tools and reject model output containing a credential.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - assert "egress-policy: block" in workflow - assert NVIDIA_API_ENDPOINT in {line.strip() for line in workflow.splitlines()} + # Runner-network egress is audit-mode, matching the only production + # precedent for this exact sidecar anywhere in the org (ContextualWisdomLab + # /.github's pr-review-autofix.yml and strix.yml); the sidecar's live + # multi-provider discovery has no fixed host set for a block-mode + # allowlist to pin. See the "Harden runner" step's own comment. + assert "egress-policy: audit" in workflow + assert "egress-policy: block" not in workflow assert 'OPENCODE_DISABLE_AUTOUPDATE: "true"' in workflow assert 'OPENCODE_DISABLE_MODELS_FETCH: "true"' in workflow assert 'OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"' in workflow @@ -67,8 +111,9 @@ def test_model_execution_keeps_a_fail_closed_permission_and_secret_boundary() -> assert '"task":"deny"' in workflow assert '"skill":"deny"' in workflow assert "Reject model credential disclosure" in workflow - assert 'grep -R -F -l -- "$NVIDIA_API_KEY"' in workflow - assert 'grep -R -F -- "$NVIDIA_API_KEY"' not in workflow + assert 'grep -R -F -l -- "$value"' in workflow + assert 'grep -R -F -- "$value"' not in workflow + assert "CONTEXTUAL_ORCHESTRATOR_TOKEN" in workflow def test_credentialed_model_runner_never_executes_model_modified_code() -> None: @@ -146,15 +191,17 @@ def test_product_scheduler_never_publishes_a_model_modified_tree() -> None: assert "retention-days: 3" in handoff -def test_operator_documentation_records_the_pinned_agent_and_secret_mapping() -> None: +def test_operator_documentation_records_the_pinned_agent_and_gateway_mapping() -> None: """Make the autonomous execution supply chain understandable to operators.""" documentation = _read(MAINTENANCE_DOCUMENTATION_PATH) assert f"OpenCode {OPENCODE_VERSION}" in documentation - assert "`NVIDIA_NIM_API_KEY`" in documentation - assert "`NVIDIA_API_KEY`" in documentation - assert NVIDIA_MODEL in documentation + for secret_name in GATEWAY_PROVIDER_SECRETS: + assert f"`{secret_name}`" in documentation + assert f"`{GATEWAY_MODEL}`" in documentation + assert "`CONTEXTUAL_ORCHESTRATOR_TOKEN`" in documentation assert OPENCODE_LINUX_X64_SHA256 in documentation + assert TRUSTED_GATEWAY_SOURCE_SHA in documentation assert "OpenAI Codex Action" not in documentation @@ -167,12 +214,13 @@ def test_operator_documentation_forbids_repository_local_patch_publication() -> assert "reconstruct and verify the exact tree" in documentation -def test_buyer_readme_identifies_the_opencode_nvidia_maintainer() -> None: +def test_buyer_readme_identifies_the_opencode_gateway_maintainer() -> None: """Keep the public execution identity aligned with the audited workflow.""" readme = _read(README_PATH) assert "bounded Codex maintainer" not in readme assert "bounded OpenCode maintainer" in readme + assert "contextual-orchestrator gateway" in readme assert "`NVIDIA_NIM_API_KEY`" in readme assert "COPILOT_GITHUB_TOKEN" not in readme @@ -228,3 +276,10 @@ def test_offline_verifier_materializes_the_complete_repository_contract() -> Non assert verifier.index(root_loop) < verifier.index("ruff check .") assert verifier.index(root_copy) < verifier.index("pytest -q") assert verifier.index(root_copy) < verifier.index(compileall) + + +def test_gateway_evidence_directory_is_excluded_from_the_captured_patch() -> None: + """Keep the sidecar's own strix_runs/ evidence out of the model's diff.""" + gitignore = _read(REPOSITORY_ROOT / ".gitignore") + + assert "strix_runs/" in gitignore