From 1e73032ba38ba5ac252ef499cdcb3b960bd49c7e Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 7 Aug 2026 10:12:57 -0400 Subject: [PATCH 1/3] fix(e2e): bind manual PR risk signals to plan --- .../skills/nemoclaw-maintainer-e2e/SKILL.md | 5 ++++- .github/workflows/e2e.yaml | 10 ++++++++++ test/e2e/docs/README.md | 6 ++++-- .../e2e-operations-workflow-boundary.test.ts | 20 +++++++++++++++++++ test/maintainer-e2e-skill.test.ts | 5 +++++ tools/e2e/cli-artifact-workflow-boundary.mts | 2 +- tools/e2e/operations-workflow-boundary.mts | 6 ++++++ 7 files changed, 50 insertions(+), 4 deletions(-) diff --git a/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md b/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md index a61a6f468b0..1625201b826 100644 --- a/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md +++ b/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md @@ -78,6 +78,8 @@ case "$E2E_JOBS" in esac REVIEW_REASON='Reviewed the PR head commit for credentialed E2E.' CORRELATION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')" +PLAN_IDENTITY="manual-pr-e2e:v1:${WORKFLOW_SHA}:${E2E_JOBS}::false" +PLAN_HASH="$(python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.argv[1].encode()).hexdigest())' "$PLAN_IDENTITY")" gh workflow run .github/workflows/e2e.yaml \ --repo NVIDIA/NemoClaw \ --ref main \ @@ -90,11 +92,12 @@ gh workflow run .github/workflows/e2e.yaml \ -f "checkout_repository=${HEAD_REPOSITORY}" \ -f "base_sha=${BASE_SHA}" \ -f "workflow_sha=${WORKFLOW_SHA}" \ + -f "plan_hash=${PLAN_HASH}" \ -f "review_reason=${REVIEW_REASON}" \ -f "correlation_id=${CORRELATION_ID}" ``` -The trusted pre-checkout step requires current `maintain` or `admin` permission and validates the open PR, repository, head SHA, base SHA, workflow SHA, review reason, and selected mode. +The trusted pre-checkout step requires current `maintain` or `admin` permission. It validates the open PR, repository, head SHA, base SHA, workflow SHA, plan hash, review reason, and selected mode. A second validation after checkout rejects a changed PR identity before preparation. Find and verify the correlated run with bounded GitHub reads: diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index fe2ce5e476b..eaa0e1da610 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -73,6 +73,11 @@ on: required: false default: "" type: string + plan_hash: + description: Optional trusted execution-plan hash for manual exact-revision E2E. + required: false + default: "" + type: string correlation_id: description: Run correlation ID (UUIDv4). @@ -91,6 +96,7 @@ concurrency: env: NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.checkout_sha }} + NEMOCLAW_E2E_PLAN_HASH: ${{ inputs.plan_hash }} NEMOCLAW_E2E_CORRELATION_ID: ${{ inputs.correlation_id }} NEMOCLAW_E2E_SHARD: default @@ -224,6 +230,7 @@ jobs: INCLUDE_LAUNCHABLE: ${{ inputs.include_staging_brev_launchable }} JOBS: ${{ inputs.jobs }} + PLAN_HASH: ${{ inputs.plan_hash }} PR_NUMBER: ${{ inputs.pr_number }} REVIEW_REASON: ${{ inputs.review_reason }} RUN_ATTEMPT: ${{ github.run_attempt }} @@ -275,6 +282,9 @@ jobs: (( ${#REVIEW_REASON} >= 10 && ${#REVIEW_REASON} <= 500 )) || { echo "::error::review_reason must contain 10 to 500 printable characters" >&2; exit 1; } [[ "$EXPECTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA" ]] || { echo "::error::workflow_sha must match the trusted main workflow SHA" >&2; exit 1; } + plan_identity="manual-pr-e2e:v1:${WORKFLOW_SHA}:${JOBS}:${TARGETS}:${INCLUDE_LAUNCHABLE}" + expected_plan_hash="$(printf '%s' "$plan_identity" | sha256sum | cut -d ' ' -f 1)" + [[ "$PLAN_HASH" =~ ^[a-f0-9]{64}$ && "$PLAN_HASH" == "$expected_plan_hash" ]] || { echo "::error::plan_hash must match the trusted manual PR E2E plan" >&2; exit 1; } require_maintainer "$ACTOR" diff --git a/test/e2e/docs/README.md b/test/e2e/docs/README.md index 737d52ecd01..2e70f2934fe 100644 --- a/test/e2e/docs/README.md +++ b/test/e2e/docs/README.md @@ -327,8 +327,10 @@ test/e2e/ The Slack and GitHub scorecard timing comparison remains scoped to the dedicated `cloud-onboard` artifact. Manual PR runs attach `test/e2e/risk-signal-reporter.ts` to live Vitest - invocations and suppress PR reporting and scorecards. The workflow boundary - requires every selected job shard to upload its evidence artifact. + invocations and suppress PR reporting and scorecards. The trusted workflow + validates a plan hash that binds each risk signal to the workflow revision and + selected mode. The workflow boundary requires every selected job shard to + upload its evidence artifact. - `.github/workflows/platform-vitest-main.yaml` runs the full Vitest suite in four independent shards on each of macOS and WSL, with `fail-fast` disabled. Each macOS shard installs the pinned OpenShell formula and has a 30-minute diff --git a/test/e2e/support/e2e-operations-workflow-boundary.test.ts b/test/e2e/support/e2e-operations-workflow-boundary.test.ts index f3c24203d67..2d57ff1c110 100644 --- a/test/e2e/support/e2e-operations-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-operations-workflow-boundary.test.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -222,6 +223,9 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; const headSha = "a".repeat(40); const baseSha = "b".repeat(40); const workflowSha = "c".repeat(40); + const planHash = createHash("sha256") + .update(`manual-pr-e2e:v1:${workflowSha}:${jobs}::false`) + .digest("hex"); const prefix = [ "curl() {", ' case "${@: -1}" in', @@ -247,6 +251,7 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; GITHUB_TOKEN: "token", INCLUDE_LAUNCHABLE: "false", JOBS: jobs, + PLAN_HASH: planHash, PR_NUMBER: "42", REVIEW_REASON: "Reviewed PR head revision", RUN_ATTEMPT: "1", @@ -263,6 +268,21 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; expect(result.stderr).toContain(expectedStderr); }); + it("rejects a manual PR plan hash that does not match the trusted selection", () => { + const workflow = readE2eOperationsWorkflow(); + const authentication = workflow.jobs["generate-matrix"].steps!.find( + (step) => step.name === "Authenticate manual PR dispatch", + )!; + authentication.run = authentication.run!.replace( + '"$PLAN_HASH" == "$expected_plan_hash"', + '"$PLAN_HASH" != "$expected_plan_hash"', + ); + + expect(validateE2eOperationsWorkflow(workflow)).toContain( + 'Manual PR authentication must retain "$PLAN_HASH" == "$expected_plan_hash"', + ); + }); + it("uses central maintainer authorization for protected managed-image qualification", () => { const workflow = readE2eOperationsWorkflow(); const guards = [ diff --git a/test/maintainer-e2e-skill.test.ts b/test/maintainer-e2e-skill.test.ts index 3335391bc4e..1ad1237a8af 100644 --- a/test/maintainer-e2e-skill.test.ts +++ b/test/maintainer-e2e-skill.test.ts @@ -223,4 +223,9 @@ describe("nemoclaw-maintainer-e2e workflow routing", () => { expect(skill).toContain("If the release candidate SHA changes"); expect(skill).toContain("nemoclaw-maintainer-cut-release-tag"); }); + + it("binds manual PR risk signals to the trusted execution plan", () => { + expect(skill).toContain('PLAN_IDENTITY="manual-pr-e2e:v1:${WORKFLOW_SHA}:${E2E_JOBS}::false"'); + expect(skill).toContain("plan_hash=${PLAN_HASH}"); + }); }); diff --git a/tools/e2e/cli-artifact-workflow-boundary.mts b/tools/e2e/cli-artifact-workflow-boundary.mts index 00bcb440235..a7efc2d79ca 100644 --- a/tools/e2e/cli-artifact-workflow-boundary.mts +++ b/tools/e2e/cli-artifact-workflow-boundary.mts @@ -40,7 +40,7 @@ const CLI_ARTIFACT_PROVENANCE_STEP = "Record CLI artifact provenance"; const CANDIDATE_CHECKOUT_STEP_CONTENT_SHA256 = "3578a053cede863f7aa4814d8399b4ca21ea0b77cee712e6d549c684818f11dd"; const CLI_ARTIFACT_WORKFLOW_CONTRACT_SHA256 = - "5d09301f3d37c6d2f588379fa8c94ef33d00efb8e25dc475bcfba24377fd8365"; + "fb5c7fe4c0ce48b712b98654cb91a832147b5828d301e01ad7a0e1899b200f5e"; const CLI_ARTIFACT_CONSUMER_JOB_NAMES = [ "agent-turn-latency", "bedrock-runtime-compatible-anthropic", diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index 65a02a806aa..2e03e3489d6 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -214,6 +214,7 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow "review_reason", "base_sha", "workflow_sha", + "plan_hash", "correlation_id", ]) { const input = inputs[name]; @@ -224,6 +225,7 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow const expectedEnvironment = { NEMOCLAW_E2E_CORRELATION_ID: "${{ inputs.correlation_id }}", NEMOCLAW_E2E_EXPECTED_SHA: "${{ inputs.checkout_sha }}", + NEMOCLAW_E2E_PLAN_HASH: "${{ inputs.plan_hash }}", NEMOCLAW_E2E_SHARD: "default", }; for (const [name, value] of Object.entries(expectedEnvironment)) { @@ -278,6 +280,7 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow GITHUB_TOKEN: "${{ github.token }}", INCLUDE_LAUNCHABLE: "${{ inputs.include_staging_brev_launchable }}", JOBS: "${{ inputs.jobs }}", + PLAN_HASH: "${{ inputs.plan_hash }}", PR_NUMBER: "${{ inputs.pr_number }}", REVIEW_REASON: "${{ inputs.review_reason }}", RUN_ATTEMPT: "${{ github.run_attempt }}", @@ -304,6 +307,9 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow "${#REVIEW_REASON} >= 10", "${#REVIEW_REASON} <= 500", '"$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA"', + 'plan_identity="manual-pr-e2e:v1:${WORKFLOW_SHA}:${JOBS}:${TARGETS}:${INCLUDE_LAUNCHABLE}"', + '"$PLAN_HASH" =~ ^[a-f0-9]{64}$', + '"$PLAN_HASH" == "$expected_plan_hash"', "Manual PR E2E requires a repository maintainer or administrator", "Manual PR E2E accepts only empty selectors or managed-image-protected-runtime", "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}", From d7a246fe1724b9b16f263959a7e78ddcadb5bb8c Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 7 Aug 2026 12:41:46 -0400 Subject: [PATCH 2/3] fix(e2e): finish plan-free risk signal cleanup Signed-off-by: Julie Yaunches --- .../skills/nemoclaw-maintainer-e2e/SKILL.md | 6 +- .github/workflows/e2e.yaml | 13 ---- test/e2e/docs/README.md | 11 +-- test/e2e/risk-signal-reporter.ts | 1 - .../e2e-operations-workflow-boundary.test.ts | 74 ++----------------- ...p-bridge-runtime-compatibility-cli.test.ts | 3 - .../mcp-bridge-runtime-compatibility.test.ts | 5 +- .../retired-selector-compatibility.test.ts | 3 - test/maintainer-e2e-skill.test.ts | 10 --- tools/e2e/cli-artifact-workflow-boundary.mts | 2 +- tools/e2e/operations-workflow-boundary.mts | 10 --- tools/e2e/risk-signal.ts | 8 -- 12 files changed, 15 insertions(+), 131 deletions(-) diff --git a/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md b/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md index 476979e314c..59e43ff6219 100644 --- a/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md +++ b/.agents/skills/nemoclaw-maintainer-e2e/SKILL.md @@ -84,8 +84,6 @@ CORRELATION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')" INFERENCE_MODE=mock ALLOW_JETSON_RUNNER_QUEUE=false ALLOW_DGX_SPARK_RUNNER_QUEUE=false -PLAN_IDENTITY="manual-pr-e2e:v2:${WORKFLOW_SHA}:${E2E_JOBS}::false:${INFERENCE_MODE}:${ALLOW_JETSON_RUNNER_QUEUE}:${ALLOW_DGX_SPARK_RUNNER_QUEUE}" -PLAN_HASH="$(python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.argv[1].encode()).hexdigest())' "$PLAN_IDENTITY")" gh workflow run .github/workflows/e2e.yaml \ --repo NVIDIA/NemoClaw \ --ref main \ @@ -100,12 +98,12 @@ gh workflow run .github/workflows/e2e.yaml \ -f "checkout_repository=${HEAD_REPOSITORY}" \ -f "base_sha=${BASE_SHA}" \ -f "workflow_sha=${WORKFLOW_SHA}" \ - -f "plan_hash=${PLAN_HASH}" \ -f "review_reason=${REVIEW_REASON}" \ -f "correlation_id=${CORRELATION_ID}" ``` -The trusted pre-checkout step requires current `maintain` or `admin` permission. It validates the open PR, repository, head SHA, base SHA, workflow SHA, plan hash, review reason, inference mode, and hardware selections. +The trusted pre-checkout step requires current `maintain` or `admin` permission. +It validates the actor, open PR, repository, head SHA, base SHA, workflow SHA, review reason, and allowed jobs, targets, and Launchable combination. A second validation after checkout rejects a changed PR identity before preparation. Find and verify the correlated run with bounded GitHub reads: diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index a24d04b7054..09622dca5b1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -78,11 +78,6 @@ on: required: false default: "" type: string - plan_hash: - description: Optional trusted execution-plan hash for manual exact-revision E2E. - required: false - default: "" - type: string correlation_id: description: Run correlation ID (UUIDv4). @@ -101,7 +96,6 @@ concurrency: env: NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.checkout_sha }} - NEMOCLAW_E2E_PLAN_HASH: ${{ inputs.plan_hash }} NEMOCLAW_E2E_CORRELATION_ID: ${{ inputs.correlation_id }} NEMOCLAW_E2E_SHARD: default @@ -227,8 +221,6 @@ jobs: if: ${{ inputs.checkout_sha != '' }} env: ACTOR: ${{ github.actor }} - ALLOW_DGX_SPARK_RUNNER_QUEUE: ${{ inputs.allow_dgx_spark_runner_queue && 'true' || 'false' }} - ALLOW_JETSON_RUNNER_QUEUE: ${{ inputs.allow_jetson_runner_queue && 'true' || 'false' }} BASE_SHA: ${{ inputs.base_sha }} CHECKOUT_REPOSITORY: ${{ inputs.checkout_repository }} CHECKOUT_SHA: ${{ inputs.checkout_sha }} @@ -236,9 +228,7 @@ jobs: EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha }} INCLUDE_LAUNCHABLE: ${{ inputs.include_staging_brev_launchable }} - INFERENCE_MODE: ${{ inputs.inference_mode || 'mock' }} JOBS: ${{ inputs.jobs }} - PLAN_HASH: ${{ inputs.plan_hash }} PR_NUMBER: ${{ inputs.pr_number }} REVIEW_REASON: ${{ inputs.review_reason }} RUN_ATTEMPT: ${{ github.run_attempt }} @@ -290,9 +280,6 @@ jobs: (( ${#REVIEW_REASON} >= 10 && ${#REVIEW_REASON} <= 500 )) || { echo "::error::review_reason must contain 10 to 500 printable characters" >&2; exit 1; } [[ "$EXPECTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA" ]] || { echo "::error::workflow_sha must match the trusted main workflow SHA" >&2; exit 1; } - plan_identity="manual-pr-e2e:v2:${WORKFLOW_SHA}:${JOBS}:${TARGETS}:${INCLUDE_LAUNCHABLE}:${INFERENCE_MODE}:${ALLOW_JETSON_RUNNER_QUEUE}:${ALLOW_DGX_SPARK_RUNNER_QUEUE}" - expected_plan_hash="$(printf '%s' "$plan_identity" | sha256sum | cut -d ' ' -f 1)" - [[ "$PLAN_HASH" =~ ^[a-f0-9]{64}$ && "$PLAN_HASH" == "$expected_plan_hash" ]] || { echo "::error::plan_hash must match the trusted manual PR E2E plan" >&2; exit 1; } require_maintainer "$ACTOR" diff --git a/test/e2e/docs/README.md b/test/e2e/docs/README.md index f60eddcf487..2a35a568f28 100644 --- a/test/e2e/docs/README.md +++ b/test/e2e/docs/README.md @@ -298,7 +298,8 @@ test/e2e/ A maintainer can also dispatch the trusted `main` workflow against the exact head of an open internal or fork PR. The manual path validates the actor, PR number, head repository, head SHA, base SHA, workflow SHA, review reason, and - selected mode before candidate checkout. For a PR revision run, leave `jobs` and + allowed jobs, targets, and Launchable combination before candidate checkout. + For a PR revision run, leave `jobs` and `targets` empty. The run selects every default-selected free-standing workflow E2E except `Exact staging Brev Launchable`. It also selects all shared credential-free tests and these controller-selected registry targets: @@ -331,10 +332,10 @@ test/e2e/ The Slack and GitHub scorecard timing comparison remains scoped to the dedicated `cloud-onboard` artifact. Manual PR runs attach `test/e2e/risk-signal-reporter.ts` to live Vitest - invocations and suppress PR reporting and scorecards. The trusted workflow - validates a plan hash that binds each risk signal to the workflow revision, - inference mode, and optional hardware selections. The workflow boundary requires - every selected job shard to upload its evidence artifact. + invocations and suppress PR reporting and scorecards. Each risk signal binds + its result counts to the expected and tested candidate SHA, correlation ID, + job ID, and shard ID. The workflow boundary requires every selected job shard + to upload its evidence artifact. - `.github/workflows/platform-vitest-main.yaml` runs the full Vitest suite in four independent shards on each of macOS and WSL, with `fail-fast` disabled. Each macOS shard installs the pinned OpenShell formula and has a 30-minute diff --git a/test/e2e/risk-signal-reporter.ts b/test/e2e/risk-signal-reporter.ts index 7afc30f3f78..a723c7565b4 100644 --- a/test/e2e/risk-signal-reporter.ts +++ b/test/e2e/risk-signal-reporter.ts @@ -92,7 +92,6 @@ function mergeSignal(previous: E2eRiskSignal | null, current: E2eRiskSignal): E2 previous.shardId !== current.shardId || previous.expectedSha !== current.expectedSha || previous.testedSha !== current.testedSha || - previous.planHash !== current.planHash || previous.correlationId !== current.correlationId ) { throw new Error("risk signal metadata changed between Vitest invocations"); diff --git a/test/e2e/support/e2e-operations-workflow-boundary.test.ts b/test/e2e/support/e2e-operations-workflow-boundary.test.ts index f5583756b99..f3c24203d67 100644 --- a/test/e2e/support/e2e-operations-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-operations-workflow-boundary.test.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -211,40 +210,11 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; }); it.each([ - ["accepts empty-selector manual PR E2E for a maintainer", "maintain", "", undefined, 0, ""], - [ - "accepts protected managed-image qualification for a maintainer", - "maintain", - "managed-image-protected-runtime", - undefined, - 0, - "", - ], - [ - "rejects an unsupported manual PR job selector", - "maintain", - "gpu-e2e", - undefined, - 1, - "accepts only empty selectors or managed-image-protected-runtime", - ], - [ - "rejects manual PR E2E for a writer", - "write", - "", - undefined, - 1, - "requires a repository maintainer or administrator", - ], - [ - "rejects a syntactically valid plan hash that does not match the selected manual PR plan", - "maintain", - "", - "d".repeat(64), - 1, - "plan_hash must match the trusted manual PR E2E plan", - ], - ])("manual PR authorization: %s", (_behavior, role, jobs, planHashOverride, expectedStatus, expectedStderr) => { + ["maintain", "", 0, ""], + ["maintain", "managed-image-protected-runtime", 0, ""], + ["maintain", "gpu-e2e", 1, "accepts only empty selectors or managed-image-protected-runtime"], + ["write", "", 1, "requires a repository maintainer or administrator"], + ])("requires a maintainer role and bounded selector before manual PR E2E for %s with %s", (role, jobs, expectedStatus, expectedStderr) => { const workflow = readE2eOperationsWorkflow(); const authentication = workflow.jobs["generate-matrix"].steps!.find( (step) => step.name === "Authenticate manual PR dispatch", @@ -252,9 +222,6 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; const headSha = "a".repeat(40); const baseSha = "b".repeat(40); const workflowSha = "c".repeat(40); - const planHash = createHash("sha256") - .update(`manual-pr-e2e:v2:${workflowSha}:${jobs}::false:mock:false:false`) - .digest("hex"); const prefix = [ "curl() {", ' case "${@: -1}" in', @@ -272,8 +239,6 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; env: { ...process.env, ACTOR: "maintainer", - ALLOW_DGX_SPARK_RUNNER_QUEUE: "false", - ALLOW_JETSON_RUNNER_QUEUE: "false", BASE_SHA: baseSha, CHECKOUT_REPOSITORY: "contributor/NemoClaw", CHECKOUT_SHA: headSha, @@ -281,9 +246,7 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; GITHUB_REPOSITORY: "NVIDIA/NemoClaw", GITHUB_TOKEN: "token", INCLUDE_LAUNCHABLE: "false", - INFERENCE_MODE: "mock", JOBS: jobs, - PLAN_HASH: planHashOverride ?? planHash, PR_NUMBER: "42", REVIEW_REASON: "Reviewed PR head revision", RUN_ATTEMPT: "1", @@ -300,33 +263,6 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; expect(result.stderr).toContain(expectedStderr); }); - it("rejects a manual PR plan hash that does not match the trusted selection", () => { - const workflow = readE2eOperationsWorkflow(); - const authentication = workflow.jobs["generate-matrix"].steps!.find( - (step) => step.name === "Authenticate manual PR dispatch", - )!; - authentication.run = authentication.run!.replace( - '"$PLAN_HASH" == "$expected_plan_hash"', - '"$PLAN_HASH" != "$expected_plan_hash"', - ); - - expect(validateE2eOperationsWorkflow(workflow)).toContain( - 'Manual PR authentication must retain "$PLAN_HASH" == "$expected_plan_hash"', - ); - }); - - it("rejects a changed manual PR plan-hash derivation", () => { - const workflow = readE2eOperationsWorkflow(); - const authentication = workflow.jobs["generate-matrix"].steps!.find( - (step) => step.name === "Authenticate manual PR dispatch", - )!; - authentication.run = authentication.run!.replace("sha256sum", "sha1sum"); - - expect(validateE2eOperationsWorkflow(workflow)).toContain( - `Manual PR authentication must retain expected_plan_hash="$(printf '%s' "$plan_identity" | sha256sum | cut -d ' ' -f 1)"`, - ); - }); - it("uses central maintainer authorization for protected managed-image qualification", () => { const workflow = readE2eOperationsWorkflow(); const guards = [ diff --git a/test/e2e/support/mcp-bridge-runtime-compatibility-cli.test.ts b/test/e2e/support/mcp-bridge-runtime-compatibility-cli.test.ts index e9c87dd02b9..d5acbf9f81b 100644 --- a/test/e2e/support/mcp-bridge-runtime-compatibility-cli.test.ts +++ b/test/e2e/support/mcp-bridge-runtime-compatibility-cli.test.ts @@ -13,7 +13,6 @@ import { MCP_BRIDGE_RUNTIME_COMPATIBILITY_ARTIFACT } from "../../../tools/e2e/mc import { RISK_SIGNAL_FILE } from "../../../tools/e2e/risk-signal.ts"; const COMPATIBILITY_TOOL = path.resolve("tools/e2e/mcp-bridge-runtime-compatibility.mts"); -const PLAN_HASH = "b".repeat(64); const CORRELATION_ID = "123e4567-e89b-42d3-a456-426614174000"; function gatedEnvironment(): Record { @@ -26,7 +25,6 @@ function gatedEnvironment(): Record { GITHUB_WORKSPACE: process.cwd(), NEMOCLAW_E2E_CORRELATION_ID: CORRELATION_ID, NEMOCLAW_E2E_EXPECTED_SHA: expectedSha, - NEMOCLAW_E2E_PLAN_HASH: PLAN_HASH, NEMOCLAW_E2E_SHARD: "openclaw", }; } @@ -153,7 +151,6 @@ describe.skipIf(process.platform === "win32")("MCP bridge compatibility CLI", () shardId: "openclaw", expectedSha: gateEnv.NEMOCLAW_E2E_EXPECTED_SHA, testedSha: gateEnv.NEMOCLAW_E2E_EXPECTED_SHA, - planHash: PLAN_HASH, correlationId: CORRELATION_ID, passed: 1, failed: 0, diff --git a/test/e2e/support/mcp-bridge-runtime-compatibility.test.ts b/test/e2e/support/mcp-bridge-runtime-compatibility.test.ts index 251c5f8d9da..5ea67c8ca81 100644 --- a/test/e2e/support/mcp-bridge-runtime-compatibility.test.ts +++ b/test/e2e/support/mcp-bridge-runtime-compatibility.test.ts @@ -21,7 +21,6 @@ import { import { RISK_SIGNAL_FILE } from "../../../tools/e2e/risk-signal.ts"; const EXPECTED_SHA = "a".repeat(40); -const PLAN_HASH = "b".repeat(64); const CORRELATION_ID = "123e4567-e89b-42d3-a456-426614174000"; function riskSignalEnv(artifactDirectory: string): NodeJS.ProcessEnv { @@ -31,7 +30,6 @@ function riskSignalEnv(artifactDirectory: string): NodeJS.ProcessEnv { GITHUB_WORKSPACE: "/test/workspace", NEMOCLAW_E2E_CORRELATION_ID: CORRELATION_ID, NEMOCLAW_E2E_EXPECTED_SHA: EXPECTED_SHA, - NEMOCLAW_E2E_PLAN_HASH: PLAN_HASH, NEMOCLAW_E2E_SHARD: "hermes", }; } @@ -94,7 +92,7 @@ describe("MCP bridge dev runtime compatibility", () => { } }); - it("reports an exact unsupported-version rejection as passing compatibility evidence (#6426)", () => { + it("reports an exact unsupported-version rejection without a plan hash (#6426)", () => { const result = classifyMcpBridgeRuntimeCompatibility( assertRuntimeVersion("0.0.78-dev.6+ga7271169"), ); @@ -146,7 +144,6 @@ describe("MCP bridge dev runtime compatibility", () => { shardId: "hermes", expectedSha: EXPECTED_SHA, testedSha: EXPECTED_SHA, - planHash: PLAN_HASH, correlationId: CORRELATION_ID, passed: 1, failed: 0, diff --git a/test/e2e/support/retired-selector-compatibility.test.ts b/test/e2e/support/retired-selector-compatibility.test.ts index f779ba754c5..1cae72744ea 100644 --- a/test/e2e/support/retired-selector-compatibility.test.ts +++ b/test/e2e/support/retired-selector-compatibility.test.ts @@ -14,7 +14,6 @@ import { } from "../../../tools/e2e/retired-selector-compatibility.mts"; const EXPECTED_SHA = "a".repeat(40); -const PLAN_HASH = "b".repeat(64); const CORRELATION_ID = "123e4567-e89b-42d3-a456-426614174000"; const REPLACEMENT_FILES = [ "src/lib/actions/sandbox/rebuild-flow-helpers.test.ts", @@ -63,7 +62,6 @@ function environment( TARGETS: targets, NEMOCLAW_E2E_CORRELATION_ID: CORRELATION_ID, NEMOCLAW_E2E_EXPECTED_SHA: EXPECTED_SHA, - NEMOCLAW_E2E_PLAN_HASH: PLAN_HASH, NEMOCLAW_E2E_SHARD: "default", }; } @@ -127,7 +125,6 @@ describe("retired E2E selector compatibility", () => { shardId: "default", expectedSha: EXPECTED_SHA, testedSha: EXPECTED_SHA, - planHash: PLAN_HASH, correlationId: CORRELATION_ID, passed: 1, failed: 0, diff --git a/test/maintainer-e2e-skill.test.ts b/test/maintainer-e2e-skill.test.ts index 3d9f296605d..8648e23ade5 100644 --- a/test/maintainer-e2e-skill.test.ts +++ b/test/maintainer-e2e-skill.test.ts @@ -241,14 +241,4 @@ describe("nemoclaw-maintainer-e2e workflow routing", () => { expect(skill).toContain("If the release candidate SHA changes"); expect(skill).toContain("nemoclaw-maintainer-cut-release-tag"); }); - - it("binds manual PR risk signals to the trusted execution plan", () => { - expect(skill).toContain( - 'PLAN_IDENTITY="manual-pr-e2e:v2:${WORKFLOW_SHA}:${E2E_JOBS}::false:${INFERENCE_MODE}:${ALLOW_JETSON_RUNNER_QUEUE}:${ALLOW_DGX_SPARK_RUNNER_QUEUE}"', - ); - expect(skill).toContain("inference_mode=${INFERENCE_MODE}"); - expect(skill).toContain("allow_jetson_runner_queue=${ALLOW_JETSON_RUNNER_QUEUE}"); - expect(skill).toContain("allow_dgx_spark_runner_queue=${ALLOW_DGX_SPARK_RUNNER_QUEUE}"); - expect(skill).toContain("plan_hash=${PLAN_HASH}"); - }); }); diff --git a/tools/e2e/cli-artifact-workflow-boundary.mts b/tools/e2e/cli-artifact-workflow-boundary.mts index 5f9f92a7112..341f04051aa 100644 --- a/tools/e2e/cli-artifact-workflow-boundary.mts +++ b/tools/e2e/cli-artifact-workflow-boundary.mts @@ -40,7 +40,7 @@ const CLI_ARTIFACT_PROVENANCE_STEP = "Record CLI artifact provenance"; const CANDIDATE_CHECKOUT_STEP_CONTENT_SHA256 = "3578a053cede863f7aa4814d8399b4ca21ea0b77cee712e6d549c684818f11dd"; const CLI_ARTIFACT_WORKFLOW_CONTRACT_SHA256 = - "a5709e5136a710749400ebd7dccc9dcd7c875446911f0e71f184d62351c47629"; + "3576771c69b3d838cf75e25782548ee20c560be58c528224adf7f3d228facc32"; const CLI_ARTIFACT_CONSUMER_JOB_NAMES = [ "agent-turn-latency", "bedrock-runtime-compatible-anthropic", diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index dca6f3c932a..65a02a806aa 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -214,7 +214,6 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow "review_reason", "base_sha", "workflow_sha", - "plan_hash", "correlation_id", ]) { const input = inputs[name]; @@ -225,7 +224,6 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow const expectedEnvironment = { NEMOCLAW_E2E_CORRELATION_ID: "${{ inputs.correlation_id }}", NEMOCLAW_E2E_EXPECTED_SHA: "${{ inputs.checkout_sha }}", - NEMOCLAW_E2E_PLAN_HASH: "${{ inputs.plan_hash }}", NEMOCLAW_E2E_SHARD: "default", }; for (const [name, value] of Object.entries(expectedEnvironment)) { @@ -273,17 +271,13 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow } const authEnvironment = { ACTOR: "${{ github.actor }}", - ALLOW_DGX_SPARK_RUNNER_QUEUE: "${{ inputs.allow_dgx_spark_runner_queue && 'true' || 'false' }}", - ALLOW_JETSON_RUNNER_QUEUE: "${{ inputs.allow_jetson_runner_queue && 'true' || 'false' }}", BASE_SHA: "${{ inputs.base_sha }}", CHECKOUT_REPOSITORY: "${{ inputs.checkout_repository }}", CHECKOUT_SHA: "${{ inputs.checkout_sha }}", EXPECTED_WORKFLOW_SHA: "${{ inputs.workflow_sha }}", GITHUB_TOKEN: "${{ github.token }}", INCLUDE_LAUNCHABLE: "${{ inputs.include_staging_brev_launchable }}", - INFERENCE_MODE: "${{ inputs.inference_mode || 'mock' }}", JOBS: "${{ inputs.jobs }}", - PLAN_HASH: "${{ inputs.plan_hash }}", PR_NUMBER: "${{ inputs.pr_number }}", REVIEW_REASON: "${{ inputs.review_reason }}", RUN_ATTEMPT: "${{ github.run_attempt }}", @@ -310,10 +304,6 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow "${#REVIEW_REASON} >= 10", "${#REVIEW_REASON} <= 500", '"$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA"', - 'plan_identity="manual-pr-e2e:v2:${WORKFLOW_SHA}:${JOBS}:${TARGETS}:${INCLUDE_LAUNCHABLE}:${INFERENCE_MODE}:${ALLOW_JETSON_RUNNER_QUEUE}:${ALLOW_DGX_SPARK_RUNNER_QUEUE}"', - `expected_plan_hash="$(printf '%s' "$plan_identity" | sha256sum | cut -d ' ' -f 1)"`, - '"$PLAN_HASH" =~ ^[a-f0-9]{64}$', - '"$PLAN_HASH" == "$expected_plan_hash"', "Manual PR E2E requires a repository maintainer or administrator", "Manual PR E2E accepts only empty selectors or managed-image-protected-runtime", "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}", diff --git a/tools/e2e/risk-signal.ts b/tools/e2e/risk-signal.ts index 7e07b98c5e9..d20a19ddd95 100644 --- a/tools/e2e/risk-signal.ts +++ b/tools/e2e/risk-signal.ts @@ -11,7 +11,6 @@ export type E2eRiskSignal = { shardId: string; expectedSha: string; testedSha: string; - planHash: string; correlationId: string; passed: number; failed: number; @@ -27,7 +26,6 @@ export type RiskSignalEnvironment = { shardId: string; expectedSha: string; testedSha: string; - planHash: string; correlationId: string; }; @@ -37,7 +35,6 @@ export type RiskSignalCounts = Pick< >; const SHA_PATTERN = /^[a-f0-9]{40}$/u; -const HASH_PATTERN = /^[a-f0-9]{64}$/u; const CORRELATION_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u; const JOB_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u; @@ -63,7 +60,6 @@ export function configuredRiskSignalEnvironment( jobId: env.E2E_TARGET_ID ?? "", shardId: env.NEMOCLAW_E2E_SHARD ?? "", expectedSha: env.NEMOCLAW_E2E_EXPECTED_SHA, - planHash: env.NEMOCLAW_E2E_PLAN_HASH ?? "", correlationId: env.NEMOCLAW_E2E_CORRELATION_ID ?? "", }; if (!values.artifactDir) throw new Error("risk signal requires E2E_ARTIFACT_DIR"); @@ -74,9 +70,6 @@ export function configuredRiskSignalEnvironment( if (!SHA_PATTERN.test(values.expectedSha)) { throw new Error("risk signal requires a 40-character lowercase expected SHA"); } - if (!HASH_PATTERN.test(values.planHash)) { - throw new Error("risk signal requires a 64-character lowercase plan hash"); - } if (!CORRELATION_PATTERN.test(values.correlationId)) { throw new Error("risk signal requires a lowercase UUIDv4 correlation id"); } @@ -97,7 +90,6 @@ export function buildRiskSignal( shardId: environment.shardId, expectedSha: environment.expectedSha, testedSha: environment.testedSha, - planHash: environment.planHash, correlationId: environment.correlationId, ...counts, }; From 21ab99f1685479e7020f5cfacd41e054ba5c7558 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 7 Aug 2026 12:49:45 -0400 Subject: [PATCH 3/3] test(e2e): remove plan hash from reporter fixture Signed-off-by: Julie Yaunches --- test/e2e-risk-signal-reporter.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/e2e-risk-signal-reporter.test.ts b/test/e2e-risk-signal-reporter.test.ts index d591da3cef9..3f9b5a1813a 100644 --- a/test/e2e-risk-signal-reporter.test.ts +++ b/test/e2e-risk-signal-reporter.test.ts @@ -30,7 +30,6 @@ vi.mock("node:child_process", () => ({ })); const EXPECTED_SHA = "a".repeat(40); -const PLAN_HASH = "b".repeat(64); const CORRELATION_ID = "12345678-1234-4123-8123-123456789abc"; function moduleWithStates(states: Array<"passed" | "failed" | "skipped" | "pending">): TestModule { @@ -79,7 +78,6 @@ function environment(artifactDir: string): RiskSignalEnvironment { shardId: "default", expectedSha: EXPECTED_SHA, testedSha: EXPECTED_SHA, - planHash: PLAN_HASH, correlationId: CORRELATION_ID, }; } @@ -99,13 +97,12 @@ describe("E2E risk signal reporter", () => { ); }); - it("attests the checked-out HEAD instead of echoing only the expected SHA", () => { + it("attests the checked-out HEAD without plan metadata", () => { const env = { E2E_ARTIFACT_DIR: "/tmp/e2e-risk-signal-test", E2E_TARGET_ID: "onboard-resume", GITHUB_WORKSPACE: "/workspace", NEMOCLAW_E2E_EXPECTED_SHA: EXPECTED_SHA, - NEMOCLAW_E2E_PLAN_HASH: PLAN_HASH, NEMOCLAW_E2E_CORRELATION_ID: CORRELATION_ID, NEMOCLAW_E2E_SHARD: "default", }; @@ -161,7 +158,6 @@ describe("E2E risk signal reporter", () => { vi.stubEnv("E2E_ARTIFACT_DIR", dir); vi.stubEnv("E2E_TARGET_ID", "network-policy"); vi.stubEnv("NEMOCLAW_E2E_EXPECTED_SHA", EXPECTED_SHA); - vi.stubEnv("NEMOCLAW_E2E_PLAN_HASH", PLAN_HASH); vi.stubEnv("NEMOCLAW_E2E_CORRELATION_ID", CORRELATION_ID); vi.stubEnv("NEMOCLAW_E2E_SHARD", "live-probes");