Skip to content

fix(cli): reject MCP credential collisions before rebuild #35904

fix(cli): reject MCP credential collisions before rebuild

fix(cli): reject MCP credential collisions before rebuild #35904

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
name: PR Review / Advisor
on:
# The target event loads this workflow from the trusted base branch and makes
# advisor secrets available for both internal and fork PRs. The analysis job
# never executes PR-controlled content and has no write permission.
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, edited]
workflow_dispatch:
inputs:
base_ref:
description: Base ref to diff against
required: false
default: origin/main
head_ref:
description: Head ref to diff
required: false
default: HEAD
target_repo:
description: Optional repo to analyze, e.g. NVIDIA/NemoClaw
required: false
type: string
default: ""
target_pr:
description: Optional pull request number in target_repo to analyze
required: false
type: string
default: ""
target_base:
description: Base branch to use with target_repo/target_pr manual analysis
required: false
type: string
default: main
run_analysis:
description: Run PR review advisor analysis
required: false
type: boolean
default: true
# Each job declares its own privilege domain. In particular, no model-bearing
# job can write to a pull request, and the publisher never receives the model
# credential or the untrusted PR worktree.
permissions: {}
concurrency:
group: pr-review-advisor-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}-${{ inputs.target_repo || github.repository }}-${{ inputs.target_pr || '' }}-${{ github.event_name != 'pull_request_target' || github.event.action != 'edited' || github.event.changes.base != null }}
cancel-in-progress: true
jobs:
review:
name: PR review advisor (${{ matrix.advisor.label }})
if: ${{ github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'pull_request_target' || github.event.action != 'edited' || github.event.changes.base != null) }}
permissions:
actions: read
checks: read
contents: read
issues: read
pull-requests: read
runs-on: ubuntu-24.04
timeout-minutes: 40
continue-on-error: ${{ !matrix.advisor.publish_comment }}
strategy:
fail-fast: false
matrix:
advisor:
- id: gpt-5.6-terra
label: GPT-5.6 Terra
model: azure/openai/gpt-5.6-terra
sandbox_name: pr-advisor-gpt
artifact_dir: pr-review-advisor
artifact_name: pr-review-advisor
publish_comment: true
- id: nemotron-ultra
label: Nemotron 3 Ultra
model: nvidia/nvidia/nemotron-3-ultra
sandbox_name: pr-advisor-nem
artifact_dir: pr-review-advisor-nemotron-ultra
artifact_name: pr-review-advisor-nemotron-ultra
publish_comment: false
env:
# Pin runtime packages to reviewed versions. Updates go through normal
# dependency review rather than floating in a secret-bearing job.
PI_SDK_VERSION: "0.80.6"
# The review ledger imports TypeBox directly. Pi 0.80.6 shrinkwraps its
# own copy, so the advisor runtime must install this direct dependency.
TYPEBOX_VERSION: "1.1.38"
# Workflow-boundary modules parse YAML before the advisor session starts.
YAML_VERSION: "2.8.3"
# Embedded Pi SDK sessions use Pi's proxy-aware Undici transport.
UNDICI_VERSION: "8.10.0"
# Credential-free inventory discovery executes the trusted Vitest entrypoint.
VITEST_VERSION: "4.1.9"
FD_FIND_VERSION: "9.0.0-1"
RIPGREP_VERSION: "14.1.0-1"
OPENSHELL_GATEWAY_ENDPOINT: http://127.0.0.1:8080
PI_IMAGE: ghcr.io/nvidia/openshell-community/sandboxes/pi@sha256:00d0c5e9e733f94f6db3eaa2ab70d4fd75bcc4aace6b13a54535cbf2dd20dfcd
PR_REVIEW_ADVISOR_TIMEOUT_MS: "900000"
PR_REVIEW_ADVISOR_HEARTBEAT_MS: "60000"
PR_REVIEW_ADVISOR_SANDBOX_TIMEOUT_SECONDS: "2100"
PR_REVIEW_ADVISOR_MODEL: ${{ matrix.advisor.model }}
PR_REVIEW_ADVISOR_ARTIFACT_DIR: ${{ matrix.advisor.artifact_dir }}
PR_REVIEW_ADVISOR_RUN_ANALYSIS: ${{ github.event_name == 'workflow_dispatch' && inputs.run_analysis == false && '0' || '1' }}
PR_REVIEW_ADVISOR_COMMENT_MARKER: "<!-- nemoclaw-pr-review-advisor -->"
PR_REVIEW_ADVISOR_COMMENT_TITLE: PR Review Advisor
PR_REVIEW_ADVISOR_COMMENT_LABEL: PR review advisor
PR_REVIEW_ADVISOR_WORKFLOW_NAME: "PR Review / Advisor"
PR_REVIEW_ADVISOR_LOAD_PREVIOUS_REVIEW: "false"
SANDBOX_NAME: ${{ matrix.advisor.sandbox_name }}
# Only executable code from this checkout may run in the analysis job.
ADVISOR_DIR: ${{ github.workspace }}/advisor
TARGET_REPO: ${{ github.event_name == 'pull_request_target' && github.repository || inputs.target_repo || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.target_pr }}
steps:
- name: Checkout trusted advisor code (workflow revision)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: NVIDIA/NemoClaw
ref: ${{ github.workflow_sha }}
path: advisor
persist-credentials: false
lfs: false
submodules: false
# A dispatch without target_repo is maintainer-triggered and analyzes the
# commit that supplied the trusted workflow. It is still treated as
# read-only data and never supplies executable advisor code.
- name: Checkout dispatch workspace (read-only data)
if: ${{ github.event_name == 'workflow_dispatch' && inputs.target_repo == '' && inputs.target_pr == '' }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
path: pr-workdir
fetch-depth: 0
persist-credentials: false
lfs: false
submodules: false
- name: Set default advisor workdir
if: ${{ github.event_name == 'workflow_dispatch' && inputs.target_repo == '' && inputs.target_pr == '' }}
run: echo "ADVISOR_WORKDIR=$GITHUB_WORKSPACE/pr-workdir" >> "$GITHUB_ENV"
# Provision Node before the isolated workspace is fetched so the trusted
# prepare helper (type-stripped .mts) runs from the pinned advisor
# checkout, never from PR-controlled content.
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
# pull_request_target content is fetched manually so no PR-controlled
# action, hook, submodule, LFS filter, or package setup can run. The base
# and head are bound to the immutable SHAs in the triggering event. The
# fetch/validation logic lives in the trusted, unit-tested helper and is
# executed only from $ADVISOR_DIR (github.workflow_sha checkout).
- name: Prepare isolated analysis workspace
if: ${{ github.event_name == 'pull_request_target' || (github.event_name == 'workflow_dispatch' && (inputs.target_repo != '' || inputs.target_pr != '')) }}
env:
TARGET_REPO: ${{ github.event_name == 'pull_request_target' && github.repository || inputs.target_repo }}
TARGET_PR: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || inputs.target_pr }}
TARGET_BASE: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.ref || inputs.target_base }}
PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || '' }}
EXPECTED_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
GIT_LFS_SKIP_SMUDGE: "1"
TARGET_DIR: ${{ github.workspace }}/pr-workdir
run: |
node --experimental-strip-types \
"$ADVISOR_DIR/tools/pr-review-advisor/prepare-target-pr.mts"
# The advisor reads repository files inside OpenShell. Remove worktree
# symlinks first so an untrusted link cannot redirect a read outside the
# uploaded repository. Git still retains the committed link target.
- name: Remove symlinks from analysis workspace
shell: bash
run: |
while IFS= read -r -d '' link; do
rm -- "$link"
done < <(find "$ADVISOR_WORKDIR" -type l -print0)
- name: Install Pi SDK
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
"fd-find=${FD_FIND_VERSION}" \
"ripgrep=${RIPGREP_VERSION}"
INSTALLED_FD_FIND_VERSION="$(dpkg-query -W -f='${Version}' fd-find)"
INSTALLED_RIPGREP_VERSION="$(dpkg-query -W -f='${Version}' ripgrep)"
if [ "$INSTALLED_FD_FIND_VERSION" != "$FD_FIND_VERSION" ]; then
echo "::error::fd-find package version $INSTALLED_FD_FIND_VERSION does not match $FD_FIND_VERSION"
exit 1
fi
if [ "$INSTALLED_RIPGREP_VERSION" != "$RIPGREP_VERSION" ]; then
echo "::error::ripgrep package version $INSTALLED_RIPGREP_VERSION does not match $RIPGREP_VERSION"
exit 1
fi
command -v fdfind >/dev/null
command -v rg >/dev/null
EXPECTED_FD_BINARY_VERSION="${FD_FIND_VERSION%%-*}"
EXPECTED_RG_BINARY_VERSION="${RIPGREP_VERSION%%-*}"
FD_BINARY_VERSION="$(fdfind --version)"
RG_BINARY_VERSION="$(rg --version)"
RG_BINARY_VERSION="${RG_BINARY_VERSION%%$'\n'*}"
if [ "$FD_BINARY_VERSION" != "fdfind $EXPECTED_FD_BINARY_VERSION" ]; then
echo "::error::fdfind binary version $FD_BINARY_VERSION does not match fdfind $EXPECTED_FD_BINARY_VERSION"
exit 1
fi
if [ "$RG_BINARY_VERSION" != "ripgrep $EXPECTED_RG_BINARY_VERSION" ]; then
echo "::error::rg binary version $RG_BINARY_VERSION does not match ripgrep $EXPECTED_RG_BINARY_VERSION"
exit 1
fi
# Install only from the trusted workflow checkout's committed lockfile.
# --ignore-scripts keeps package lifecycle code out of this secret-bearing job.
(
cd "$ADVISOR_DIR"
npm ci --ignore-scripts --no-audit --no-fund
)
# Materialize GitHub metadata before sandbox creation. This is the only
# analysis-phase step with a GitHub token, and it has no model credential.
- name: Prepare advisor sandbox inputs
env:
GH_TOKEN: ${{ github.token }}
run: node --experimental-strip-types --no-warnings "$ADVISOR_DIR/tools/pr-review-advisor/openshell.mts" prepare
- name: Install OpenShell
if: ${{ env.PR_REVIEW_ADVISOR_RUN_ANALYSIS == '1' }}
run: |
env -u GITHUB_TOKEN -u GH_TOKEN -u PR_REVIEW_ADVISOR_API_KEY \
NEMOCLAW_NON_INTERACTIVE=1 \
bash "$ADVISOR_DIR/scripts/install-openshell.sh"
# The upstream model key exists only while the trusted host registers the
# provider. The sandbox uses inference.local and receives an inert key.
- name: Configure OpenShell inference
id: configure-openshell
if: ${{ env.PR_REVIEW_ADVISOR_RUN_ANALYSIS == '1' }}
continue-on-error: true
env:
OPENAI_API_KEY: ${{ secrets.PR_REVIEW_ADVISOR_API_KEY }}
run: node --experimental-strip-types --no-warnings "$ADVISOR_DIR/tools/pr-review-advisor/openshell.mts" configure
# Preserve a review artifact when the provider credential is unavailable
# or gateway configuration fails. This trusted host fallback has neither
# a GitHub token nor a model credential and never executes PR content.
- name: Write unavailable advisor artifacts
id: unavailable-analysis
if: ${{ always() && steps.configure-openshell.outcome != 'success' }}
env:
BASE_REF: ${{ github.event_name == 'pull_request_target' && 'target/base' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'target/base' || inputs.base_ref) }}
HEAD_REF: ${{ github.event_name == 'pull_request_target' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}
PR_REVIEW_ADVISOR_UNAVAILABLE_REASON: ${{ env.PR_REVIEW_ADVISOR_RUN_ANALYSIS == '0' && 'PR_REVIEW_ADVISOR_RUN_ANALYSIS=0' || 'OpenShell inference configuration failed or the advisor credential is unavailable' }}
run: node --experimental-strip-types --no-warnings "$ADVISOR_DIR/tools/pr-review-advisor/openshell.mts" unavailable
- name: Create credential-free advisor sandbox
if: ${{ steps.configure-openshell.outcome == 'success' }}
run: node --experimental-strip-types --no-warnings "$ADVISOR_DIR/tools/pr-review-advisor/openshell.mts" create
- name: Run PR review advisor
id: analysis
if: ${{ steps.configure-openshell.outcome == 'success' }}
continue-on-error: true
env:
BASE_REF: ${{ github.event_name == 'pull_request_target' && 'target/base' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'target/base' || inputs.base_ref) }}
HEAD_REF: ${{ github.event_name == 'pull_request_target' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.target_pr }}
run: node --experimental-strip-types --no-warnings "$ADVISOR_DIR/tools/pr-review-advisor/openshell.mts" run
- id: download-analysis
name: Download advisor artifacts from sandbox
if: ${{ always() && steps.configure-openshell.outcome == 'success' }}
continue-on-error: true
run: node --experimental-strip-types --no-warnings "$ADVISOR_DIR/tools/pr-review-advisor/openshell.mts" download
- name: Delete advisor sandbox
if: always()
run: node --experimental-strip-types --no-warnings "$ADVISOR_DIR/tools/pr-review-advisor/openshell.mts" delete
- name: Publish job summary
if: always()
run: |
SUMMARY_PATH="$GITHUB_WORKSPACE/artifacts/$PR_REVIEW_ADVISOR_ARTIFACT_DIR/pr-review-advisor-summary.md"
if [ -f "$SUMMARY_PATH" ]; then
cat "$SUMMARY_PATH" >> "$GITHUB_STEP_SUMMARY"
else
printf '# %s\n\nAdvisor analysis did not produce a summary. See raw artifacts/logs.\n' "$PR_REVIEW_ADVISOR_COMMENT_TITLE" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload advisor artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.advisor.artifact_name }}
path: artifacts/${{ matrix.advisor.artifact_dir }}/
if-no-files-found: warn
retention-days: 14
- name: Verify advisor analysis outcome
if: always()
env:
ANALYSIS_OUTCOME: ${{ steps.analysis.outcome }}
ANALYSIS_REQUESTED: ${{ env.PR_REVIEW_ADVISOR_RUN_ANALYSIS }}
CONFIGURE_OUTCOME: ${{ steps.configure-openshell.outcome }}
DOWNLOAD_OUTCOME: ${{ steps.download-analysis.outcome }}
UNAVAILABLE_OUTCOME: ${{ steps.unavailable-analysis.outcome }}
run: |
if [ "$CONFIGURE_OUTCOME" != "success" ]; then
if [ "$UNAVAILABLE_OUTCOME" != "success" ]; then
echo "::error::PR review advisor could not write unavailable artifacts: outcome=$UNAVAILABLE_OUTCOME"
exit 1
fi
if [ "$ANALYSIS_REQUESTED" = "0" ]; then
exit 0
fi
echo "::error::PR review advisor inference configuration did not complete: outcome=$CONFIGURE_OUTCOME"
exit 1
fi
if [ "$ANALYSIS_OUTCOME" != "success" ]; then
echo "::error::PR review advisor analysis did not complete: outcome=$ANALYSIS_OUTCOME"
exit 1
fi
if [ "$DOWNLOAD_OUTCOME" != "success" ]; then
echo "::error::PR review advisor artifacts were not downloaded: outcome=$DOWNLOAD_OUTCOME"
exit 1
fi
publish:
name: Publish PR review advisor
needs: review
if: ${{ always() && github.event_name == 'pull_request_target' && (github.event.action != 'edited' || github.event.changes.base != null) && needs.review.result != 'cancelled' }}
# Publication is best-effort and must never hide the primary analysis
# lane's required outcome in the review matrix above.
continue-on-error: true
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
PR_REVIEW_ADVISOR_COMMENT_MARKER: "<!-- nemoclaw-pr-review-advisor -->"
PR_REVIEW_ADVISOR_COMMENT_TITLE: PR Review Advisor
PR_REVIEW_ADVISOR_COMMENT_LABEL: PR review advisor
PR_REVIEW_ADVISOR_WORKFLOW_NAME: "PR Review / Advisor"
PR_REVIEW_ADVISOR_WORKFLOW_PATH: .github/workflows/pr-review-advisor.yaml
PR_REVIEW_ADVISOR_EVENT_NAME: ${{ github.event_name }}
PR_REVIEW_ADVISOR_RUN_ID: ${{ github.run_id }}
PR_REVIEW_ADVISOR_RUN_ATTEMPT: ${{ github.run_attempt }}
PR_NUMBER: ${{ github.event.pull_request.number }}
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
TRUSTED_WORKFLOW_SHA: ${{ github.workflow_sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
ADVISOR_DIR: ${{ github.workspace }}/advisor
PUBLISH_ARTIFACT_DIR: ${{ github.workspace }}/publish-artifacts/pr-review-advisor
SECONDARY_PUBLISH_ARTIFACT_DIR: ${{ github.workspace }}/publish-artifacts/pr-review-advisor-nemotron-ultra
PR_REVIEW_ADVISOR_MAX_RESULT_BYTES: "2097152"
PR_REVIEW_ADVISOR_MAX_SUMMARY_BYTES: "1048576"
steps:
- name: Checkout trusted comment publisher (workflow revision)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: NVIDIA/NemoClaw
ref: ${{ github.workflow_sha }}
path: advisor
persist-credentials: false
lfs: false
submodules: false
- name: Setup Node for trusted publisher
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
- name: Install trusted publisher dependencies
working-directory: advisor
run: npm ci --ignore-scripts --no-audit --no-fund
# With no run-id or repository override, download-artifact can only read
# the named artifact produced by this same workflow run.
- name: Download primary advisor artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pr-review-advisor
path: publish-artifacts/pr-review-advisor
# A missing or invalid second opinion must not suppress a completed
# primary review. A present artifact must pass the same validation before
# the publisher includes its sanitized status.
- name: Download secondary advisor artifact
id: download-secondary-advisor-artifact
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pr-review-advisor-nemotron-ultra
path: publish-artifacts/pr-review-advisor-nemotron-ultra
- name: Validate advisor artifacts
id: validate-advisor-artifacts
env:
GH_TOKEN: ${{ github.token }}
SECONDARY_ARTIFACT_OUTCOME: ${{ steps.download-secondary-advisor-artifact.outcome }}
run: |
node --experimental-strip-types \
"$ADVISOR_DIR/tools/pr-review-advisor/validate-artifacts.mts"
- name: Post PR review advisor comment
env:
GH_TOKEN: ${{ github.token }}
SECONDARY_ARTIFACT_VALIDATED: ${{ steps.validate-advisor-artifacts.outputs.secondary_artifact_validated }}
run: |
SECONDARY_ARGS=()
if [ "$SECONDARY_ARTIFACT_VALIDATED" = "true" ]; then
SECONDARY_ARGS=(
--second-opinion-analysis-result "$SECONDARY_PUBLISH_ARTIFACT_DIR/pr-review-advisor-result.json"
--second-opinion-result "$SECONDARY_PUBLISH_ARTIFACT_DIR/pr-review-advisor-final-result.json"
)
fi
node --experimental-strip-types "$ADVISOR_DIR/tools/pr-review-advisor/comment.mts" \
--repo "$GITHUB_REPOSITORY" \
--pr "$PR_NUMBER" \
--summary "$PUBLISH_ARTIFACT_DIR/pr-review-advisor-summary.md" \
--analysis-result "$PUBLISH_ARTIFACT_DIR/pr-review-advisor-result.json" \
--result "$PUBLISH_ARTIFACT_DIR/pr-review-advisor-final-result.json" \
--marker "$PR_REVIEW_ADVISOR_COMMENT_MARKER" \
--title "$PR_REVIEW_ADVISOR_COMMENT_TITLE" \
--label "$PR_REVIEW_ADVISOR_COMMENT_LABEL" \
"${SECONDARY_ARGS[@]}"