From 95dca3abedaca06fc3723d953dec90121bd044b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:00:24 +0900 Subject: [PATCH 01/12] feat: route hourly product agents through contextual orchestrator --- .../hourly-nim-product-development.yml | 132 ++++++++++++------ docs/adr/0010-adaptive-llm-orchestration.md | 2 +- ...-hourly-contextual-orchestrator-gateway.md | 74 ++++++++++ docs/adr/README.md | 2 + .../hourly-nim-opencode-development.md | 22 +-- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 51 ++++--- scripts/run_contextual_orchestrator.py | 106 ++++++++++++++ .../test_hourly_nim_product_development.py | 32 +++-- 8 files changed, 339 insertions(+), 82 deletions(-) create mode 100644 docs/adr/0017-hourly-contextual-orchestrator-gateway.md create mode 100644 scripts/run_contextual_orchestrator.py diff --git a/.github/workflows/hourly-nim-product-development.yml b/.github/workflows/hourly-nim-product-development.yml index 93ec6061c..2408b1b9b 100644 --- a/.github/workflows/hourly-nim-product-development.yml +++ b/.github/workflows/hourly-nim-product-development.yml @@ -1,4 +1,4 @@ -name: Hourly NVIDIA NIM Product Development +name: Hourly Contextual Orchestrator Product Development on: workflow_dispatch: @@ -25,10 +25,10 @@ env: # for the newer Linux x64 asset is committed. OPENCODE_VERSION: "1.17.13" OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348 - OPENCODE_MODELS: >- - 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 + CONTEXTUAL_ORCHESTRATOR_COMMIT: e226e1197bdfc890c9d8e5b9b648c78857d7e465 + CONTEXTUAL_ORCHESTRATOR_SHA256: 964b22ff577e3862b761af847ccad65489bb3f8fc750c8f84fcf8628df096673 + CONTEXTUAL_ORCHESTRATOR_PORT: "18000" + OPENCODE_MODEL: contextual-orchestrator/contextual-orchestrator OPENCODE_RUN_TIMEOUT_SECONDS: "900" OPENCODE_KILL_GRACE_SECONDS: "30" MAX_CHANGED_FILES: "40" @@ -60,7 +60,7 @@ jobs: shell: bash env: GH_TOKEN: ${{ github.token }} - NIM_CONFIGURED: ${{ secrets.NVIDIA_NIM_API_KEY != '' }} + ORCHESTRATOR_KEYS_CONFIGURED: ${{ secrets.BYTEZ_API_KEY != '' && secrets.NVIDIA_NIM_API_KEY != '' && secrets.NVIDIA_NIM_API_KEY_SUB != '' && secrets.OPENROUTER_API_KEY != '' && secrets.OPENAI_API_KEY != '' }} APP_ID_CONFIGURED: ${{ vars.TEPP_MAINTAINER_APP_CLIENT_ID != '' }} APP_KEY_CONFIGURED: ${{ secrets.TEPP_MAINTAINER_APP_PRIVATE_KEY != '' }} run: | @@ -75,9 +75,9 @@ jobs: echo 'reason=open_pull_request' >>"$GITHUB_OUTPUT" exit 0 fi - if [ "$NIM_CONFIGURED" != true ] && [ "$DRY_RUN" != true ]; then + if [ "$ORCHESTRATOR_KEYS_CONFIGURED" != true ] && [ "$DRY_RUN" != true ]; then echo 'dispatch=false' >>"$GITHUB_OUTPUT" - echo 'reason=nim_api_key_unavailable' >>"$GITHUB_OUTPUT" + echo 'reason=contextual_orchestrator_credentials_unavailable' >>"$GITHUB_OUTPUT" exit 0 fi if { [ "$APP_ID_CONFIGURED" != true ] || [ "$APP_KEY_CONFIGURED" != true ]; } \ @@ -174,7 +174,22 @@ jobs: install -D -m 0755 "$RUNNER_TEMP/opencode" "$HOME/.opencode/bin/opencode" echo "$HOME/.opencode/bin" >>"$GITHUB_PATH" - - name: Configure OpenCode for NVIDIA NIM only + - name: Install checksum-pinned contextual-orchestrator + if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != 'true' + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/contextual-orchestrator.tar.gz" + package_root="$RUNNER_TEMP/contextual-orchestrator" + install -d -m 0700 "$package_root" + curl --fail --silent --show-error --location --proto '=https' \ + --connect-timeout 10 --max-time 120 --output "$archive" \ + "https://github.com/ContextualWisdomLab/contextual-orchestrator/archive/${CONTEXTUAL_ORCHESTRATOR_COMMIT}.tar.gz" + printf '%s %s\n' "$CONTEXTUAL_ORCHESTRATOR_SHA256" "$archive" | sha256sum -c - + tar -xzf "$archive" --strip-components=1 -C "$package_root" + test -f "$package_root/contextual_orchestrator/__main__.py" + + - name: Configure OpenCode for contextual-orchestrator if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != 'true' shell: bash run: | @@ -185,7 +200,7 @@ jobs: "autoupdate": false, "lsp": false, "mcp": {}, - "enabled_providers": ["nvidia-nim"], + "enabled_providers": ["contextual-orchestrator"], "permission": { "*": "allow", "external_directory": "deny", @@ -209,17 +224,15 @@ jobs: } }, "provider": { - "nvidia-nim": { + "contextual-orchestrator": { "npm": "@ai-sdk/openai-compatible", - "name": "NVIDIA NIM", + "name": "Contextual Orchestrator", "options": { - "baseURL": "https://integrate.api.nvidia.com/v1", - "apiKey": "{env:NVIDIA_NIM_API_KEY}" + "baseURL": "http://127.0.0.1:18000/v1", + "apiKey": "{env:OPENCODE_GATEWAY_TOKEN}" }, "models": { - "nvidia/llama-3.3-nemotron-super-49b-v1.5": {"tool_call": true}, - "nvidia/nemotron-3-super-120b-a12b": {"tool_call": true}, - "deepseek-ai/deepseek-v4-pro": {"tool_call": true} + "contextual-orchestrator": {"tool_call": true} } } } @@ -227,11 +240,54 @@ jobs: CONFIG chmod 0400 "$RUNNER_TEMP/opencode.json" - - name: Run bounded NVIDIA NIM fallback + - name: Start contextual-orchestrator with all-provider discovery if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != '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 + umask 077 + token_file="$RUNNER_TEMP/contextual-orchestrator-token" + printf '%s' "$(openssl rand -hex 32)" >"$token_file" + report_file="$RUNNER_TEMP/contextual-orchestrator-discovery.json" + log_file="$RUNNER_TEMP/contextual-orchestrator.log" + gateway_token="$(cat "$token_file")" + export CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN="$gateway_token" + export PYTHONPATH="$RUNNER_TEMP/contextual-orchestrator" + python3 scripts/run_contextual_orchestrator.py \ + --host 127.0.0.1 --port "$CONTEXTUAL_ORCHESTRATOR_PORT" \ + --report "$report_file" >"$log_file" 2>&1 & + echo $! >"$RUNNER_TEMP/contextual-orchestrator.pid" + ready=false + for _ in $(seq 1 60); do + if curl --fail --silent --show-error "http://127.0.0.1:${CONTEXTUAL_ORCHESTRATOR_PORT}/healthz" >/dev/null; then + ready=true + break + fi + if ! kill -0 "$(cat "$RUNNER_TEMP/contextual-orchestrator.pid")" 2>/dev/null; then + cat "$log_file" + exit 1 + fi + sleep 1 + done + [ "$ready" = true ] + curl --fail --silent --show-error \ + -H "Authorization: Bearer $(cat "$token_file")" \ + "http://127.0.0.1:${CONTEXTUAL_ORCHESTRATOR_PORT}/v1/models" \ + >"$RUNNER_TEMP/contextual-orchestrator-models.json" + jq -e '.data | length > 0' "$RUNNER_TEMP/contextual-orchestrator-models.json" >/dev/null + jq -e '.discovered_count > 0 and (.selected_models | length > 0)' "$report_file" >/dev/null + echo "CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE=$token_file" >>"$GITHUB_ENV" + + - name: Run bounded contextual-orchestrator agent + if: steps.gate.outputs.dispatch == 'true' && env.DRY_RUN != 'true' + shell: bash + env: OPENCODE_CONFIG: ${{ runner.temp }}/opencode.json OPENCODE_DISABLE_AUTOUPDATE: 'true' XDG_CONFIG_HOME: ${{ runner.temp }}/opencode-config @@ -239,33 +295,19 @@ jobs: run: | set -euo pipefail prompt="$(cat "$RUNNER_TEMP/agent-prompt.md")" - status=1 - read -r -a models <<<"$OPENCODE_MODELS" - model_count=${#models[@]} - for ((model_index = 0; model_index < model_count; model_index++)); do - model="${models[$model_index]}" - if timeout --kill-after="${OPENCODE_KILL_GRACE_SECONDS}s" \ - "${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 \ - -u ACTIONS_RUNTIME_TOKEN -u ACTIONS_RUNTIME_URL \ - -u ACTIONS_RESULTS_URL -u ACTIONS_CACHE_URL \ - -u GITHUB_ENV -u GITHUB_OUTPUT -u GITHUB_PATH \ - -u GITHUB_STATE -u GITHUB_STEP_SUMMARY \ - opencode run "$prompt" --agent build --model "$model"; then - status=0 - break - fi - if [ "$model_index" -eq $((model_count - 1)) ]; then - break - fi - git reset --hard HEAD - git clean -fdx - env -u NVIDIA_NIM_API_KEY \ - timeout --kill-after=15s 180s \ - python3 -m pip install --disable-pip-version-check --no-deps --require-hashes -r requirements-quality.txt - done - [ "$status" -eq 0 ] || { echo '::error::All NIM candidates failed'; exit 1; } + gateway_token="$(cat "$CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE")" + export OPENCODE_GATEWAY_TOKEN="$gateway_token" + timeout --kill-after="${OPENCODE_KILL_GRACE_SECONDS}s" \ + "${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 \ + -u ACTIONS_RUNTIME_TOKEN -u ACTIONS_RUNTIME_URL \ + -u ACTIONS_RESULTS_URL -u ACTIONS_CACHE_URL \ + -u GITHUB_ENV -u GITHUB_OUTPUT -u GITHUB_PATH \ + -u GITHUB_STATE -u GITHUB_STEP_SUMMARY \ + -u BYTEZ_API_KEY -u NVIDIA_NIM_API_KEY -u NVIDIA_NIM_API_KEY_SUB \ + -u OPENROUTER_API_KEY -u OPENAI_API_KEY \ + opencode run "$prompt" --agent build --model "$OPENCODE_MODEL" - name: Verify, bound, and export the uncredentialed proposal id: proposal diff --git a/docs/adr/0010-adaptive-llm-orchestration.md b/docs/adr/0010-adaptive-llm-orchestration.md index d04bd6bce..1d07fbca5 100644 --- a/docs/adr/0010-adaptive-llm-orchestration.md +++ b/docs/adr/0010-adaptive-llm-orchestration.md @@ -34,7 +34,7 @@ Statistical estimation, temporal eligibility, event-relation validity, measureme ## Security and privacy -Documents cannot change orchestration policy, allowed tools, provider credentials, access lists, or role authority. Calls use evidence-minimized payloads and raw secrets are never exposed to models. Live development/model tests use `NVIDIA_NIM_API_KEY`; `COPILOT_GITHUB_TOKEN` is prohibited. Review-agent identities and credentials remain separate. +Documents cannot change orchestration policy, allowed tools, provider credentials, access lists, or role authority. Calls use evidence-minimized payloads and raw secrets are never exposed to models. Live scientific/model tests use `NVIDIA_NIM_API_KEY`; the separate hourly proposal gateway exception is governed by ADR 0017 and uses its own all-provider bootstrap. `COPILOT_GITHUB_TOKEN` is prohibited. Review-agent identities and credentials remain separate. ## Compatibility and migration diff --git a/docs/adr/0017-hourly-contextual-orchestrator-gateway.md b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md new file mode 100644 index 000000000..c6ff78c75 --- /dev/null +++ b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md @@ -0,0 +1,74 @@ +# ADR 0017 — Hourly contextual-orchestrator gateway for autonomous proposals + +**Decision status:** Accepted +**Implementation maturity:** active-PR — workflow wiring and bootstrap contract are implemented on this PR and require exact-head Checks and protected-main integration +**Date:** 2026-08-20 +**Supersedes:** None; this ADR clarifies the hourly proposal credential route without changing scientific or reviewer authority +**Related ADRs:** ADR 0010 (LLM test-time compute), ADR 0011 (modular MSA), ADR 0015 (autonomous development authority) + +## Context + +The hourly product-development workflow previously configured OpenCode directly +against one NVIDIA NIM endpoint. That did not exercise the repository's +contextual-orchestrator integration or its model-discovery contract, and it put a +provider key in the model-agent process. The scheduler must remain a proposal +producer: it may not merge, release, deploy, approve, or alter reviewer-agent +credentials. + +## Decision + +The hourly proposal runner starts a pinned, ephemeral +`ContextualWisdomLab/contextual-orchestrator` loopback gateway. At bootstrap it +registers `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`, +`OPENROUTER_API_KEY`, and `OPENAI_API_KEY` in the gateway KV, removes those +provider values from the gateway environment, and discovers models from all five +provider entries. The gateway enables the three lowest-cost discovered +candidates using contextual-orchestrator's existing price-book selector and +records only secret-free discovery evidence. + +OpenCode calls only `http://127.0.0.1:18000/v1` with a separately generated +loopback bearer token. It receives no provider credential. Liveness and +authenticated model-list checks must pass before the proposal agent runs. + +The workflow remains fail-closed when any provider credential is absent, the +queue is unreadable, an open PR exists, or the publication App is unavailable. +The verifier and publisher receive neither provider nor model credentials. + +## Alternatives considered + +1. **Direct NVIDIA NIM configuration** — rejected because it bypasses the + contextual-orchestrator discovery and routing boundary. +2. **One CLI registration process per provider** — rejected because the default + in-memory KV is process-local and would silently lose earlier registrations. +3. **Persistent shared credential database in the workflow** — rejected for this + ephemeral proposal path because it expands infrastructure and retention scope. +4. **One in-process gateway bootstrap with pinned source and loopback auth** — + accepted as the smallest auditable boundary. + +## Consequences + +- All configured provider discovery paths are exercised and auditable without + exposing provider keys to OpenCode. +- Discovery can fail for an individual provider while another provider supplies + a candidate; zero discovered candidates fail the proposal job. +- The three-candidate selection is cost-oriented, not a scientific quality + claim. TEPP deterministic evidence and human review remain authoritative. +- Provider terms, retention, region, and confidentiality must be reviewed for + every configured provider before the schedule is enabled. +- The workflow is not a Figma or visual interaction surface; no Figma file is + applicable to this headless scheduler decision. + +## Verification + +The PR contract tests assert the hourly schedule, all five credential names, +immutable gateway source digest, loopback-only OpenCode configuration, health and +model-list probes, provider-key removal from OpenCode, no Copilot token, and +absence of merge/release commands. The bootstrap import and credential transfer +self-checks verify that the provider keys enter the KV and leave the process +environment before serving. + +## Rollback + +Disable the schedule or revert this PR. Removing any required provider secret +causes a stable no-op. Rollback does not touch review-agent credentials, +protected-branch rules, or scientific runtime contracts. diff --git a/docs/adr/README.md b/docs/adr/README.md index 258eb7f31..61c7a75ef 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -22,6 +22,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | | [0015](0015-autonomous-development-review-and-merge-authority.md) | Autonomous development, review, and merge authority separation | Accepted | active-PR | Separates model proposal, deterministic verification, publication, independent review, and merge/release authority. | | [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | accepted-target | Separates observed evidence, detection/tracking, prediction/schema inference, temporal consistency, and promoted transition authority. | +| [0017](0017-hourly-contextual-orchestrator-gateway.md) | Hourly contextual-orchestrator gateway and all-provider model discovery | Accepted | active-PR | Keeps proposal-model execution behind a pinned loopback gateway while preserving independent verifier, publisher, reviewer, and merge authority. | ## Decision ownership summary @@ -43,6 +44,7 @@ Use the narrowest owning ADR when decisions overlap: - **claim maturity / release evidence:** ADR 0014; - **autonomous development/review/merge authority:** ADR 0015; - **TDT/CHRONOS event intelligence:** ADR 0016. +- **hourly proposal gateway and provider discovery:** ADR 0017. ## Change and supersession rule diff --git a/docs/doctoring/hourly-nim-opencode-development.md b/docs/doctoring/hourly-nim-opencode-development.md index e5fc6ca18..5d30f8240 100644 --- a/docs/doctoring/hourly-nim-opencode-development.md +++ b/docs/doctoring/hourly-nim-opencode-development.md @@ -1,4 +1,4 @@ -# Hourly NVIDIA NIM OpenCode Development — Evidence Doctoring +# Hourly Contextual Orchestrator OpenCode Development — Evidence Doctoring ## Claim boundary @@ -15,8 +15,9 @@ and exact-head branch rules remain independent controls. | GitHub jobs | Jobs run on separate runner instances and receive scoped tokens | Model, verifier, and publisher are separate jobs | | GitHub App authentication | A workflow can mint an installation token with explicit repository permissions | Publication token is minted only after non-executing validation | | Artifact handoff | Artifact upload/download actions expose immutable IDs and digests | Patch ID, digest, base, size, count, and modes are checked twice | -| OpenCode | OpenCode is a programmable coding agent with provider configuration | One checksum-pinned binary uses NVIDIA NIM only | -| NVIDIA NIM | NIM exposes OpenAI-compatible hosted inference APIs | Only `NVIDIA_NIM_API_KEY` reaches the proposal process | +| OpenCode | OpenCode is a programmable coding agent with provider configuration | One checksum-pinned binary calls only the loopback gateway | +| contextual-orchestrator | The pinned gateway discovers configured provider models and routes through its KV credential seam | All five provider keys are registered at bootstrap; OpenCode receives only a loopback token | +| Provider APIs | OpenAI-compatible and provider-specific model-list APIs expose discoverable models | Discovery is attempted for OpenAI, OpenRouter, both NVIDIA NIM keys, and Bytez before selection | | SSDF | NIST SP 800-218 recommends protected build environments, review, provenance, and vulnerability response | Fresh verification and ordinary PR governance remain mandatory | | AI risk | ISO/IEC 23894:2023 and 42001:2023 require contextual risk treatment and controlled change | Stable no-op reasons, bounded proposals, traceable decisions, and rollback are documented | | Test-time compute | Fugu, Conductor, and TRINITY distinguish routing from deeper role-based workflows | Runtime LLM increments must preserve route/conduct and access-list controls | @@ -67,6 +68,10 @@ GitHub. (n.d.). *Making authenticated API requests with a GitHub App in a GitHub Actions workflow*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow +ContextualWisdomLab. (2026). *contextual-orchestrator* (commit +e226e1197bdfc890c9d8e5b9b648c78857d7e465) [Computer software]. GitHub. +https://github.com/ContextualWisdomLab/contextual-orchestrator + International Organization for Standardization, & International Electrotechnical Commission. (2023a). *Information technology—Artificial intelligence—Guidance on risk management* (ISO/IEC Standard No. 23894:2023). @@ -109,8 +114,9 @@ deterministic workflow tests and must be revisited in a later literature update. ## Residual risk -The NIM key exists inside the model process, proposed code is executed by an -uncredentialed verifier with ordinary hosted-runner egress, and artifact digests -do not establish semantic safety. A later broker can narrow NIM egress; a later -central reusable workflow can remove duplicated repository policy. Neither -future improvement may combine model execution with publication authority. +Provider keys exist inside the ephemeral gateway process during bootstrap, +proposed code is executed by an uncredentialed verifier with ordinary +hosted-runner egress, and artifact digests do not establish semantic safety. A +later broker can narrow provider egress; a later central reusable workflow can +remove duplicated repository policy. Neither future improvement may combine +model execution with publication authority. diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 652acfe21..086f23af9 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -1,6 +1,6 @@ -# Hourly NVIDIA NIM Product Development +# Hourly Contextual Orchestrator Product Development -The hourly NVIDIA NIM product-development workflow proposes one bounded +The hourly contextual-orchestrator product-development workflow proposes one bounded commercial-quality increment when the repository has no open pull request. It is separate from the deterministic minute-17 quality sentinel and never merges, releases, deploys, approves, or changes reviewer credentials. @@ -13,9 +13,9 @@ avoids the busiest scheduler boundary. A repository-scoped concurrency group does not cancel an active run. Before checkout or model execution, the proposal job reads at most one open pull -request. Unreadable inventory, any open PR, a missing `NVIDIA_NIM_API_KEY`, or a +request. Unreadable inventory, any open PR, any missing provider key, or a missing Maintainer App configuration produces a stable fail-closed no-op. A dry -run may print the task contract without either credential. +run may print the task contract without credentials. When a PR exists, normal review → repair → exact-head Checks → merge governance owns the hour. The scheduler does not create a competing branch. @@ -24,7 +24,8 @@ owns the hour. The scheduler does not create a competing branch. Configure these repository or organization values: -- Secret `NVIDIA_NIM_API_KEY` for the proposal runner only. +- Secrets `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`, + `OPENROUTER_API_KEY`, and `OPENAI_API_KEY` for gateway bootstrap only. - Variable `TEPP_MAINTAINER_APP_CLIENT_ID`. - Secret `TEPP_MAINTAINER_APP_PRIVATE_KEY`. - A repository-scoped GitHub App installation with metadata read, contents @@ -44,10 +45,20 @@ developer disabled rather than falling back to `GITHUB_TOKEN`. The proposal runner has read-only repository and pull-request permissions. OpenCode is downloaded from an immutable versioned URL and checked against a -committed SHA-256. Its only model provider is NVIDIA NIM. - -The OpenCode process receives `NVIDIA_NIM_API_KEY` but has GitHub, OIDC, Actions -runtime/cache, and runner command-file variables removed. Network tools, GitHub +committed SHA-256. It calls only the loopback contextual-orchestrator gateway. + +The gateway source is downloaded from the pinned +`ContextualWisdomLab/contextual-orchestrator` commit and checked against a +committed SHA-256. At bootstrap, `scripts/run_contextual_orchestrator.py` +registers all five provider keys in the orchestrator KV, removes them from its +environment, discovers every provider model, records secret-free discovery +evidence, and enables the three lowest-cost discovered candidates. OpenCode +receives only the gateway bearer token; it never receives a provider key. The +gateway's `/healthz` and authenticated `/v1/models` responses are checked before +the agent starts. + +The OpenCode process has provider keys, GitHub, OIDC, Actions runtime/cache, and +runner command-file variables removed. Network tools, GitHub CLI, remote Git operations, commits, pushes, tags, external-directory access, task delegation, interactive questions, and OpenCode web tools are denied. @@ -104,7 +115,7 @@ Stable no-op reasons are: - `pull_request_inventory_unavailable` - `open_pull_request` -- `nim_api_key_unavailable` +- `contextual_orchestrator_credentials_unavailable` - `maintainer_app_unavailable` A failed model candidate is discarded before a later candidate runs. A cleanup @@ -120,9 +131,9 @@ write token. ## Disablement and rollback Disable scheduled development by disabling the workflow, removing its schedule, -or removing either dedicated credential. Removing only the model secret stops -model execution; removing the Maintainer App values stops publication. The -minute-17 deterministic quality sentinel continues independently. +or removing any one of the five provider credentials. Removing any provider key +stops model execution; removing the Maintainer App values stops publication. +The minute-17 deterministic quality sentinel continues independently. Rollback a faulty workflow through a reviewed revert PR. Do not edit branch protection, review workflows, or release workflows as an incident shortcut. @@ -131,13 +142,15 @@ references them. ## Residual risks -- The model process necessarily receives the NIM credential. A future narrow - inference broker could keep the upstream secret outside the model process. -- NVIDIA NIM may process repository source; operators must review confidentiality, - retention, regional, and contractual obligations. +- The ephemeral gateway process receives provider credentials during bootstrap, + then removes them from its environment before serving. A future narrow + inference broker could keep upstream secrets outside the runner. +- Each configured provider may process repository source; operators must review + confidentiality, retention, regional, and contractual obligations for every + provider before enabling the schedule. - The verifier executes untrusted code on an ephemeral hosted runner with - outbound network access, but receives no publication, NIM, OIDC, artifact/cache - runtime, command-file, or reviewer credential. + outbound network access, but receives no publication, provider, OIDC, + artifact/cache runtime, command-file, or reviewer credential. - GitHub artifact storage, hosted runners, and pinned actions remain trusted infrastructure. Digests prove identity, not semantic correctness. - GitHub cannot atomically create a PR only when none exists. Final queue and diff --git a/scripts/run_contextual_orchestrator.py b/scripts/run_contextual_orchestrator.py new file mode 100644 index 000000000..1b18f797a --- /dev/null +++ b/scripts/run_contextual_orchestrator.py @@ -0,0 +1,106 @@ +"""Bootstrap an ephemeral contextual-orchestrator gateway for CI agents. + +Provider credentials arrive only at bootstrap, are registered in the +orchestrator KV, and are removed from this process environment before the +gateway begins serving requests. Model discovery therefore exercises every +configured provider while OpenCode receives only the loopback bearer token. +""" + +from __future__ import annotations + +import argparse +from dataclasses import replace +import json +import os +from pathlib import Path + +from contextual_orchestrator import InMemoryConfigStore, ModelAgent, PriceBook, TaskOrchestrator, register_credential +from contextual_orchestrator.model_discovery import ( + agent_from_discovered, + discover_all_models, + refresh_price_book, + select_top_n_cheapest_discovered_agents, +) +from contextual_orchestrator.server import SecurityConfig, serve + +PROVIDER_CREDENTIAL_NAMES = ( + "BYTEZ_API_KEY", + "NVIDIA_NIM_API_KEY", + "NVIDIA_NIM_API_KEY_SUB", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", +) + + +def _register_bootstrap_credentials() -> None: + """Move all provider keys from bootstrap environment into the KV registry.""" + + missing: list[str] = [] + for name in PROVIDER_CREDENTIAL_NAMES: + value = os.environ.pop(name, "") + if value: + register_credential(name, value) + else: + missing.append(name) + if missing: + raise RuntimeError(f"missing provider credentials: {', '.join(missing)}") + + +def _selected_agents() -> tuple[list[ModelAgent], dict[str, object]]: + """Discover all providers and enable the three lowest-cost candidates.""" + + discovered, errors = discover_all_models() + if not discovered: + providers = ", ".join(sorted(error.provider_name for error in errors)) or "none" + raise RuntimeError(f"model discovery produced no candidates; providers_with_errors={providers}") + price_book = PriceBook(InMemoryConfigStore()) + priced_count = refresh_price_book(discovered, price_book) + selected = select_top_n_cheapest_discovered_agents(discovered, price_book, 3) + agents = [ + replace(agent_from_discovered(model, priority=3 - index), disabled=False) + for index, model in enumerate(selected) + ] + report = { + "discovered_count": len(discovered), + "priced_count": priced_count, + "providers_discovered": sorted({model.provider_name for model in discovered}), + "providers_with_errors": sorted({error.provider_name for error in errors}), + "selected_models": [agent.model for agent in agents], + } + return agents, report + + +def _write_report(path: Path, report: dict[str, object]) -> None: + """Write secret-free discovery evidence with restrictive permissions.""" + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(report, ensure_ascii=True, sort_keys=True) + "\n", encoding="utf-8") + path.chmod(0o600) + + +def main() -> None: + """Start the authenticated gateway after provider discovery succeeds.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=18000) + parser.add_argument("--report", type=Path, required=True) + args = parser.parse_args() + + _register_bootstrap_credentials() + gateway_token = os.environ.pop("CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN", "") + if not gateway_token: + raise RuntimeError("CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN is required") + agents, report = _selected_agents() + _write_report(args.report, report) + orchestrator = TaskOrchestrator(agents) + serve( + orchestrator, + host=args.host, + port=args.port, + security=SecurityConfig(auth_token=gateway_token), + ) + + +if __name__ == "__main__": + main() diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index 7c56183ca..100a7c7f1 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -1,4 +1,4 @@ -"""Lock the hourly NVIDIA NIM OpenCode development security contract.""" +"""Lock the hourly contextual-orchestrator OpenCode security contract.""" from __future__ import annotations @@ -7,6 +7,7 @@ from types import ModuleType WORKFLOW = Path(".github/workflows/hourly-nim-product-development.yml") +BOOTSTRAP = Path("scripts/run_contextual_orchestrator.py") PARSER = Path("scripts/prepare_agent_pr_message.py") RUNBOOK = Path("docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md") DOCTORING = Path("docs/doctoring/hourly-nim-opencode-development.md") @@ -32,31 +33,43 @@ class HourlyNimProductDevelopmentContractTests(unittest.TestCase): """Structural tests for the credential-separated product-development loop.""" def test_hourly_workflow_schedule_credentials_and_queue_gate(self) -> None: - """Run at minute 47 with NIM only and fail closed around PR inventory.""" + """Run at minute 47 with provider discovery and fail closed around inventory.""" text = _text(WORKFLOW) + bootstrap = _text(BOOTSTRAP) for token in ( 'cron: "47 * * * *"', "workflow_dispatch:", "dry_run:", "hourly-nim-product-development-${{ github.repository }}", "cancel-in-progress: false", + "secrets.BYTEZ_API_KEY", "secrets.NVIDIA_NIM_API_KEY", - "{env:NVIDIA_NIM_API_KEY}", + "secrets.NVIDIA_NIM_API_KEY_SUB", + "secrets.OPENROUTER_API_KEY", + "secrets.OPENAI_API_KEY", + "CONTEXTUAL_ORCHESTRATOR_COMMIT", + "CONTEXTUAL_ORCHESTRATOR_SHA256", + "run_contextual_orchestrator.py", + "/healthz", + "/v1/models", + "{env:OPENCODE_GATEWAY_TOKEN}", "OPENCODE_VERSION", "OPENCODE_SHA256", "sha256sum -c", "pull_request_inventory_unavailable", "open_pull_request", - "nim_api_key_unavailable", + "contextual_orchestrator_credentials_unavailable", "maintainer_app_unavailable", "base_branch_advanced", "open_pull_request_after_generation", "ContextualWisdomLab/TEPP", ): self.assertIn(token, text) + for token in ("discover_all_models", "register_credential", "PROVIDER_CREDENTIAL_NAMES"): + self.assertIn(token, bootstrap) self.assertNotIn("COPILOT_GITHUB_TOKEN", text) - self.assertNotIn("CONTEXTUAL_ORCHESTRATOR_TOKEN", text) + self.assertNotIn("CONTEXTUAL_ORCHESTRATOR_TOKEN=", text) self.assertEqual(text.count("gh pr create"), 1) self.assertNotIn("gh pr merge", text) self.assertNotIn("gh release create", text) @@ -73,7 +86,8 @@ def test_hourly_workflow_separates_three_runner_trust_boundaries(self) -> None: )[0] publisher = text.split("publish_product_increment:", 1)[1] - self.assertIn("NVIDIA_NIM_API_KEY", proposer) + self.assertIn("BYTEZ_API_KEY", proposer) + self.assertIn("OPENAI_API_KEY", proposer) self.assertNotIn("create-github-app-token", proposer) self.assertNotIn("gh pr create", proposer) self.assertNotIn("NVIDIA_NIM_API_KEY", verifier) @@ -199,7 +213,7 @@ def test_parser_accepts_unicode_and_owner_only_outputs(self) -> None: title_path = tmp_path / "title.txt" body_path = tmp_path / "body.md" source.write_text( - "feat: 시간별 NIM 제품 개발 루프 추가\r\n\r\n" + "feat: 시간별 오케스트레이터 제품 개발 루프 추가\r\n\r\n" "구매자가 체감하는 제품 Gap 하나를 안전하게 닫습니다.\r\n", encoding="utf-8", ) @@ -212,7 +226,7 @@ def test_parser_accepts_unicode_and_owner_only_outputs(self) -> None: ) self.assertEqual( title_path.read_text(encoding="utf-8"), - "feat: 시간별 NIM 제품 개발 루프 추가", + "feat: 시간별 오케스트레이터 제품 개발 루프 추가", ) self.assertIn("구매자가 체감하는 제품 Gap", body_path.read_text(encoding="utf-8")) for path in (title_path, body_path): @@ -223,7 +237,7 @@ def test_supporting_runbook_and_doctoring_exist(self) -> None: runbook = _text(RUNBOOK) doctoring = _text(DOCTORING) - for token in ("NVIDIA_NIM_API_KEY", "proposal", "verification", "publication"): + for token in ("OPENAI_API_KEY", "contextual-orchestrator", "proposal", "verification", "publication"): self.assertIn(token, runbook) self.assertIn("APA", doctoring) self.assertIn("Do not configure `COPILOT_GITHUB_TOKEN`", runbook) From a7ce5ada1fa882d24ec38eccd5af6a3717fe0879 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:07:28 +0900 Subject: [PATCH 02/12] fix: allow provider discovery startup window --- .github/workflows/hourly-nim-product-development.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-nim-product-development.yml b/.github/workflows/hourly-nim-product-development.yml index 2408b1b9b..a118a77c8 100644 --- a/.github/workflows/hourly-nim-product-development.yml +++ b/.github/workflows/hourly-nim-product-development.yml @@ -264,7 +264,7 @@ jobs: --report "$report_file" >"$log_file" 2>&1 & echo $! >"$RUNNER_TEMP/contextual-orchestrator.pid" ready=false - for _ in $(seq 1 60); do + for _ in $(seq 1 120); do if curl --fail --silent --show-error "http://127.0.0.1:${CONTEXTUAL_ORCHESTRATOR_PORT}/healthz" >/dev/null; then ready=true break From 37f85fed18d78e992e5ec2de3ffcc44eb98bbca4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:13:49 +0900 Subject: [PATCH 03/12] test: verify contextual credential bootstrap --- scripts/run_contextual_orchestrator.py | 54 ++++--- .../test_hourly_nim_product_development.py | 147 ++++++++++++++++++ 2 files changed, 182 insertions(+), 19 deletions(-) diff --git a/scripts/run_contextual_orchestrator.py b/scripts/run_contextual_orchestrator.py index 1b18f797a..0244f0f6a 100644 --- a/scripts/run_contextual_orchestrator.py +++ b/scripts/run_contextual_orchestrator.py @@ -13,15 +13,7 @@ import json import os from pathlib import Path - -from contextual_orchestrator import InMemoryConfigStore, ModelAgent, PriceBook, TaskOrchestrator, register_credential -from contextual_orchestrator.model_discovery import ( - agent_from_discovered, - discover_all_models, - refresh_price_book, - select_top_n_cheapest_discovered_agents, -) -from contextual_orchestrator.server import SecurityConfig, serve +from typing import Any PROVIDER_CREDENTIAL_NAMES = ( "BYTEZ_API_KEY", @@ -32,6 +24,14 @@ ) +def _register_credential(name: str, value: str) -> None: + """Register one bootstrap secret through contextual-orchestrator's KV seam.""" + + from contextual_orchestrator import register_credential + + register_credential(name, value) + + def _register_bootstrap_credentials() -> None: """Move all provider keys from bootstrap environment into the KV registry.""" @@ -39,16 +39,24 @@ def _register_bootstrap_credentials() -> None: for name in PROVIDER_CREDENTIAL_NAMES: value = os.environ.pop(name, "") if value: - register_credential(name, value) + _register_credential(name, value) else: missing.append(name) if missing: raise RuntimeError(f"missing provider credentials: {', '.join(missing)}") -def _selected_agents() -> tuple[list[ModelAgent], dict[str, object]]: +def _selected_agents() -> tuple[list[Any], dict[str, object]]: """Discover all providers and enable the three lowest-cost candidates.""" + from contextual_orchestrator import InMemoryConfigStore, PriceBook + from contextual_orchestrator.model_discovery import ( + agent_from_discovered, + discover_all_models, + refresh_price_book, + select_top_n_cheapest_discovered_agents, + ) + discovered, errors = discover_all_models() if not discovered: providers = ", ".join(sorted(error.provider_name for error in errors)) or "none" @@ -78,6 +86,20 @@ def _write_report(path: Path, report: dict[str, object]) -> None: path.chmod(0o600) +def _start_gateway(agents: list[Any], gateway_token: str, host: str, port: int) -> None: + """Construct and serve the contextual-orchestrator HTTP gateway.""" + + from contextual_orchestrator import TaskOrchestrator + from contextual_orchestrator.server import SecurityConfig, serve + + serve( + TaskOrchestrator(agents), + host=host, + port=port, + security=SecurityConfig(auth_token=gateway_token), + ) + + def main() -> None: """Start the authenticated gateway after provider discovery succeeds.""" @@ -93,14 +115,8 @@ def main() -> None: raise RuntimeError("CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN is required") agents, report = _selected_agents() _write_report(args.report, report) - orchestrator = TaskOrchestrator(agents) - serve( - orchestrator, - host=args.host, - port=args.port, - security=SecurityConfig(auth_token=gateway_token), - ) + _start_gateway(agents, gateway_token, args.host, args.port) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover - exercised by the workflow process main() diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index 100a7c7f1..a29e7aa2c 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -2,9 +2,13 @@ from __future__ import annotations +from dataclasses import dataclass +import os +import sys import unittest from pathlib import Path from types import ModuleType +from unittest.mock import patch WORKFLOW = Path(".github/workflows/hourly-nim-product-development.yml") BOOTSTRAP = Path("scripts/run_contextual_orchestrator.py") @@ -242,6 +246,149 @@ def test_supporting_runbook_and_doctoring_exist(self) -> None: self.assertIn("APA", doctoring) self.assertIn("Do not configure `COPILOT_GITHUB_TOKEN`", runbook) + def test_bootstrap_registers_each_provider_key_and_removes_environment_values(self) -> None: + """Exercise the real bootstrap loop with a key-counting KV double.""" + + import scripts.run_contextual_orchestrator as bootstrap + + calls: list[tuple[str, str]] = [] + fake_package = ModuleType("contextual_orchestrator") + fake_package.register_credential = lambda name, value: calls.append((name, value)) + values = {name: f"test-value-{name}" for name in bootstrap.PROVIDER_CREDENTIAL_NAMES} + with patch.dict(sys.modules, {"contextual_orchestrator": fake_package}), patch.dict( + os.environ, values, clear=False + ): + bootstrap._register_bootstrap_credentials() + self.assertEqual( + calls, + [(name, values[name]) for name in bootstrap.PROVIDER_CREDENTIAL_NAMES], + ) + for name in bootstrap.PROVIDER_CREDENTIAL_NAMES: + self.assertNotIn(name, os.environ) + + def test_bootstrap_fails_closed_when_one_provider_key_is_missing(self) -> None: + """Reject incomplete provider bootstrap without silently selecting a subset.""" + + import scripts.run_contextual_orchestrator as bootstrap + + fake_package = ModuleType("contextual_orchestrator") + fake_package.register_credential = lambda _name, _value: None + values = { + name: ("present" if index == 0 else "") + for index, name in enumerate(bootstrap.PROVIDER_CREDENTIAL_NAMES) + } + with patch.dict(sys.modules, {"contextual_orchestrator": fake_package}), patch.dict( + os.environ, values, clear=False + ): + with self.assertRaisesRegex(RuntimeError, "missing provider credentials"): + bootstrap._register_bootstrap_credentials() + + def test_discovery_selection_and_empty_provider_fail_closed_paths(self) -> None: + """Select discovered candidates and reject a discovery result with no models.""" + + import scripts.run_contextual_orchestrator as bootstrap + + @dataclass(frozen=True) + class FakeAgent: + """Small dataclass matching the fields changed by dataclasses.replace.""" + + model: str + priority: int = 0 + disabled: bool = True + + class FakePriceBook: + """Minimal price-book constructor accepted by the selection seam.""" + + def __init__(self, _store: object) -> None: + pass + + sample = type("SampleModel", (), {"model_id": "model_one", "provider_name": "provider_one"})() + error = type("SampleError", (), {"provider_name": "provider_two"})() + fake_package = ModuleType("contextual_orchestrator") + fake_package.InMemoryConfigStore = lambda: object() + fake_package.PriceBook = FakePriceBook + fake_discovery = ModuleType("contextual_orchestrator.model_discovery") + fake_discovery.agent_from_discovered = lambda model, priority=0: FakeAgent( + model.model_id, priority=priority + ) + fake_discovery.discover_all_models = lambda: ([sample], [error]) + fake_discovery.refresh_price_book = lambda _models, _book: 1 + fake_discovery.select_top_n_cheapest_discovered_agents = lambda models, _book, _limit: models + fake_modules = { + "contextual_orchestrator": fake_package, + "contextual_orchestrator.model_discovery": fake_discovery, + } + with patch.dict(sys.modules, fake_modules): + agents, report = bootstrap._selected_agents() + self.assertEqual([agent.model for agent in agents], ["model_one"]) + self.assertFalse(agents[0].disabled) + self.assertEqual(report["providers_with_errors"], ["provider_two"]) + + fake_discovery.discover_all_models = lambda: ([], []) + with self.assertRaisesRegex(RuntimeError, "providers_with_errors=none"): + bootstrap._selected_agents() + + def test_report_gateway_and_main_contract_are_executable_with_seams(self) -> None: + """Cover report permissions, gateway construction, and CLI orchestration.""" + + import tempfile + + import scripts.run_contextual_orchestrator as bootstrap + + class FakeOrchestrator: + """Capture the selected agent pool passed to the gateway runtime.""" + + def __init__(self, agents: list[object]) -> None: + self.agents = agents + + class FakeSecurity: + """Capture the loopback bearer token passed to the HTTP server.""" + + def __init__(self, auth_token: str) -> None: + self.auth_token = auth_token + + server_calls: list[tuple[object, str, int, object]] = [] + fake_package = ModuleType("contextual_orchestrator") + fake_package.TaskOrchestrator = FakeOrchestrator + fake_server = ModuleType("contextual_orchestrator.server") + fake_server.SecurityConfig = FakeSecurity + fake_server.serve = lambda orchestrator, *, host, port, security: server_calls.append( + (orchestrator, host, port, security) + ) + with patch.dict( + sys.modules, + {"contextual_orchestrator": fake_package, "contextual_orchestrator.server": fake_server}, + ): + with tempfile.TemporaryDirectory() as tmp: + report_path = Path(tmp) / "nested" / "discovery.json" + bootstrap._write_report(report_path, {"discovered_count": 1}) + self.assertEqual(report_path.read_text(encoding="utf-8"), '{"discovered_count": 1}\n') + self.assertEqual(report_path.stat().st_mode & 0o777, 0o600) + + bootstrap._start_gateway(["agent"], "gateway-token", "127.0.0.1", 18000) + self.assertEqual(server_calls[0][0].agents, ["agent"]) + self.assertEqual(server_calls[0][1], "127.0.0.1") + self.assertEqual(server_calls[0][2], 18000) + self.assertEqual(server_calls[0][3].auth_token, "gateway-token") + + with patch.object(bootstrap, "_register_bootstrap_credentials"), patch.object( + bootstrap, "_selected_agents", return_value=([], {"discovered_count": 0}) + ) as selected, patch.object(bootstrap, "_write_report") as written, patch.object( + bootstrap, "_start_gateway" + ) as started, patch.dict( + os.environ, {"CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN": "gateway-token"}, clear=False + ), patch.object(sys, "argv", ["run_contextual_orchestrator.py", "--report", "report.json"]): + bootstrap.main() + selected.assert_called_once_with() + written.assert_called_once_with(Path("report.json"), {"discovered_count": 0}) + started.assert_called_once_with([], "gateway-token", "127.0.0.1", 18000) + + with patch.object(bootstrap, "_register_bootstrap_credentials"), patch.dict( + os.environ, {}, clear=False + ), patch.object(sys, "argv", ["run_contextual_orchestrator.py", "--report", "report.json"]): + with self.assertRaisesRegex(RuntimeError, "INFERENCE_TOKEN is required"): + bootstrap.main() + if __name__ == "__main__": unittest.main() From 02ace789541acd4e25e888876a16ff4aa395c0d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:16:26 +0900 Subject: [PATCH 04/12] test: remove redundant discovery mock lambda --- tests/quality/test_hourly_nim_product_development.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index a29e7aa2c..0b18ee787 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -305,7 +305,7 @@ def __init__(self, _store: object) -> None: sample = type("SampleModel", (), {"model_id": "model_one", "provider_name": "provider_one"})() error = type("SampleError", (), {"provider_name": "provider_two"})() fake_package = ModuleType("contextual_orchestrator") - fake_package.InMemoryConfigStore = lambda: object() + fake_package.InMemoryConfigStore = object fake_package.PriceBook = FakePriceBook fake_discovery = ModuleType("contextual_orchestrator.model_discovery") fake_discovery.agent_from_discovered = lambda model, priority=0: FakeAgent( From 619bdbcd1ac4be904620d18737bed040ec9b5210 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 04:51:09 +0900 Subject: [PATCH 05/12] fix(hourly): exclude non-chat discovery models --- CHANGELOG.md | 1 + ...-hourly-contextual-orchestrator-gateway.md | 9 ++- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 3 +- scripts/run_contextual_orchestrator.py | 61 ++++++++++++++++++- .../test_hourly_nim_product_development.py | 16 ++++- 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93891a271..dfcf26bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- Hourly contextual-orchestrator discovery records all provider models but routes OpenCode only through general-chat candidates, excluding embedding, image, reranker, transcription, moderation, safety, and other endpoint-only identifiers before price selection. - `tepp_api` adaptive orchestration router (ADR 0010): versioned `direct`/`verify`/`committee`/`conductor`/`abstain` selection from CPU `f64` risk, ambiguity, evidence, and token-budget inputs; recorded stages, recursion, decomposition, access lists, and role-specific reasoning effort; fail-closed document-controlled policy/access/credentials; LLM plans remain proposals under deterministic statistical authority; comparable-budget ablation requires a direct baseline; credential-free contextual-orchestrator binding. Live NIM HTTP remains accepted-target. - `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure. - `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013). diff --git a/docs/adr/0017-hourly-contextual-orchestrator-gateway.md b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md index c6ff78c75..ae2be9c5a 100644 --- a/docs/adr/0017-hourly-contextual-orchestrator-gateway.md +++ b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md @@ -22,9 +22,12 @@ The hourly proposal runner starts a pinned, ephemeral registers `BYTEZ_API_KEY`, `NVIDIA_NIM_API_KEY`, `NVIDIA_NIM_API_KEY_SUB`, `OPENROUTER_API_KEY`, and `OPENAI_API_KEY` in the gateway KV, removes those provider values from the gateway environment, and discovers models from all five -provider entries. The gateway enables the three lowest-cost discovered -candidates using contextual-orchestrator's existing price-book selector and -records only secret-free discovery evidence. +provider entries. The gateway records every discovered provider model, excludes +endpoint-only and safety-only model identifiers from the chat pool, and enables +the three lowest-cost general-chat candidates using contextual-orchestrator's +existing price-book selector. This prevents an embedding or image model from +being sent to an ordinary chat endpoint while preserving full discovery +evidence. OpenCode calls only `http://127.0.0.1:18000/v1` with a separately generated loopback bearer token. It receives no provider credential. Liveness and diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 086f23af9..1b45ad233 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -52,7 +52,8 @@ The gateway source is downloaded from the pinned committed SHA-256. At bootstrap, `scripts/run_contextual_orchestrator.py` registers all five provider keys in the orchestrator KV, removes them from its environment, discovers every provider model, records secret-free discovery -evidence, and enables the three lowest-cost discovered candidates. OpenCode +evidence, excludes endpoint-only and safety-only model identifiers from chat +routing, and enables the three lowest-cost general-chat candidates. OpenCode receives only the gateway bearer token; it never receives a provider key. The gateway's `/healthz` and authenticated `/v1/models` responses are checked before the agent starts. diff --git a/scripts/run_contextual_orchestrator.py b/scripts/run_contextual_orchestrator.py index 0244f0f6a..cf426514e 100644 --- a/scripts/run_contextual_orchestrator.py +++ b/scripts/run_contextual_orchestrator.py @@ -13,6 +13,7 @@ import json import os from pathlib import Path +import re from typing import Any PROVIDER_CREDENTIAL_NAMES = ( @@ -23,6 +24,53 @@ "OPENAI_API_KEY", ) +_MODEL_TOKEN_RE = re.compile(r"[a-z0-9]+") +_NON_CHAT_MODEL_TOKENS = frozenset( + { + "bge", + "clip", + "dall", + "e5", + "embed", + "embedding", + "embeddings", + "image", + "images", + "moderation", + "realtime", + "rerank", + "reranker", + "siglip", + "sora", + "speech", + "transcribe", + "transcription", + "tts", + "whisper", + } +) +_NON_CHAT_MODEL_PREFIXES = ("embed", "moderat", "rerank", "transcrib") + + +def _is_general_chat_model(model_id: object) -> bool: + """Keep endpoint-only and safety-only catalog rows out of chat routing.""" + if not isinstance(model_id, str): + return False + tokens = tuple(_MODEL_TOKEN_RE.findall(model_id.casefold())) + if not tokens or any( + token in _NON_CHAT_MODEL_TOKENS + or token.startswith(_NON_CHAT_MODEL_PREFIXES) + for token in tokens + ): + return False + return not any( + token == "safety" + or token == "guard" + or token == "shieldgemma" + or token.startswith("nemoguard") + for token in tokens + ) + def _register_credential(name: str, value: str) -> None: """Register one bootstrap secret through contextual-orchestrator's KV seam.""" @@ -62,14 +110,23 @@ def _selected_agents() -> tuple[list[Any], dict[str, object]]: providers = ", ".join(sorted(error.provider_name for error in errors)) or "none" raise RuntimeError(f"model discovery produced no candidates; providers_with_errors={providers}") price_book = PriceBook(InMemoryConfigStore()) - priced_count = refresh_price_book(discovered, price_book) - selected = select_top_n_cheapest_discovered_agents(discovered, price_book, 3) + chat_discovered = [ + model for model in discovered if _is_general_chat_model(model.model_id) + ] + if not chat_discovered: + raise RuntimeError("model discovery produced no general chat candidates") + priced_count = refresh_price_book(chat_discovered, price_book) + selected = select_top_n_cheapest_discovered_agents(chat_discovered, price_book, 3) + if not selected: + raise RuntimeError("model discovery selected no general chat candidates") agents = [ replace(agent_from_discovered(model, priority=3 - index), disabled=False) for index, model in enumerate(selected) ] report = { "discovered_count": len(discovered), + "chat_candidate_count": len(chat_discovered), + "excluded_non_chat_count": len(discovered) - len(chat_discovered), "priced_count": priced_count, "providers_discovered": sorted({model.provider_name for model in discovered}), "providers_with_errors": sorted({error.provider_name for error in errors}), diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index 0b18ee787..31cc6b9fd 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -303,6 +303,11 @@ def __init__(self, _store: object) -> None: pass sample = type("SampleModel", (), {"model_id": "model_one", "provider_name": "provider_one"})() + embedding = type( + "EmbeddingModel", + (), + {"model_id": "text-embedding-3-small", "provider_name": "provider_one"}, + )() error = type("SampleError", (), {"provider_name": "provider_two"})() fake_package = ModuleType("contextual_orchestrator") fake_package.InMemoryConfigStore = object @@ -311,8 +316,8 @@ def __init__(self, _store: object) -> None: fake_discovery.agent_from_discovered = lambda model, priority=0: FakeAgent( model.model_id, priority=priority ) - fake_discovery.discover_all_models = lambda: ([sample], [error]) - fake_discovery.refresh_price_book = lambda _models, _book: 1 + fake_discovery.discover_all_models = lambda: ([embedding, sample], [error]) + fake_discovery.refresh_price_book = lambda models, _book: len(models) fake_discovery.select_top_n_cheapest_discovered_agents = lambda models, _book, _limit: models fake_modules = { "contextual_orchestrator": fake_package, @@ -322,12 +327,19 @@ def __init__(self, _store: object) -> None: agents, report = bootstrap._selected_agents() self.assertEqual([agent.model for agent in agents], ["model_one"]) self.assertFalse(agents[0].disabled) + self.assertEqual(report["discovered_count"], 2) + self.assertEqual(report["chat_candidate_count"], 1) + self.assertEqual(report["excluded_non_chat_count"], 1) self.assertEqual(report["providers_with_errors"], ["provider_two"]) fake_discovery.discover_all_models = lambda: ([], []) with self.assertRaisesRegex(RuntimeError, "providers_with_errors=none"): bootstrap._selected_agents() + fake_discovery.discover_all_models = lambda: ([embedding], []) + with self.assertRaisesRegex(RuntimeError, "no general chat candidates"): + bootstrap._selected_agents() + def test_report_gateway_and_main_contract_are_executable_with_seams(self) -> None: """Cover report permissions, gateway construction, and CLI orchestration.""" From 5a98c8a5c3f8c9d57ecdde99aab139780335e3f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 04:53:05 +0900 Subject: [PATCH 06/12] docs(hourly): record chat eligibility boundary --- docs/doctoring/hourly-nim-opencode-development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/hourly-nim-opencode-development.md b/docs/doctoring/hourly-nim-opencode-development.md index 5d30f8240..2422cf4ff 100644 --- a/docs/doctoring/hourly-nim-opencode-development.md +++ b/docs/doctoring/hourly-nim-opencode-development.md @@ -16,7 +16,7 @@ and exact-head branch rules remain independent controls. | GitHub App authentication | A workflow can mint an installation token with explicit repository permissions | Publication token is minted only after non-executing validation | | Artifact handoff | Artifact upload/download actions expose immutable IDs and digests | Patch ID, digest, base, size, count, and modes are checked twice | | OpenCode | OpenCode is a programmable coding agent with provider configuration | One checksum-pinned binary calls only the loopback gateway | -| contextual-orchestrator | The pinned gateway discovers configured provider models and routes through its KV credential seam | All five provider keys are registered at bootstrap; OpenCode receives only a loopback token | +| contextual-orchestrator | The pinned gateway discovers configured provider models, filters general-chat eligibility, and routes through its KV credential seam | All five provider keys are registered at bootstrap; endpoint-only models stay out of chat routing; OpenCode receives only a loopback token | | Provider APIs | OpenAI-compatible and provider-specific model-list APIs expose discoverable models | Discovery is attempted for OpenAI, OpenRouter, both NVIDIA NIM keys, and Bytez before selection | | SSDF | NIST SP 800-218 recommends protected build environments, review, provenance, and vulnerability response | Fresh verification and ordinary PR governance remain mandatory | | AI risk | ISO/IEC 23894:2023 and 42001:2023 require contextual risk treatment and controlled change | Stable no-op reasons, bounded proposals, traceable decisions, and rollback are documented | From da2bc4c305dc59165a6ad04a79dd6cb495855c99 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:13:00 +0900 Subject: [PATCH 07/12] fix(scheduler): gate product gaps on empty issue queue --- .../hourly-nim-product-development.yml | 17 ++++++++++++++++- CHANGELOG.md | 1 + .../hourly-nim-opencode-development.md | 3 ++- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 13 +++++++++---- .../test_hourly_nim_product_development.py | 4 ++++ 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.github/workflows/hourly-nim-product-development.yml b/.github/workflows/hourly-nim-product-development.yml index a118a77c8..a1a0c9a2e 100644 --- a/.github/workflows/hourly-nim-product-development.yml +++ b/.github/workflows/hourly-nim-product-development.yml @@ -55,7 +55,7 @@ jobs: env: DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }} steps: - - name: Enforce zero-open-PR single-flight gate + - name: Enforce zero-open-PR-and-issue single-flight gate id: gate shell: bash env: @@ -75,6 +75,16 @@ jobs: echo 'reason=open_pull_request' >>"$GITHUB_OUTPUT" exit 0 fi + if ! open_issues="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --limit 1 --json number,url)"; then + echo 'dispatch=false' >>"$GITHUB_OUTPUT" + echo 'reason=issue_inventory_unavailable' >>"$GITHUB_OUTPUT" + exit 0 + fi + if [ "$(jq 'length' <<<"$open_issues")" -gt 0 ]; then + echo 'dispatch=false' >>"$GITHUB_OUTPUT" + echo 'reason=open_issue' >>"$GITHUB_OUTPUT" + exit 0 + fi if [ "$ORCHESTRATOR_KEYS_CONFIGURED" != true ] && [ "$DRY_RUN" != true ]; then echo 'dispatch=false' >>"$GITHUB_OUTPUT" echo 'reason=contextual_orchestrator_credentials_unavailable' >>"$GITHUB_OUTPUT" @@ -603,6 +613,11 @@ jobs: exit 1 fi [ "$(jq 'length' <<<"$open_prs")" -eq 0 ] || { echo '::error::open_pull_request_after_generation'; exit 1; } + if ! open_issues="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --limit 1 --json number,url)"; then + echo '::error::issue_inventory_unavailable_after_generation' + exit 1 + fi + [ "$(jq 'length' <<<"$open_issues")" -eq 0 ] || { echo '::error::open_issue_after_generation'; exit 1; } live_base="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${DEFAULT_BRANCH}" --jq '.object.sha')" [ "$live_base" = "$expected_base" ] || { echo '::error::base_branch_advanced'; exit 1; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 93891a271..e6f0afec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang - `persistence_postgres` live SQL port: `SqlSession` transport, migration batch applicator, document/audit SQL contracts, `LiveDocumentRepository`, and fail-closed `DATABASE_URL`/`LiveSqlxConfig` gate for SQLx pool wiring (optional `live-sqlx` driver attaches `PgPool`). - `membership_core` Kish effective sample size, design effect, and group-normalized ESS helpers for multiple-membership estimation inputs. - Credential-separated hourly NVIDIA NIM/OpenCode product-development workflow (issue #2): proposal, independent verification, and late Maintainer-App publication with `NVIDIA_NIM_API_KEY` only for model work. +- Hourly product-development queue gate now fails closed when either an open pull request or an open issue exists, including a second queue check immediately before publication. - Documented modular naruon consumer contract for TEPP analysis-run and export surfaces, with a committed example request payload. - Documented contextual-orchestrator interpretation port boundary and credential separation for TEPP. - Foundation validation/release-readiness ledger at `docs/validation/temporal-event-foundation.md` tracking capability maturity and scientific acceptance gates. diff --git a/docs/doctoring/hourly-nim-opencode-development.md b/docs/doctoring/hourly-nim-opencode-development.md index 5d30f8240..f676bb28d 100644 --- a/docs/doctoring/hourly-nim-opencode-development.md +++ b/docs/doctoring/hourly-nim-opencode-development.md @@ -2,7 +2,8 @@ ## Claim boundary -This workflow can propose one bounded pull request. It does not claim autonomous +This workflow can propose one bounded pull request only after both the pull-request +and issue queues are empty. It does not claim autonomous semantic correctness, production safety, customer acceptance, scientific validity, or permission to merge, release, deploy, or approve. Deterministic tests, schemas, security scanners, CodeRabbit/OpenCode review, human judgment, diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 086f23af9..15c421ea1 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -1,7 +1,8 @@ # Hourly Contextual Orchestrator Product Development The hourly contextual-orchestrator product-development workflow proposes one bounded -commercial-quality increment when the repository has no open pull request. It is +commercial-quality increment when the repository has no open pull request or open +issue. It is separate from the deterministic minute-17 quality sentinel and never merges, releases, deploys, approves, or changes reviewer credentials. @@ -13,9 +14,9 @@ avoids the busiest scheduler boundary. A repository-scoped concurrency group does not cancel an active run. Before checkout or model execution, the proposal job reads at most one open pull -request. Unreadable inventory, any open PR, any missing provider key, or a -missing Maintainer App configuration produces a stable fail-closed no-op. A dry -run may print the task contract without credentials. +request and one open issue. Unreadable inventory, any open PR or issue, any +missing provider key, or a missing Maintainer App configuration produces a stable +fail-closed no-op. A dry run may print the task contract without credentials. When a PR exists, normal review → repair → exact-head Checks → merge governance owns the hour. The scheduler does not create a competing branch. @@ -115,6 +116,10 @@ Stable no-op reasons are: - `pull_request_inventory_unavailable` - `open_pull_request` +- `issue_inventory_unavailable` +- `open_issue` +- `issue_inventory_unavailable_after_generation` +- `open_issue_after_generation` - `contextual_orchestrator_credentials_unavailable` - `maintainer_app_unavailable` diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index 0b18ee787..1859dec11 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -63,10 +63,14 @@ def test_hourly_workflow_schedule_credentials_and_queue_gate(self) -> None: "sha256sum -c", "pull_request_inventory_unavailable", "open_pull_request", + "issue_inventory_unavailable", + "open_issue", "contextual_orchestrator_credentials_unavailable", "maintainer_app_unavailable", "base_branch_advanced", "open_pull_request_after_generation", + "issue_inventory_unavailable_after_generation", + "open_issue_after_generation", "ContextualWisdomLab/TEPP", ): self.assertIn(token, text) From 9558591e1318d055c46e2fe6c6abd328a1607c86 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:14:30 +0900 Subject: [PATCH 08/12] docs(scheduler): describe issue queue ownership --- docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index e4fd0d332..6c5795db9 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -18,8 +18,8 @@ request and one open issue. Unreadable inventory, any open PR or issue, any missing provider key, or a missing Maintainer App configuration produces a stable fail-closed no-op. A dry run may print the task contract without credentials. -When a PR exists, normal review → repair → exact-head Checks → merge governance -owns the hour. The scheduler does not create a competing branch. +When a PR or issue exists, normal review → repair → exact-head Checks → merge +governance owns the hour. The scheduler does not create a competing branch. ## Required repository configuration From 087ed2d06d621575aaea8955d5cf1819d830f4e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:15:48 +0900 Subject: [PATCH 09/12] fix(scheduler): grant issue inventory read access --- .github/workflows/hourly-nim-product-development.yml | 2 ++ tests/quality/test_hourly_nim_product_development.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/hourly-nim-product-development.yml b/.github/workflows/hourly-nim-product-development.yml index a1a0c9a2e..f986237eb 100644 --- a/.github/workflows/hourly-nim-product-development.yml +++ b/.github/workflows/hourly-nim-product-development.yml @@ -43,6 +43,7 @@ jobs: timeout-minutes: 55 permissions: contents: read + issues: read pull-requests: read outputs: has_changes: ${{ steps.proposal.outputs.has_changes }} @@ -597,6 +598,7 @@ jobs: owner: ContextualWisdomLab repositories: ${{ github.event.repository.name }} permission-contents: write + permission-issues: read permission-metadata: read permission-pull-requests: write diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index af85d3118..ec3c59229 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -52,6 +52,8 @@ def test_hourly_workflow_schedule_credentials_and_queue_gate(self) -> None: "secrets.NVIDIA_NIM_API_KEY_SUB", "secrets.OPENROUTER_API_KEY", "secrets.OPENAI_API_KEY", + "issues: read", + "permission-issues: read", "CONTEXTUAL_ORCHESTRATOR_COMMIT", "CONTEXTUAL_ORCHESTRATOR_SHA256", "run_contextual_orchestrator.py", From 44903e35b54d23effbb30bec556ef75ef5c32ccf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:16:55 +0900 Subject: [PATCH 10/12] test(scheduler): lock issue inventory command --- tests/quality/test_hourly_nim_product_development.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index ec3c59229..c5d056fe2 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -65,6 +65,7 @@ def test_hourly_workflow_schedule_credentials_and_queue_gate(self) -> None: "sha256sum -c", "pull_request_inventory_unavailable", "open_pull_request", + "gh issue list --repo", "issue_inventory_unavailable", "open_issue", "contextual_orchestrator_credentials_unavailable", From b4914aadb65e557588f606611f644c2bbf437ebe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:06:45 +0900 Subject: [PATCH 11/12] docs(hourly): document issue queue revalidation --- docs/adr/0017-hourly-contextual-orchestrator-gateway.md | 3 ++- docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0017-hourly-contextual-orchestrator-gateway.md b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md index ae2be9c5a..cc0e21b06 100644 --- a/docs/adr/0017-hourly-contextual-orchestrator-gateway.md +++ b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md @@ -34,7 +34,8 @@ loopback bearer token. It receives no provider credential. Liveness and authenticated model-list checks must pass before the proposal agent runs. The workflow remains fail-closed when any provider credential is absent, the -queue is unreadable, an open PR exists, or the publication App is unavailable. +PR or issue queue is unreadable, an open PR or issue exists, or the publication +App is unavailable. The verifier and publisher receive neither provider nor model credentials. ## Alternatives considered diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 6c5795db9..2e125531f 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -94,8 +94,8 @@ build scripts, packages, binaries, or shell files. The copied parser rejects symlinks, non-regular files, malformed UTF-8, unsupported controls, bidirectional spoofing, and byte-limit violations. Only after bounded metadata is written does the publisher mint the repository-scoped -Maintainer App token. It then rechecks open-PR inventory and live `main`, pushes -one unique branch, and calls `gh pr create` exactly once. +Maintainer App token. It then rechecks open-PR and open-issue inventory plus live +`main`, pushes one unique branch, and calls `gh pr create` exactly once. ## Proposal contract From 3a0fbfc24a2294e43ad04ee72064362e4385dee9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:12:51 +0900 Subject: [PATCH 12/12] docs(hourly): record nonvisual figma boundary --- docs/adr/0017-hourly-contextual-orchestrator-gateway.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/adr/0017-hourly-contextual-orchestrator-gateway.md b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md index cc0e21b06..9cb32d1bd 100644 --- a/docs/adr/0017-hourly-contextual-orchestrator-gateway.md +++ b/docs/adr/0017-hourly-contextual-orchestrator-gateway.md @@ -61,6 +61,8 @@ The verifier and publisher receive neither provider nor model credentials. every configured provider before the schedule is enabled. - The workflow is not a Figma or visual interaction surface; no Figma file is applicable to this headless scheduler decision. +- Figma File ID: N/A — this headless scheduler introduces no visual interaction + contract or design artifact. ## Verification