diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index cf66a76..4fa2ff2 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -17,8 +17,10 @@ concurrency: # The default token is read-only. The single bounded job may push either one # verified repair commit to an existing same-repository PR branch or one new -# product branch. The OpenCode process receives NVIDIA NIM credentials only; -# every GitHub credential is stripped from the agent process. +# product branch. Model traffic is served by a loopback contextual-orchestrator +# gateway sidecar seeded with the org's provider credentials; the OpenCode +# process itself receives only that gateway's ephemeral local bearer token, +# and every GitHub credential is stripped from the agent process. permissions: contents: read @@ -39,11 +41,11 @@ jobs: REPOSITORY_TOKEN: ${{ github.token }} 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: "2400" + # Same governed commit ContextualWisdomLab/.github's central review + # sidecar (scripts/ci/contextual_orchestrator_review_sidecar.sh) and + # ContextualWisdomLab/contextual-orchestrator's own hourly loop pin to; + # update only through a reviewed pull request. + ORCHESTRATOR_PIN_SHA: "045d17da5e2aea56a97e241ee158ab1628d78660" steps: - name: Select remediation or product-development mode id: gate @@ -454,19 +456,35 @@ jobs: cat "$RUNNER_TEMP/diagramweave-agent-prompt.md" } >>"$GITHUB_STEP_SUMMARY" - - name: Require the NVIDIA NIM model credential + - name: Require at least one contextual-orchestrator provider credential if: steps.gate.outputs.dispatch == 'true' shell: bash 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 only for the selected model-backed path." - echo "reason=nim_api_key_unavailable" >>"$GITHUB_STEP_SUMMARY" + # Missing individual secrets are allowed; the gateway's own + # auto-discovery skips an unregistered provider. Only a fully empty + # set is fatal, matching ContextualWisdomLab/.github's central + # sidecar contract. + provider_secret_count=0 + for secret_name in BYTEZ_API_KEY NVIDIA_NIM_API_KEY NVIDIA_NIM_API_KEY_SUB OPENROUTER_API_KEY OPENAI_API_KEY; do + value="${!secret_name:-}" + if [ -n "$value" ]; then + echo "::add-mask::$value" + provider_secret_count=$((provider_secret_count + 1)) + fi + done + if [ "$provider_secret_count" -lt 1 ]; then + echo "::error::At least one of BYTEZ_API_KEY / NVIDIA_NIM_API_KEY / NVIDIA_NIM_API_KEY_SUB / OPENROUTER_API_KEY / OPENAI_API_KEY is required only for the selected model-backed path." + echo "reason=orchestrator_credential_unavailable" >>"$GITHUB_STEP_SUMMARY" exit 1 fi - echo "::add-mask::$NVIDIA_API_KEY" + echo "provider secrets present: $provider_secret_count of 5" >>"$GITHUB_STEP_SUMMARY" - name: Install the pinned OpenCode CLI if: steps.gate.outputs.dispatch == 'true' @@ -485,84 +503,143 @@ jobs: "${install_dir}/opencode" --version echo "$install_dir" >>"$GITHUB_PATH" - - name: Configure OpenCode for NVIDIA NIM + - name: Set up Python for the gateway sidecar + if: steps.gate.outputs.dispatch == 'true' + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 + with: + python-version: "3.12" + + - name: Vendor the contextual-orchestrator gateway + if: steps.gate.outputs.dispatch == 'true' + shell: bash + run: | + set -euo pipefail + orchestrator_source="$RUNNER_TEMP/contextual-orchestrator" + rm -rf "$orchestrator_source" + git clone --quiet --filter=blob:none --no-checkout \ + https://github.com/ContextualWisdomLab/contextual-orchestrator.git \ + "$orchestrator_source" + git -C "$orchestrator_source" -c advice.detachedHead=false \ + checkout --quiet "$ORCHESTRATOR_PIN_SHA" + checked_out="$(git -C "$orchestrator_source" rev-parse HEAD)" + if [ "$checked_out" != "$ORCHESTRATOR_PIN_SHA" ]; then + echo "::error::vendored contextual-orchestrator HEAD $checked_out != pin $ORCHESTRATOR_PIN_SHA" + exit 1 + fi + python -m pip install --quiet --disable-pip-version-check --no-cache-dir \ + --require-hashes \ + -r "$orchestrator_source/requirements.lock" + + - name: Start the contextual-orchestrator gateway with auto-discovery + if: steps.gate.outputs.dispatch == 'true' + shell: bash + 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 + gateway_token="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" + echo "::add-mask::$gateway_token" + token_file="$RUNNER_TEMP/contextual-orchestrator.token" + (umask 077 && printf '%s' "$gateway_token" >"$token_file") + cd "$RUNNER_TEMP/contextual-orchestrator" + # ORCHESTRATOR_PIN_SHA's scripts/ci/serve_seeded_gateway.py predates + # that script's own --auth-token-key (KV-registered bootstrap token) + # support upstream; --auth-token is the explicit-value flag proven + # to work at this exact pinned commit (verified directly against + # it). Re-verify this choice if the pin is ever bumped. + nohup python -m scripts.ci.serve_seeded_gateway \ + --serve \ + --agents examples/agents.mock.json \ + --auto-discover-model-agents \ + --auth-token "$gateway_token" \ + --host 127.0.0.1 --port 8000 \ + >"$RUNNER_TEMP/contextual-orchestrator-gateway.log" 2>&1 & + gateway_ready=false + for _attempt in $(seq 1 30); do + if curl -fsS -H "Authorization: Bearer $gateway_token" \ + http://127.0.0.1:8000/healthz >/dev/null 2>&1; then + gateway_ready=true + break + fi + sleep 2 + done + if [ "$gateway_ready" != true ]; then + echo "::error::contextual-orchestrator gateway did not become healthy" + tail -50 "$RUNNER_TEMP/contextual-orchestrator-gateway.log" + exit 1 + fi + response_file="$RUNNER_TEMP/contextual-orchestrator-preflight.json" + if ! curl -fsS \ + -H "Authorization: Bearer $gateway_token" \ + -H "Content-Type: application/json" \ + --data '{"model":"orchestrator/free","messages":[{"role":"user","content":"Reply READY."}],"max_tokens":16}' \ + http://127.0.0.1:8000/v1/chat/completions >"$response_file" || \ + ! jq -e '.choices[0].message.content | type == "string" and length > 0' \ + "$response_file" >/dev/null; then + echo "::error::contextual-orchestrator has no verified usable model route" + exit 1 + fi + echo "gateway and model route ready" + + - name: Point OpenCode at the local gateway if: steps.gate.outputs.dispatch == 'true' shell: bash run: | set -euo pipefail - cat >"$GITHUB_WORKSPACE/opencode.json" <<'CONFIG' + agent_workspace="$RUNNER_TEMP/diagramweave-agent" + rm -rf "$agent_workspace" + git clone --quiet --local --no-hardlinks "$GITHUB_WORKSPACE" "$agent_workspace" + mkdir -p "$RUNNER_TEMP/agent-home" "$RUNNER_TEMP/agent-config" + sudo chown 65532:65532 "$RUNNER_TEMP/agent-home" "$RUNNER_TEMP/agent-config" + sudo chown -R 65532:65532 "$agent_workspace" + sudo chown -R root:root "$agent_workspace/.git" + sudo chmod -R go-w "$agent_workspace/.git" + gateway_token="$(cat "$RUNNER_TEMP/contextual-orchestrator.token")" + cat >"$agent_workspace/opencode.json" <>"$GITHUB_WORKSPACE/.git/info/exclude" + sudo chown 65532:65532 "$agent_workspace/opencode.json" - - name: Run the NVIDIA NIM development agent + - name: Run the gateway-routed development agent if: steps.gate.outputs.dispatch == 'true' shell: bash - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} run: | set -euo pipefail - cd "$GITHUB_WORKSPACE" - prompt="$(cat "$RUNNER_TEMP/diagramweave-agent-prompt.md")" - status=1 - for model in $OPENCODE_MODEL_CANDIDATES; do - echo "::group::opencode $model" - if timeout --kill-after=30s "${OPENCODE_RUN_TIMEOUT_SECONDS}s" \ - env -u GH_TOKEN -u GITHUB_TOKEN -u REPOSITORY_TOKEN \ - -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u ACTIONS_ID_TOKEN_REQUEST_URL \ - opencode run "$prompt" --model "$model"; then - status=0 - echo "::endgroup::" - echo "Agent session completed with \`$model\`." >>"$GITHUB_STEP_SUMMARY" - break - fi + echo "::group::opencode contextual_orchestrator_gateway/orchestrator/free" + if ! sudo -u '#65532' -g '#65532' env -i \ + HOME="$RUNNER_TEMP/agent-home" \ + PATH="$PATH" \ + XDG_CONFIG_HOME="$RUNNER_TEMP/agent-config" \ + opencode run "$prompt" \ + --dir "$RUNNER_TEMP/diagramweave-agent" \ + --model contextual_orchestrator_gateway/orchestrator/free; then echo "::endgroup::" - echo "::warning::Model $model failed; discarding partial work and trying the next candidate." - git reset --hard HEAD - git clean -fd - done - if [ "$status" -ne 0 ]; then - echo "::error::Every NVIDIA NIM model candidate failed; no work was proposed." + echo "::error::The gateway-routed development agent failed; no work was proposed." exit 1 fi + echo "::endgroup::" + echo "Agent session completed via contextual-orchestrator's orchestrator/free pool." >>"$GITHUB_STEP_SUMMARY" - name: Set up Node.js for exact repository verification if: steps.gate.outputs.dispatch == 'true' @@ -579,24 +656,37 @@ jobs: EXECUTION_MODE: ${{ steps.gate.outputs.mode }} run: | set -euo pipefail - cd "$GITHUB_WORKSPACE" - rm -f opencode.json + agent_workspace="$RUNNER_TEMP/diagramweave-agent" + sudo chown -R "$(id -u):$(id -g)" "$agent_workspace" + rm -f "$agent_workspace/opencode.json" if [ "$EXECUTION_MODE" = "remediation" ]; then - rm -f PR_MESSAGE.md + rm -f "$agent_workspace/PR_MESSAGE.md" fi meaningful_status="$( - git status --porcelain --untracked-files=all \ + git -C "$agent_workspace" status --porcelain --untracked-files=all \ | grep -vE '^\?\? PR_MESSAGE\.md$' || true )" if [ -z "$meaningful_status" ]; then - rm -f PR_MESSAGE.md + rm -f "$agent_workspace/PR_MESSAGE.md" echo "mutation=false" >>"$GITHUB_OUTPUT" echo "No safe repository mutation was produced." >>"$GITHUB_STEP_SUMMARY" exit 0 fi echo "mutation=true" >>"$GITHUB_OUTPUT" + git -C "$agent_workspace" add -N -- . + git -C "$agent_workspace" diff --name-only -z --diff-filter=ACDMRTUXB | \ + python -c 'import os,sys; root=os.environ["RUNNER_TEMP"]+"/diagramweave-agent"; paths=sys.stdin.buffer.read().split(b"\0"); bad=[p for p in paths if p and os.path.lexists(q:=os.path.join(root, os.fsdecode(p))) and (os.path.islink(q) or not os.path.isfile(q))]; sys.exit(f"unsafe changed file: {bad[0]!r}" if bad else 0)' + if git -C "$agent_workspace" diff --summary | grep -Eq 'mode change|create mode 160000'; then + echo "::error::The agent proposed an unsafe file-mode or submodule change." + exit 1 + fi + patch_file="$RUNNER_TEMP/diagramweave-agent.patch" + git -C "$agent_workspace" diff --binary --full-index --no-ext-diff >"$patch_file" + cd "$GITHUB_WORKSPACE" + git apply --check "$patch_file" + git apply "$patch_file" git diff --check npm ci --ignore-scripts --no-audit --no-fund npm run verify @@ -613,11 +703,13 @@ jobs: TARGET_HEAD_SHA: ${{ steps.gate.outputs.target_head_sha }} run: | set -euo pipefail + export HOME="$RUNNER_TEMP/publisher-home" + mkdir -p "$HOME" cd "$GITHUB_WORKSPACE" - rm -f opencode.json git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config core.hooksPath /dev/null git_basic_auth="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')" git_auth_header="AUTHORIZATION: basic $git_basic_auth" echo "::add-mask::$git_basic_auth" @@ -694,11 +786,11 @@ jobs: tail -n +2 PR_MESSAGE.md >"$body_file" rm -f PR_MESSAGE.md else - echo "Autonomous NVIDIA NIM increment; see the diff and CHANGELOG.md." \ + echo "Autonomous orchestrator/free increment; see the diff and CHANGELOG.md." \ >"$body_file" fi - branch="nim-agent/product-dev-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + branch="orchestrator-agent/product-dev-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" git checkout -b "$branch" git add -A git commit -m "$title" diff --git a/AGENTS.md b/AGENTS.md index d343797..9e28b5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,10 @@ ## Automation contract -- Scheduled product development uses OpenCode with `NVIDIA_NIM_API_KEY`; do not use or introduce `COPILOT_GITHUB_TOKEN`. +- Scheduled product development routes OpenCode only through the pinned local + `contextual-orchestrator` gateway and its `orchestrator/free` pool. Provider + credentials remain gateway-owned; do not pass them to OpenCode or introduce + `COPILOT_GITHUB_TOKEN`. - Do not change the credential contract of the existing independent review agent. - Prefer the immutable organization-central `.github` workflows over repository-local policy copies. - Process open PRs before creating another bounded product-development PR. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index eb00037..7508370 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -64,7 +64,9 @@ the trust kernel. 14. LSP positions use UTF-16 code units; multilingual and emoji ranges are regression-tested across LF, CRLF, and CR source. 15. Organization-central `.github` workflows own merge governance. Scheduled - product development uses OpenCode with `NVIDIA_NIM_API_KEY`, not Copilot. + product development routes OpenCode through the pinned local + `contextual-orchestrator` gateway's `orchestrator/free` pool, not Copilot + or a direct provider credential. 16. No release occurs while packages remain `0.0.0` under `Unreleased` or while Studio, cross-platform runtime evidence, signing, SBOM/provenance, and rollback evidence remain incomplete. diff --git a/CHANGELOG.md b/CHANGELOG.md index f498fd9..991f255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,26 @@ The project follows Semantic Versioning after the first release. ### Changed +- Hourly development's own OpenCode reasoning backend no longer calls NVIDIA + NIM directly. It now vendors a pinned `ContextualWisdomLab/contextual-orchestrator` + gateway sidecar, seeds it from whichever of five organization provider + secrets are present, and routes through the fail-closed, zero-cost + `orchestrator/free` virtual pool, matching the pattern already landed in + `ContextualWisdomLab/.github`'s central review sidecar and + `contextual-orchestrator`'s own hourly loop. The sequential 3-model + `OPENCODE_MODEL_CANDIDATES` fallback list is retired; the gateway's own + auto-discovery now owns fallback across real providers and models. This is + unrelated to DiagramWeave's own product `Contextual Orchestrator` adapter + (`packages/contextual-orchestrator`), which is unchanged. See + [ContextualWisdomLab/DiagramWeave#35](https://github.com/ContextualWisdomLab/DiagramWeave/issues/35). - Hourly development now routes an open same-repository pull request into an RCA-driven exact-head remediation session, verifies candidate actions against live review and Check evidence, publishes only a normal fast-forward repair after full repository verification, and re-fetches post-push state. - When no pull request is open, the same credential-isolated OpenCode workflow - uses NVIDIA NIM (`NVIDIA_NIM_API_KEY`) to create at most one bounded product - pull request; it no longer assumes `COPILOT_GITHUB_TOKEN` or the Copilot - Agent Tasks API and rechecks the queue immediately before creation. + uses the gateway-owned `orchestrator/free` pool to create at most one bounded + product pull request; it no longer assumes `COPILOT_GITHUB_TOKEN` or the + Copilot Agent Tasks API and rechecks the queue immediately before creation. ### Security diff --git a/CLAUDE.md b/CLAUDE.md index 4a3365d..e07eb68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,9 @@ Before changing code: 7th-edition references in durable documentation; 5. keep JSON-RPC, source, URI, renderer, filesystem, LLM, and credential inputs inside their explicit trust boundaries; -6. use OpenCode with `NVIDIA_NIM_API_KEY` for scheduled product-development +6. route scheduled OpenCode work through the pinned local + `contextual-orchestrator` gateway's `orchestrator/free` pool; keep provider + credentials out of the agent process automation and never introduce `COPILOT_GITHUB_TOKEN`; 7. do not weaken checks, branch protection, review independence, package gates, or release evidence. diff --git a/docs/operations/hourly-development.md b/docs/operations/hourly-development.md index 5a60508..b8c289e 100644 --- a/docs/operations/hourly-development.md +++ b/docs/operations/hourly-development.md @@ -40,7 +40,7 @@ Independent approval cannot be manufactured. Queued or pending Checks cannot be ### Product-development mode -When the verified inventory contains no open pull request, the workflow may run exactly one bounded OpenCode session against NVIDIA NIM and package one buyer-visible increment as one new pull request. Immediately before creating that PR it re-fetches the queue; if another PR appeared after the initial gate, it fails closed rather than creating duplicate work. +When the verified inventory contains no open pull request, the workflow may run exactly one bounded OpenCode session routed through the local contextual-orchestrator gateway sidecar and package one buyer-visible increment as one new pull request. Immediately before creating that PR it re-fetches the queue; if another PR appeared after the initial gate, it fails closed rather than creating duplicate work. The delegated session preserves DiagramWeave's source-first manual editing mode, uses or improves Contextual Orchestrator for product LLM work, retains modular MSA compatibility with central `.github`, naruon, and other CWL services, and satisfies the repository's test, coverage, docstring, security, documentation, and design contracts. @@ -62,13 +62,15 @@ Do not add a personal access token merely to dispatch another central scheduler ### Remediation and product-development agent -The development agent authenticates to NVIDIA NIM with the `NVIDIA_NIM_API_KEY` organization secret. That value is injected as `NVIDIA_API_KEY` only after the inventory gate selects an actual model-backed path, and only into the OpenCode model-execution step. +The development agent's model traffic is served by a loopback `contextual-orchestrator` gateway sidecar, not by a direct provider call. The workflow vendors `ContextualWisdomLab/contextual-orchestrator` at a pinned commit (`ORCHESTRATOR_PIN_SHA`, the same commit `ContextualWisdomLab/.github`'s central review sidecar and `contextual-orchestrator`'s own hourly loop pin to), installs its hash-pinned dependencies, and starts it with auto-discovery against whichever of five organization provider secrets are present: `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY`. At least one must be present for a model-backed path; a missing individual secret only narrows the gateway's discovered pool. + +OpenCode itself never receives a raw provider key. Its isolated configuration contains only the gateway's ephemeral, per-run bearer token, and its model is set to the gateway's fail-closed, zero-cost virtual pool: `contextual_orchestrator_gateway/orchestrator/free`. The gateway's own auto-discovery, not this workflow, owns fallback across real underlying models and providers. Before OpenCode starts, the workflow checks both gateway liveness and one non-empty completion from that virtual route; invalid credentials or an empty usable pool therefore fail before the agent runs. The built-in token is used by shell steps for inventory, bounded evidence capture, exact-head comparison, branch push, and product PR creation. The OpenCode process is launched with `GH_TOKEN`, `GITHUB_TOKEN`, `REPOSITORY_TOKEN`, and Actions OIDC request variables removed from its environment. Git publication uses an ephemeral masked HTTP header rather than a credential-bearing remote URL. This replaces the retired Copilot Agent Tasks integration and its `COPILOT_GITHUB_TOKEN` user token. No Copilot subscription is required and the Agent Tasks preview API is not called. -If a model-backed path is selected and the NIM secret is absent, the run fails visibly and creates nothing. Missing credentials are not repaired by inventing new secret names. +If a model-backed path is selected and every one of the five provider secrets is absent, the run fails visibly and creates nothing. Missing credentials are not repaired by inventing new secret names. ## Dry run @@ -77,11 +79,11 @@ Use the Actions interface to run either workflow with `dry_run: true`. - PR maintenance invokes the pinned central scheduler in dry-run mode and does not require a separate repository-dispatch credential. - The development workflow performs the live PR inventory, selects the mode that a real run would use, and prints the exact bounded agent contract without checking out code, reading the model secret, or invoking a model. -A dry run does not require `NVIDIA_NIM_API_KEY`. It still requires successful pull-request inventory so an API failure cannot be disguised as a successful simulation. +A dry run does not require any of the five gateway provider secrets, including `NVIDIA_NIM_API_KEY`. It still requires successful pull-request inventory so an API failure cannot be disguised as a successful simulation. ## Contextual Orchestrator boundary -All product LLM functionality must use or improve `ContextualWisdomLab/contextual-orchestrator` through the DiagramWeave adapter. The hourly workflow itself does not call the product inference API. Its OpenCode session uses NVIDIA NIM only as the delegated development agent's reasoning backend and instructs the agent to preserve the Contextual Orchestrator product boundary. +All product LLM functionality must use or improve `ContextualWisdomLab/contextual-orchestrator` through the DiagramWeave adapter (`packages/contextual-orchestrator`, see `docs/operations/contextual-orchestrator.md`). The hourly workflow itself does not call that product inference API path. Its OpenCode session's own reasoning backend is served by a separate, workflow-local `contextual-orchestrator` gateway sidecar (see above) routed to the same organization's `orchestrator/free` pool, and the delegated agent is instructed to preserve the product's distinct Contextual Orchestrator boundary rather than conflate the two. ## Failure handling @@ -93,9 +95,10 @@ All product LLM functionality must use or improve `ContextualWisdomLab/contextua - Valid finding has a feasible repository change: reproduce it test-first, implement the smallest correction, run complete verification, push normally, and re-fetch exact-head state. - Independent approval remains absent: do not synthesize it. - A required Check is queued or pending: do not call it successful. Continue with the next safe, non-conflicting activity when one exists. -- Dry run: print the selected task contract without reading or requiring the NVIDIA model credential. -- Selected model path lacks `NVIDIA_NIM_API_KEY`: fail before installing or invoking OpenCode. -- Every NVIDIA NIM model candidate fails: reset partial work, fail visibly, and publish nothing. +- Dry run: print the selected task contract without reading or requiring any gateway provider credential. +- Selected model path lacks every one of the five gateway provider secrets: fail before installing or invoking OpenCode. +- The vendored gateway sidecar fails to become healthy, or the checked-out commit does not match `ORCHESTRATOR_PIN_SHA`: fail before invoking OpenCode. +- The gateway-routed development agent fails or times out: fail visibly and publish nothing. - Repository verification fails: publish nothing. - A product PR appears between inventory and publication: fail closed and create no duplicate pull request. - Delayed schedule: rely on the next scheduled run or invoke a manual dry run; do not add a duplicate scheduler. @@ -105,4 +108,4 @@ All product LLM functionality must use or improve `ContextualWisdomLab/contextua To disable autonomous model-backed remediation and product creation, disable `Hourly Product Development` in GitHub Actions or remove its `schedule` event through a pull request. To disable repository-local hourly PR governance, disable `Hourly PR Maintenance`; organization-central event and sweep policies may still process PRs according to organization policy. -Do not remove `NVIDIA_NIM_API_KEY` as an intentional disablement mechanism. Once deterministic gates select the model path, a missing required credential is an operational failure and remains visible rather than producing a false-green skip. +Do not remove `NVIDIA_NIM_API_KEY` or the other four gateway provider secrets as an intentional disablement mechanism. Once deterministic gates select the model path, a fully missing credential set is an operational failure and remains visible rather than producing a false-green skip. diff --git a/tests/hourly-runtime-budget-contract.test.js b/tests/hourly-runtime-budget-contract.test.js index 08901ae..de18a8b 100644 --- a/tests/hourly-runtime-budget-contract.test.js +++ b/tests/hourly-runtime-budget-contract.test.js @@ -3,9 +3,7 @@ import test from 'node:test'; import { readRepositoryFile } from './helpers/repository-contract.js'; -const orchestrationReserveSeconds = 30 * 60; - -test('hourly development budget can execute every sequential model fallback', async () => { +test('hourly development leaves model duration to the three-hour job budget', async () => { const workflow = await readRepositoryFile( '.github/workflows/hourly-product-development.yml', ); @@ -13,31 +11,13 @@ test('hourly development budget can execute every sequential model fallback', as const jobTimeoutMatch = workflow.match( /runs-on: ubuntu-latest\n timeout-minutes: (\d+)/, ); - const modelTimeoutMatch = workflow.match( - /OPENCODE_RUN_TIMEOUT_SECONDS: ["'](\d+)["']/, - ); - const candidatesMatch = workflow.match( - /OPENCODE_MODEL_CANDIDATES: >-\n((?: \S.*\n)+) OPENCODE_RUN_TIMEOUT_SECONDS:/, - ); - assert.ok(jobTimeoutMatch, 'hourly development must declare a job timeout'); - assert.ok(modelTimeoutMatch, 'hourly development must bound each model attempt'); - assert.ok(candidatesMatch, 'hourly development must declare its model fallback pool'); - - const jobTimeoutSeconds = Number(jobTimeoutMatch[1]) * 60; - const modelTimeoutSeconds = Number(modelTimeoutMatch[1]); - const modelCandidates = candidatesMatch[1] - .trim() - .split('\n') - .map((candidate) => candidate.trim()) - .filter(Boolean); - const requiredSeconds = - modelCandidates.length * modelTimeoutSeconds + orchestrationReserveSeconds; - - assert.ok(modelCandidates.length > 1, 'the fallback pool must remain explicit'); - assert.ok( - jobTimeoutSeconds >= requiredSeconds, - `job timeout ${jobTimeoutSeconds}s cannot cover ${modelCandidates.length} sequential ` + - `model attempts at ${modelTimeoutSeconds}s plus ${orchestrationReserveSeconds}s reserve`, + assert.ok(Number(jobTimeoutMatch[1]) >= 180); + assert.doesNotMatch(workflow, /OPENCODE_RUN_TIMEOUT_SECONDS/); + assert.doesNotMatch(workflow, /timeout --kill-after=30s/); + assert.doesNotMatch( + workflow, + /OPENCODE_MODEL_CANDIDATES/, + 'model-level fallback belongs to the contextual-orchestrator gateway now, not a repository-level candidate list', ); }); diff --git a/tests/workflow-contract.test.js b/tests/workflow-contract.test.js index 8b8620f..d2532a2 100644 --- a/tests/workflow-contract.test.js +++ b/tests/workflow-contract.test.js @@ -46,7 +46,7 @@ test('hourly PR maintenance uses only the pinned reusable governance workflow', ); }); -test('hourly development performs RCA remediation or one bounded NIM product increment', async () => { +test('hourly development performs RCA remediation or one bounded gateway-routed product increment', async () => { const workflow = await readRepositoryFile( '.github/workflows/hourly-product-development.yml', ); @@ -56,16 +56,47 @@ test('hourly development performs RCA remediation or one bounded NIM product inc assert.match(workflow, /group: hourly-product-development-\$\{\{ github\.repository \}\}/); assert.match(workflow, /cancel-in-progress: false/); assert.match(workflow, new RegExp(`github\\.repository == '${repositoryName}'`)); - assert.match(workflow, /NVIDIA_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/); + assert.match(workflow, /BYTEZ_API_KEY: \$\{\{ secrets\.BYTEZ_API_KEY \}\}/); + assert.match(workflow, /NVIDIA_NIM_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/); + assert.match(workflow, /NVIDIA_NIM_API_KEY_SUB: \$\{\{ secrets\.NVIDIA_NIM_API_KEY_SUB \}\}/); + assert.match(workflow, /OPENROUTER_API_KEY: \$\{\{ secrets\.OPENROUTER_API_KEY \}\}/); + assert.match(workflow, /OPENAI_API_KEY: \$\{\{ secrets\.OPENAI_API_KEY \}\}/); assert.match(workflow, /gh pr list/); assert.match(workflow, /--state open/); assert.match(workflow, /reason=open_pull_request_remediation/); assert.match(workflow, /reason=pull_request_inventory_unavailable/); - assert.match(workflow, /reason=nim_api_key_unavailable/); - assert.match(workflow, /https:\/\/integrate\.api\.nvidia\.com\/v1/); - assert.match(workflow, /\{env:NVIDIA_API_KEY\}/); + assert.match(workflow, /reason=orchestrator_credential_unavailable/); + assert.match(workflow, /ContextualWisdomLab\/contextual-orchestrator\.git/); + assert.match(workflow, /ORCHESTRATOR_PIN_SHA: ["']045d17da5e2aea56a97e241ee158ab1628d78660["']/); + assert.match(workflow, /--require-hashes/); + assert.match(workflow, /--auto-discover-model-agents/); + assert.match(workflow, /scripts\.ci\.serve_seeded_gateway/); + assert.match(workflow, /contextual_orchestrator_gateway\/orchestrator\/free/); + assert.match(workflow, /contextual-orchestrator\.token/); + assert.doesNotMatch(workflow, /integrate\.api\.nvidia\.com/); + assert.doesNotMatch(workflow, /nvidia-nim\/nvidia\//); + assert.doesNotMatch(workflow, /Autonomous NVIDIA NIM increment/); + assert.doesNotMatch(workflow, /nim-agent\/product-dev/); + assert.match(workflow, /Autonomous orchestrator\/free increment/); + assert.match(workflow, /orchestrator-agent\/product-dev/); + assert.doesNotMatch(workflow, /enabled_providers/); + assert.doesNotMatch(workflow, /OPENCODE_MODEL_CANDIDATES/); assert.match(workflow, /persist-credentials: false/); - assert.match(workflow, /env -u GH_TOKEN -u GITHUB_TOKEN -u REPOSITORY_TOKEN/); + assert.match(workflow, /sudo -u '#65532' -g '#65532' env -i/); + assert.match(workflow, /git clone --quiet --local --no-hardlinks/); + assert.match(workflow, /sudo chmod -R go-w "\$agent_workspace\/\.git"/); + assert.match(workflow, /sudo chown -R "\$\(id -u\):\$\(id -g\)" "\$agent_workspace"/); + assert.match(workflow, /git apply --check "\$patch_file"/); + assert.match(workflow, /git config core\.hooksPath \/dev\/null/); + assert.doesNotMatch(workflow, /OPENCODE_RUN_TIMEOUT_SECONDS/); + assert.doesNotMatch( + workflowStep( + workflow, + 'Run the gateway-routed development agent', + 'Set up Node.js for exact repository verification', + ), + /timeout --kill-after/, + ); assert.match(workflow, /OPENCODE_VERSION: ["']1\.17\.13["']/); assert.match(workflow, /sha256sum -c -/); assert.match(workflow, /gh pr create/); @@ -142,7 +173,7 @@ test('hourly gate fails closed, preserves dry-run isolation, and selects exact-h assert.ok(jobStart >= 0 && stepsStart > jobStart); assert.doesNotMatch( workflow.slice(jobStart, stepsStart), - /NVIDIA_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/, + /secrets\.(BYTEZ_API_KEY|NVIDIA_NIM_API_KEY|NVIDIA_NIM_API_KEY_SUB|OPENROUTER_API_KEY|OPENAI_API_KEY)/, ); const checkout = workflowStep( @@ -184,32 +215,58 @@ test('hourly gate fails closed, preserves dry-run isolation, and selects exact-h const dryRunStep = workflowStep( workflow, 'Record dry-run decision', - 'Require the NVIDIA NIM model credential', + 'Require at least one contextual-orchestrator provider credential', ); assert.match(dryRunStep, /steps\.gate\.outputs\.reason == 'dry_run'/); const credentialStep = workflowStep( workflow, - 'Require the NVIDIA NIM model credential', + 'Require at least one contextual-orchestrator provider credential', 'Install the pinned OpenCode CLI', ); assert.match(credentialStep, /if: steps\.gate\.outputs\.dispatch == 'true'/); assert.match( credentialStep, - /NVIDIA_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/, + /NVIDIA_NIM_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/, ); - assert.match(credentialStep, /reason=nim_api_key_unavailable/); + assert.match(credentialStep, /provider_secret_count/); + assert.match(credentialStep, /reason=orchestrator_credential_unavailable/); assert.match(credentialStep, /exit 1/); + const vendorStep = workflowStep( + workflow, + 'Vendor the contextual-orchestrator gateway', + 'Start the contextual-orchestrator gateway with auto-discovery', + ); + assert.match(vendorStep, /ORCHESTRATOR_PIN_SHA/); + assert.match(vendorStep, /--require-hashes/); + + const gatewayStep = workflowStep( + workflow, + 'Start the contextual-orchestrator gateway with auto-discovery', + 'Point OpenCode at the local gateway', + ); + assert.match( + gatewayStep, + /NVIDIA_NIM_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/, + ); + assert.match(gatewayStep, /contextual-orchestrator\.token/); + assert.doesNotMatch(gatewayStep, /GITHUB_ENV/); + assert.match(gatewayStep, /healthz/); + assert.match(gatewayStep, /\/v1\/chat\/completions/); + assert.match(gatewayStep, /choices\[0\]\.message\.content/); + const modelStep = workflowStep( workflow, - 'Run the NVIDIA NIM development agent', + 'Run the gateway-routed development agent', 'Set up Node.js for exact repository verification', ); assert.match( modelStep, - /NVIDIA_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/, + /opencode run "\$prompt"[\s\S]*--model contextual_orchestrator_gateway\/orchestrator\/free/, ); + assert.doesNotMatch(modelStep, /secrets\./); + assert.match(modelStep, /RUNNER_TEMP\/diagramweave-agent/); const publish = finalWorkflowStep(workflow, 'Publish one bounded mutation'); assert.match(publish, /remote_head_sha/);