Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
963b454
feat(noema): wire Noema to a same-job contextual-orchestrator sidecar
seonghobae Aug 18, 2026
c964271
feat(opencode): wire the real dispatch model pool to contextual-orche…
seonghobae Aug 18, 2026
d1454e0
fix(noema): allow-list GitHub CLI execution
seonghobae Aug 19, 2026
de0cbf7
test(opencode): pin updated review workflow blob
seonghobae Aug 19, 2026
2c6530a
fix: pin orchestrator sidecar source and bound readiness
seonghobae Aug 19, 2026
a83257e
test: pin sidecar routing security contracts
seonghobae Aug 19, 2026
c233890
ci: repair contextual sidecar review blockers
seonghobae Aug 19, 2026
c4428dc
chore: remove inert one-shot repair workflow
seonghobae Aug 19, 2026
8105d55
fix(review): enforce sidecar privacy and provider allowlist
seonghobae Aug 19, 2026
07d7d9e
fix(security): start orchestrator sidecar only for public reviews
seonghobae Aug 20, 2026
ae090ab
fix(security): validate sidecar licenses before install
seonghobae Aug 20, 2026
5dd18d0
test(sidecar): bind license evidence to lock artifacts
seonghobae Aug 20, 2026
b63f0ff
test: expose process-local sidecar credential gap
seonghobae Aug 20, 2026
7044963
fix: keep sidecar bootstrap and credentials in one process
seonghobae Aug 20, 2026
d224ced
fix: bind sidecar licenses to locked artifacts
seonghobae Aug 20, 2026
3268c25
test: bind cleanup disarm to successful readiness
seonghobae Aug 20, 2026
3d45c3e
test: require a non-overridable sidecar source pin
seonghobae Aug 20, 2026
ee277c4
fix: make the sidecar source pin non-overridable
seonghobae Aug 20, 2026
cd9f025
test: complete sidecar contract docstrings
seonghobae Aug 20, 2026
4006c43
fix(noema): confine optional codegraph context
seonghobae Aug 20, 2026
82b5aa7
docs(noema): record codegraph context boundary
seonghobae Aug 20, 2026
81c1872
fix(noema): reject platform path separators
seonghobae Aug 20, 2026
3840d87
docs(noema): cite current NIST control release
seonghobae Aug 20, 2026
101e690
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/noema-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,16 @@ jobs:
esac
echo "is_private=$is_private" >>"$GITHUB_OUTPUT"

- name: Start contextual-orchestrator sidecar
if: env.PR_NUMBER != '' && steps.target_visibility.outputs.is_private == 'false'
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: bash scripts/ci/start_contextual_orchestrator_sidecar.sh

- name: Run Noema LLM review and submit verdict
if: env.PR_NUMBER != ''
env:
Expand All @@ -276,6 +286,8 @@ jobs:
NOEMA_LLM_MODEL: ${{ vars.NOEMA_LLM_MODEL || '' }}
NOEMA_LLM_API_KEY: ${{ secrets.NOEMA_LLM_API_KEY || secrets.OPENAI_API_KEY || '' }}
NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}
CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ env.CONTEXTUAL_ORCHESTRATOR_BASE_URL || '' }}
CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ env.CONTEXTUAL_ORCHESTRATOR_TOKEN || '' }}
TARGET_REPOSITORY_PRIVATE: ${{ steps.target_visibility.outputs.is_private }}
run: |
set -euo pipefail
Expand All @@ -287,6 +299,15 @@ jobs:
echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot submit a verdict."
exit 1
fi
# Prefer the contextual-orchestrator sidecar (auto-discovers and
# cost-optimizes across every registered upstream provider) over the
# single-provider NVIDIA NIM direct fallback below, when the sidecar
# started successfully and nothing more specific was configured.
if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ] && [ -z "${NOEMA_LLM_API_URL:-}" ] && [ -z "${NOEMA_LLM_MODEL:-}" ] && [ -n "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ]; then
export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL}/chat/completions"
export NOEMA_LLM_MODEL="contextual-orchestrator"
export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ] && [ -n "${NVIDIA_NIM_API_KEY:-}" ] && [ -z "${NOEMA_LLM_API_URL:-}" ] && [ -z "${NOEMA_LLM_MODEL:-}" ]; then
export NOEMA_LLM_API_URL="https://integrate.api.nvidia.com/v1/chat/completions"
export NOEMA_LLM_MODEL="nvidia/nemotron-3-ultra-550b-a55b"
Expand Down
34 changes: 32 additions & 2 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3230,6 +3230,16 @@ jobs:
)
PY

- name: Start contextual-orchestrator sidecar
if: needs.coverage-evidence.result == 'success'
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: bash scripts/ci/start_contextual_orchestrator_sidecar.sh

- name: Prepare isolated OpenCode review workspace
env:
OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project
Expand Down Expand Up @@ -3583,7 +3593,7 @@ jobs:
"$schema": "https://opencode.ai/config.json",
"model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5",
"small_model": "nvidia-nim/meta/llama-3.3-70b-instruct",
"enabled_providers": ["nvidia-nim", "opencode-free", "opencode", "openai", "openrouter", "github-models"],
"enabled_providers": ["contextual-orchestrator", "nvidia-nim", "opencode-free", "opencode", "openai", "openrouter", "github-models"],
"lsp": false,
"mcp": {},
"permission": {
Expand Down Expand Up @@ -4261,6 +4271,24 @@ jobs:
}
}
}
},
"contextual-orchestrator": {
"npm": "@ai-sdk/openai-compatible",
"name": "Contextual Orchestrator",
"options": {
"baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}",
"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"
},
"models": {
"contextual-orchestrator": {
"name": "Contextual Orchestrator (auto-routed)",
"tool_call": true,
"limit": {
"context": 200000,
"output": 32768
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}' >"${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc"
Expand Down Expand Up @@ -4293,6 +4321,8 @@ jobs:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}
CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ env.CONTEXTUAL_ORCHESTRATOR_BASE_URL || '' }}
CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ env.CONTEXTUAL_ORCHESTRATOR_TOKEN || '' }}
SHARE: "false"
NPM_CONFIG_IGNORE_SCRIPTS: "true"
NO_COLOR: "1"
Expand All @@ -4313,7 +4343,7 @@ jobs:
# cost-efficient tier, cheaper than the legacy gpt-5 it replaced
# ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget
# stretches further between top-ups.
OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free opencode-free/hy3-free opencode-free/minimax-m3-free opencode-free/glm-5-free opencode-free/kimi-k2.5-free opencode-free/qwen3.6-plus-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1"
OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'contextual-orchestrator/contextual-orchestrator nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free opencode-free/hy3-free opencode-free/minimax-m3-free opencode-free/glm-5-free opencode-free/kimi-k2.5-free opencode-free/qwen3.6-plus-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1"
# One attempt per model, then fall through to the next model. Retrying
# the SAME model 5x let a rate-limited/hung leader consume the whole
# step, so the pool never reached a healthy fallback model.
Expand Down
45 changes: 45 additions & 0 deletions docs/doctoring/noema-codegraph-context-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Noema CodeGraph Context Boundary

## Customer action

Keep `NOEMA_CODEGRAPH_CONTEXT_PATH` unset unless the review job has generated a
context file inside its checked-out workspace. When it is set, use a relative
regular-file path such as `.codegraph/review-context.md`; absolute paths,
`..` traversal, and symlinked components are rejected before any bytes are read.

## Root-cause record

The Noema reviewer accepted an environment-provided filename directly in
`load_codegraph_context()`. A review-job process that inherited an unintended
absolute or traversal path could therefore read files outside the repository.
This is CWE-22 path traversal: canonicalization must remain inside a restricted
parent, and error output must not disclose the rejected path.

The repair establishes one bounded trust boundary:

1. accept only a non-empty relative path without `..` components;
2. reject symlinks in every path component;
3. resolve the candidate and require it to remain below the current workspace;
4. require a regular file and read at most `MAX_REVIEW_CONTEXT_CHARS + 1` bytes;
5. return a generic diagnostic so the untrusted environment value is not echoed.

## Verification

The contract tests cover a valid workspace file, absolute traversal, relative
traversal, a symlink escape, bounded reads, and the existing review-context
assembly. The check is intentionally fail-closed: missing or invalid context
reduces evidence rather than expanding file access.

## Standards and authoritative references (APA 7th)

Joint Task Force. (2020/2025). *Security and privacy controls for information
systems and organizations* (NIST Special Publication 800-53, Revision 5,
Release 5.2.0). National Institute of Standards and Technology.
https://doi.org/10.6028/NIST.SP.800-53r5

MITRE. (n.d.). *CWE-22: Improper limitation of a pathname to a restricted
directory ('Path traversal')* (Version 4.20). Common Weakness Enumeration.
Retrieved August 21, 2026, from https://cwe.mitre.org/data/definitions/22

OWASP Foundation. (n.d.). *Path traversal*. Retrieved August 21, 2026, from
https://owasp.org/www-community/attacks/Path_Traversal
13 changes: 0 additions & 13 deletions opencode.jsonc
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"$schema": "https://opencode.ai/config.json",
// NOT switched to "contextual-orchestrator/contextual-orchestrator" yet:
// that requires CONTEXTUAL_ORCHESTRATOR_BASE_URL/_TOKEN to be provisioned
// first (see the "contextual-orchestrator" provider block below).
"model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5",
"small_model": "nvidia-nim/meta/llama-3.3-70b-instruct",
"enabled_providers": ["nvidia-nim", "github-models", "contextual-orchestrator"],
Expand Down Expand Up @@ -375,15 +372,6 @@
}
}
},
// Added (not yet the default -- see model/small_model above): the org's
// contextual-orchestrator LLM gateway. It auto-discovers models across
// Bytez/NVIDIA NIM (x2 keys)/OpenRouter/OpenAI from KV-registered
// credentials and auto-optimizes routing by cost, so pointing OpenCode at
// one model id here delegates upstream selection to the gateway. Requires
// CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN to be
// provisioned as repo/org Actions variables before switching the default
// model/small_model above to "contextual-orchestrator/contextual-orchestrator";
// until then this provider is defined but unused, so OpenCode keeps working.
"contextual-orchestrator": {
"npm": "@ai-sdk/openai-compatible",
"name": "Contextual Orchestrator",
Expand All @@ -395,7 +383,6 @@
"contextual-orchestrator": {
"name": "Contextual Orchestrator (auto-routed)",
"tool_call": true,
"reasoning": true,
"limit": {
"context": 200000,
"output": 32768
Expand Down
13 changes: 13 additions & 0 deletions scripts/ci/assert_opencode_reasoning_effort.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@

import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any

_JSONC_LINE_COMMENT = re.compile(r"^[ \t]*//.*$", re.MULTILINE)


def strip_jsonc_line_comments(text: str) -> str:
"""Strip whole-line ``//`` comments so JSONC content parses as strict JSON.

Only strips a ``//`` that starts a line (after leading whitespace); this
file's comments are always written that way, never trailing after a JSON
token, so this cannot accidentally eat a string value containing "//".
"""
return _JSONC_LINE_COMMENT.sub("", text)


def is_known_reasoning_capable(model_name: str) -> bool:
"""Return whether the model family is expected to support reasoning effort."""
Expand Down
63 changes: 50 additions & 13 deletions scripts/ci/noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import ipaddress
import json
import os
import pathlib
import re
import socket
import subprocess
Expand Down Expand Up @@ -41,6 +42,8 @@
MAX_FILE_CONTEXT_CHARS = 4000
MAX_REVIEW_CONTEXT_CHARS = 24000
MAX_THREAD_BODY_CHARS = 1200
GH_EXECUTABLE = "gh"
REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")

# ⚡ Bolt: Pre-compiled regex patterns to avoid recompilation on every scrub_sensitive_data call.
# Impact: Improves string processing performance in error reporting.
Expand All @@ -65,11 +68,16 @@ def scrub_sensitive_data(text: str | None) -> str | None:


def run(args: Sequence[str], *, stdin: str | None = None) -> str:
"""Run a command without invoking a shell and return stdout."""
"""Run an allow-listed GitHub CLI argv without invoking a shell."""
if isinstance(args, str):
raise TypeError("run() requires argv, not a shell command string")
argv = list(args)
if not argv or any(not isinstance(arg, str) for arg in argv):
raise TypeError("run() requires a non-empty sequence of argv strings")
if argv[0] != GH_EXECUTABLE:
raise ValueError("run() only permits the GitHub CLI executable")
completed = subprocess.run(
list(args),
[GH_EXECUTABLE, *argv[1:]],
input=stdin,
text=True,
stdout=subprocess.PIPE,
Expand All @@ -80,16 +88,16 @@ def run(args: Sequence[str], *, stdin: str | None = None) -> str:
if completed.returncode != 0:
scrubbed_stderr = scrub_sensitive_data(completed.stderr.strip())
raise RuntimeError(
f"Command failed ({completed.returncode}): {args[0]}\n{scrubbed_stderr}"
f"Command failed ({completed.returncode}): {GH_EXECUTABLE}\n{scrubbed_stderr}"
)
return completed.stdout


def split_repo(repo: str) -> tuple[str, str]:
"""Split an owner/name repository string into owner and repository."""
owner, name = repo.split("/", 1)
if not owner or not name:
if not REPOSITORY_RE.fullmatch(repo):
raise ValueError(f"repo must be owner/name, got {repo!r}")
owner, name = repo.split("/", 1)
return owner, name


Expand Down Expand Up @@ -382,14 +390,32 @@ def review_thread_context(pr: dict[str, Any]) -> str:

def load_codegraph_context() -> str:
"""Load optional precomputed CodeGraph context for structural review evidence."""
path = os.environ.get("NOEMA_CODEGRAPH_CONTEXT_PATH", "").strip()
if not path:
raw_path = os.environ.get("NOEMA_CODEGRAPH_CONTEXT_PATH", "").strip()
if not raw_path:
return ""
candidate = pathlib.Path(raw_path)
if candidate.is_absolute() or "\\" in raw_path or ".." in candidate.parts:
return "CodeGraph context unavailable: path must stay within the workspace"
workspace = pathlib.Path.cwd().resolve()
try:
with open(path, encoding="utf-8") as handle:
return truncate_text(handle.read(), MAX_REVIEW_CONTEXT_CHARS)
current = workspace
for part in candidate.parts:
current /= part
if current.is_symlink():
return "CodeGraph context unavailable: symlinked paths are not allowed"
resolved = candidate.resolve(strict=True)
resolved.relative_to(workspace)
if not resolved.is_file():
return "CodeGraph context unavailable: context path is not a regular file"
with resolved.open(encoding="utf-8") as handle:
return truncate_text(
handle.read(MAX_REVIEW_CONTEXT_CHARS + 1),
MAX_REVIEW_CONTEXT_CHARS,
)
except OSError as exc:
return f"CodeGraph context unavailable: {exc}"
return f"CodeGraph context unavailable: {type(exc).__name__}"
except ValueError:
return "CodeGraph context unavailable: path is outside the workspace"


def build_review_context(repo: str, number: int, pr: dict[str, Any]) -> str:
Expand Down Expand Up @@ -460,8 +486,17 @@ def call_llm(
hostname = (parsed.hostname or "").lower()
if not hostname:
raise ValueError("URL must have a valid hostname")
if hostname in {"localhost", "localhost.localdomain"} or hostname.endswith(".localhost"):
raise ValueError("URL cannot target localhost")
# Loopback is allowed: NOEMA_LLM_API_URL is only ever set from a
# repository/org-level Actions *variable* (see noema-review.yml), which
# only someone with write access to repo settings can set -- it is never
# influenced by pull request content. The trust boundary here is "who can
# set the variable," not "what IP range the value resolves to," so
# blocking loopback specifically doesn't add real protection once that's
# true -- and it's exactly the target needed to reach a same-job
# contextual-orchestrator sidecar (see start_contextual_orchestrator_sidecar.sh).
# Non-loopback internal ranges stay blocked below: those would indicate a
# genuine misconfiguration (an unintended internal service), not this
# deliberate same-job pattern.
try:
addrinfo = socket.getaddrinfo(hostname, None)
except socket.gaierror:
Expand All @@ -473,7 +508,9 @@ def call_llm(
ip = ipaddress.ip_address(ip_str)
except ValueError:
continue
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified:
if ip.is_loopback:
continue
if ip.is_private or ip.is_link_local or ip.is_multicast or ip.is_unspecified:
raise ValueError("URL cannot target internal IP addresses")

prompt = {
Expand Down
11 changes: 11 additions & 0 deletions scripts/ci/run_opencode_review_model_pool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,13 @@ is_openrouter_candidate() {
esac
}

is_contextual_orchestrator_candidate() {
case "$1" in
contextual-orchestrator/*) return 0 ;;
*) return 1 ;;
esac
}

is_nvidia_nim_candidate() {
case "$1" in
nvidia-nim/*) return 0 ;;
Expand Down Expand Up @@ -416,6 +423,10 @@ should_skip_model_candidate() {
printf 'Skipping OpenCode %s because scoped NVIDIA_NIM_API_KEY is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate"
return 0
fi
if is_contextual_orchestrator_candidate "$model_candidate" && [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ]; then
printf 'Skipping OpenCode %s because the contextual-orchestrator sidecar did not start (no upstream provider credential configured); falling back to the next provider-qualified candidate.\n' "$model_candidate"
return 0
fi
return 1
}

Expand Down
Loading
Loading