From 7e6ba5bf8b325e9175a91e3ebf80688d7d8c66d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:19:21 +0900 Subject: [PATCH 1/4] fix(ci): route hourly dev agent through contextual-orchestrator gateway hourly-product-development.yml called NVIDIA NIM directly (integrate.api.nvidia.com), bypassing the org's governed contextual-orchestrator gateway and its fail-closed orchestrator/free pool entirely -- an org-wide audit finding tracked in #35. Vendors ContextualWisdomLab/contextual-orchestrator at the same pinned commit ContextualWisdomLab/.github's central review sidecar uses, installs its hash-pinned dependencies, and starts it locally seeded from whichever of the five organization provider secrets (BYTEZ, NVIDIA_NIM, NVIDIA_NIM_SUB, OPENROUTER, OPENAI) are present. OpenCode is repointed at contextual_orchestrator_gateway/orchestrator/free through an {env:CONTEXTUAL_ORCHESTRATOR_TOKEN} reference instead of a direct NVIDIA provider id/base URL, so it never holds a raw provider key. The sequential 3-model OPENCODE_MODEL_CANDIDATES fallback loop is retired: the gateway's own auto-discovery now owns fallback across real providers and models, matching the pattern already landed in contextual-orchestrator's own opencode-hourly-loop.yml (PR #1013). Verified directly against the pinned commit rather than assumed: its scripts/ci/serve_seeded_gateway.py predates that script's own --auth-token-key (KV-registered token) support upstream, so this uses --auth-token with the generated per-run token instead, confirmed end-to-end (healthz, then a chat/completions request against orchestrator/free returning a clean fail-closed "no enabled zero-cost model is available" with no providers configured). This is unrelated to DiagramWeave's own product Contextual Orchestrator adapter (packages/contextual-orchestrator, ADR-0006), which already routes correctly and is unchanged. Updates the pinned workflow-contract and runtime-budget tests plus docs/operations/hourly-development.md to match. actionlint clean (same pre-existing shellcheck SC2016 note on the untouched PR-evidence GraphQL step). node --test: all touched contract tests pass; the full suite's only failures are 12 pre-existing packages/cli symlink tests that also fail on an unmodified checkout in this macOS sandbox (/private/tmp is itself a symlink there) and are unrelated to this change. Co-Authored-By: Claude Sonnet 5 --- .../workflows/hourly-product-development.yml | 174 ++++++++++++------ CHANGELOG.md | 12 ++ docs/operations/hourly-development.md | 21 ++- tests/hourly-runtime-budget-contract.test.js | 31 ++-- tests/workflow-contract.test.js | 59 ++++-- 5 files changed, 199 insertions(+), 98 deletions(-) diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 6ee3a8a..e62dfe0 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,12 @@ 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 @@ -452,19 +455,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' @@ -483,7 +502,73 @@ 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" + echo "CONTEXTUAL_ORCHESTRATOR_TOKEN=$gateway_token" >>"$GITHUB_ENV" + 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 & + 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 + echo "gateway ready" + exit 0 + fi + sleep 2 + done + echo "::error::contextual-orchestrator gateway did not become healthy" + tail -50 "$RUNNER_TEMP/contextual-orchestrator-gateway.log" + exit 1 + + - name: Point OpenCode at the local gateway if: steps.gate.outputs.dispatch == 'true' shell: bash run: | @@ -491,38 +576,17 @@ jobs: cat >"$GITHUB_WORKSPACE/opencode.json" <<'CONFIG' { "$schema": "https://opencode.ai/config.json", - "enabled_providers": ["nvidia-nim"], - "model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5", - "small_model": "nvidia-nim/meta/llama-3.3-70b-instruct", + "model": "contextual_orchestrator_gateway/orchestrator/free", "provider": { - "nvidia-nim": { + "contextual_orchestrator_gateway": { "npm": "@ai-sdk/openai-compatible", - "name": "NVIDIA NIM", + "name": "Contextual Orchestrator Gateway", "options": { - "baseURL": "https://integrate.api.nvidia.com/v1", - "apiKey": "{env:NVIDIA_API_KEY}" + "baseURL": "http://127.0.0.1:8000/v1", + "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)", - "tool_call": true, - "limit": {"context": 131072, "output": 8192} - }, - "meta/llama-3.3-70b-instruct": { - "name": "Meta Llama 3.3 70B Instruct (NIM)", - "tool_call": true, - "limit": {"context": 131072, "output": 8192} - } + "orchestrator/free": { "name": "fail-closed zero-cost orchestration" } } } } @@ -530,37 +594,25 @@ jobs: CONFIG echo "/opencode.json" >>"$GITHUB_WORKSPACE/.git/info/exclude" - - 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 ! 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 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' diff --git a/CHANGELOG.md b/CHANGELOG.md index f498fd9..667041b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ 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 diff --git a/docs/operations/hourly-development.md b/docs/operations/hourly-development.md index 5a60508..91df75f 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. It is configured with an `{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}` reference to the gateway's own 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. 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..b4bf2b7 100644 --- a/tests/hourly-runtime-budget-contract.test.js +++ b/tests/hourly-runtime-budget-contract.test.js @@ -3,9 +3,14 @@ import test from 'node:test'; import { readRepositoryFile } from './helpers/repository-contract.js'; +// Reserve covers PR inventory/evidence gathering, vendoring the +// contextual-orchestrator gateway (git clone plus hash-pinned pip install), +// and post-agent verification/publish. Model-level fallback across providers +// is now the gateway's own auto-discovery concern, not this workflow's, so +// only one bounded model attempt needs to fit inside the job timeout. const orchestrationReserveSeconds = 30 * 60; -test('hourly development budget can execute every sequential model fallback', async () => { +test('hourly development budget can execute one full gateway-routed model attempt', async () => { const workflow = await readRepositoryFile( '.github/workflows/hourly-product-development.yml', ); @@ -16,28 +21,22 @@ test('hourly development budget can execute every sequential model fallback', as 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'); + assert.ok(modelTimeoutMatch, 'hourly development must bound the model attempt'); + assert.doesNotMatch( + workflow, + /OPENCODE_MODEL_CANDIDATES/, + 'model-level fallback belongs to the contextual-orchestrator gateway now, not a repository-level candidate list', + ); 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; + const requiredSeconds = 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`, + `job timeout ${jobTimeoutSeconds}s cannot cover one model attempt at ` + + `${modelTimeoutSeconds}s plus ${orchestrationReserveSeconds}s reserve`, ); }); diff --git a/tests/workflow-contract.test.js b/tests/workflow-contract.test.js index 8b8620f..7a90333 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,14 +56,27 @@ 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, /\{env:CONTEXTUAL_ORCHESTRATOR_TOKEN\}/); + assert.doesNotMatch(workflow, /integrate\.api\.nvidia\.com/); + assert.doesNotMatch(workflow, /nvidia-nim\/nvidia\//); + 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, /OPENCODE_VERSION: ["']1\.17\.13["']/); @@ -142,7 +155,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 +197,54 @@ 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.match(gatewayStep, /healthz/); + 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" --model contextual_orchestrator_gateway\/orchestrator\/free/, ); + assert.doesNotMatch(modelStep, /secrets\./); const publish = finalWorkflowStep(workflow, 'Publish one bounded mutation'); assert.match(publish, /remote_head_sha/); From a02bc00d6e755c468b04ef872b6324faf623df7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:46:56 +0900 Subject: [PATCH 2/4] docs(actions): remove stale direct NIM wording Describe the gateway-owned orchestrator/free route in generated PR prose, branch names, changelog text, and regression contracts. Assisted-by: OpenAI Codex Signed-off-by: Seongho Bae --- .github/workflows/hourly-product-development.yml | 4 ++-- CHANGELOG.md | 6 +++--- tests/workflow-contract.test.js | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index af44d06..9f03a4d 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -746,11 +746,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/CHANGELOG.md b/CHANGELOG.md index 667041b..991f255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,9 +25,9 @@ The project follows Semantic Versioning after the first release. 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/tests/workflow-contract.test.js b/tests/workflow-contract.test.js index 7a90333..1c46568 100644 --- a/tests/workflow-contract.test.js +++ b/tests/workflow-contract.test.js @@ -75,6 +75,10 @@ test('hourly development performs RCA remediation or one bounded gateway-routed assert.match(workflow, /\{env: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/); From e8ab5fc2a7819c211403300edcd7c9ac2271a479 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:51:38 +0900 Subject: [PATCH 3/4] fix: isolate hourly agent mutations --- .../workflows/hourly-product-development.yml | 61 +++++++++++++------ tests/hourly-runtime-budget-contract.test.js | 27 ++------ tests/workflow-contract.test.js | 24 ++++++-- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 9f03a4d..8591482 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -41,7 +41,6 @@ jobs: REPOSITORY_TOKEN: ${{ github.token }} OPENCODE_VERSION: "1.17.13" OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348 - 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; @@ -544,7 +543,8 @@ jobs: set -euo pipefail gateway_token="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" echo "::add-mask::$gateway_token" - echo "CONTEXTUAL_ORCHESTRATOR_TOKEN=$gateway_token" >>"$GITHUB_ENV" + 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) @@ -575,9 +575,18 @@ jobs: 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 gateway-routed development agent if: steps.gate.outputs.dispatch == 'true' shell: bash run: | set -euo pipefail - cd "$GITHUB_WORKSPACE" - prompt="$(cat "$RUNNER_TEMP/diagramweave-agent-prompt.md")" echo "::group::opencode contextual_orchestrator_gateway/orchestrator/free" - 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 contextual_orchestrator_gateway/orchestrator/free; then + 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 "::error::The gateway-routed development agent failed; no work was proposed." exit 1 @@ -631,24 +641,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 @@ -665,11 +688,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" diff --git a/tests/hourly-runtime-budget-contract.test.js b/tests/hourly-runtime-budget-contract.test.js index b4bf2b7..de18a8b 100644 --- a/tests/hourly-runtime-budget-contract.test.js +++ b/tests/hourly-runtime-budget-contract.test.js @@ -3,14 +3,7 @@ import test from 'node:test'; import { readRepositoryFile } from './helpers/repository-contract.js'; -// Reserve covers PR inventory/evidence gathering, vendoring the -// contextual-orchestrator gateway (git clone plus hash-pinned pip install), -// and post-agent verification/publish. Model-level fallback across providers -// is now the gateway's own auto-discovery concern, not this workflow's, so -// only one bounded model attempt needs to fit inside the job timeout. -const orchestrationReserveSeconds = 30 * 60; - -test('hourly development budget can execute one full gateway-routed model attempt', async () => { +test('hourly development leaves model duration to the three-hour job budget', async () => { const workflow = await readRepositoryFile( '.github/workflows/hourly-product-development.yml', ); @@ -18,25 +11,13 @@ test('hourly development budget can execute one full gateway-routed model attemp const jobTimeoutMatch = workflow.match( /runs-on: ubuntu-latest\n timeout-minutes: (\d+)/, ); - const modelTimeoutMatch = workflow.match( - /OPENCODE_RUN_TIMEOUT_SECONDS: ["'](\d+)["']/, - ); - assert.ok(jobTimeoutMatch, 'hourly development must declare a job timeout'); - assert.ok(modelTimeoutMatch, 'hourly development must bound the model attempt'); + 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', ); - - const jobTimeoutSeconds = Number(jobTimeoutMatch[1]) * 60; - const modelTimeoutSeconds = Number(modelTimeoutMatch[1]); - const requiredSeconds = modelTimeoutSeconds + orchestrationReserveSeconds; - - assert.ok( - jobTimeoutSeconds >= requiredSeconds, - `job timeout ${jobTimeoutSeconds}s cannot cover one model attempt at ` + - `${modelTimeoutSeconds}s plus ${orchestrationReserveSeconds}s reserve`, - ); }); diff --git a/tests/workflow-contract.test.js b/tests/workflow-contract.test.js index 1c46568..0ead88a 100644 --- a/tests/workflow-contract.test.js +++ b/tests/workflow-contract.test.js @@ -72,7 +72,7 @@ test('hourly development performs RCA remediation or one bounded gateway-routed 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, /\{env:CONTEXTUAL_ORCHESTRATOR_TOKEN\}/); + 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/); @@ -82,7 +82,21 @@ test('hourly development performs RCA remediation or one bounded gateway-routed 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/); @@ -236,7 +250,8 @@ test('hourly gate fails closed, preserves dry-run isolation, and selects exact-h gatewayStep, /NVIDIA_NIM_API_KEY: \$\{\{ secrets\.NVIDIA_NIM_API_KEY \}\}/, ); - assert.match(gatewayStep, /CONTEXTUAL_ORCHESTRATOR_TOKEN/); + assert.match(gatewayStep, /contextual-orchestrator\.token/); + assert.doesNotMatch(gatewayStep, /GITHUB_ENV/); assert.match(gatewayStep, /healthz/); const modelStep = workflowStep( @@ -246,9 +261,10 @@ test('hourly gate fails closed, preserves dry-run isolation, and selects exact-h ); assert.match( modelStep, - /opencode run "\$prompt" --model contextual_orchestrator_gateway\/orchestrator\/free/, + /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/); From 818621de82c2cde3a34b82e9988049f145d8b0af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:52:44 +0900 Subject: [PATCH 4/4] fix: verify orchestrator route readiness --- .../workflows/hourly-product-development.yml | 25 +++++++++++++++---- AGENTS.md | 5 +++- ARCHITECTURE.md | 4 ++- CLAUDE.md | 4 ++- docs/operations/hourly-development.md | 2 +- tests/workflow-contract.test.js | 2 ++ 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 8591482..4fa2ff2 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -558,17 +558,32 @@ jobs: --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 - echo "gateway ready" - exit 0 + gateway_ready=true + break fi sleep 2 done - echo "::error::contextual-orchestrator gateway did not become healthy" - tail -50 "$RUNNER_TEMP/contextual-orchestrator-gateway.log" - exit 1 + 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' 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/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 91df75f..b8c289e 100644 --- a/docs/operations/hourly-development.md +++ b/docs/operations/hourly-development.md @@ -64,7 +64,7 @@ Do not add a personal access token merely to dispatch another central scheduler 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. It is configured with an `{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}` reference to the gateway's own 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. +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. diff --git a/tests/workflow-contract.test.js b/tests/workflow-contract.test.js index 0ead88a..d2532a2 100644 --- a/tests/workflow-contract.test.js +++ b/tests/workflow-contract.test.js @@ -253,6 +253,8 @@ test('hourly gate fails closed, preserves dry-run isolation, and selects exact-h 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,