diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 319b90d38a..485cbd1c65 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -178,6 +178,12 @@ jobs: ubuntu-repo-cloud-langchain-deepagents-code) matrix='[{"id":"ubuntu-repo-cloud-langchain-deepagents-code","runner":"ubuntu-latest","label":"ubuntu-repo-cloud-langchain-deepagents-code"}]' ;; + ubuntu-repo-docker-post-reboot-recovery) + matrix='[{"id":"ubuntu-repo-docker-post-reboot-recovery","runner":"ubuntu-latest","label":"ubuntu-repo-docker-post-reboot-recovery"}]' + ;; + ubuntu-repo-cloud-langchain-deepagents-code,ubuntu-repo-docker-post-reboot-recovery) + matrix='[{"id":"ubuntu-repo-cloud-langchain-deepagents-code","runner":"ubuntu-latest","label":"ubuntu-repo-cloud-langchain-deepagents-code"},{"id":"ubuntu-repo-docker-post-reboot-recovery","runner":"ubuntu-latest","label":"ubuntu-repo-docker-post-reboot-recovery"}]' + ;; *) echo "::error::PR E2E target is not approved by the trusted controller" >&2 exit 1 @@ -346,7 +352,10 @@ jobs: [[ "$CORRELATION_ID" =~ ^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$ ]] || { echo "::error::correlation_id must be a lowercase UUIDv4"; exit 1; } [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::pr_number must be a positive integer"; exit 1; } [[ -n "$JOBS" || -n "$TARGETS" ]] || { echo "::error::PR E2E runs require controller-selected jobs or targets"; exit 1; } - [[ -z "$TARGETS" || "$TARGETS" == "ubuntu-repo-cloud-langchain-deepagents-code" ]] || { echo "::error::PR E2E target is not approved by the trusted controller"; exit 1; } + case "$TARGETS" in + ""|ubuntu-repo-cloud-langchain-deepagents-code|ubuntu-repo-docker-post-reboot-recovery|ubuntu-repo-cloud-langchain-deepagents-code,ubuntu-repo-docker-post-reboot-recovery) ;; + *) echo "::error::PR E2E target is not approved by the trusted controller"; exit 1 ;; + esac pull_json="$(curl --fail --silent --show-error --proto '=https' \ --header "Authorization: Bearer ${GITHUB_TOKEN}" \ diff --git a/test/e2e/README.md b/test/e2e/README.md index e78fc1f266..d9a48ea223 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -601,6 +601,10 @@ both selector types in one correlated workflow run. Fork revisions whose plans select credential-bearing jobs or targets instead require explicit maintainer approval through the `approve-e2e` workflow operation. Plans with no selected jobs or targets can complete without an E2E run. +Changes to `src/lib/actions/sandbox/status-snapshot.ts` select the exact +`ubuntu-repo-docker-post-reboot-recovery` typed target. This keeps status +delivery-recovery changes bound to the reboot simulation that independently +probes the restored gateway and host forwarding. An internal revision whose matched control-plane files are drawn only from the trusted controller and observer boundaries—`.github/workflows/pr-e2e-gate.yaml`, `tools/e2e/pr-e2e-gate.mts`, and `tools/e2e/pr-e2e-required.mts`—automatically diff --git a/test/e2e/support/trusted-target-routing-workflow-boundary.test.ts b/test/e2e/support/trusted-target-routing-workflow-boundary.test.ts new file mode 100644 index 0000000000..f44af1890c --- /dev/null +++ b/test/e2e/support/trusted-target-routing-workflow-boundary.test.ts @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { validateE2eWorkflow } from "../../../tools/e2e/workflow-boundary.mts"; +import { readWorkflow } from "../../helpers/e2e-workflow-contract"; +import { requireFixture } from "./require-fixture"; + +type ControllerWorkflow = { + jobs: Record }>; +}; + +const EXPECTED_ERROR = "trusted controller matrix must pin typed target runner to ubuntu-latest"; +const TRUSTED_MAPPING = + '{"id":"ubuntu-repo-docker-post-reboot-recovery","runner":"ubuntu-latest","label":"ubuntu-repo-docker-post-reboot-recovery"}'; + +function fixture() { + const workflow = readWorkflow() as ControllerWorkflow; + const controllerMatrix = workflow.jobs["generate-matrix"]!.steps.find( + (step) => step.id === "controller_matrix", + )!; + return { controllerMatrix, workflow }; +} + +describe("trusted E2E target routing boundary (#7824)", () => { + it("rejects trusted target mappings outside their exact case branch", () => { + const { controllerMatrix, workflow } = fixture(); + expect(validateE2eWorkflow(workflow)).not.toContain(EXPECTED_ERROR); + requireFixture( + controllerMatrix.run?.includes(TRUSTED_MAPPING), + "trusted target fixture mapping is missing", + ); + + controllerMatrix.run = controllerMatrix + .run!.replace(TRUSTED_MAPPING, TRUSTED_MAPPING.replace("ubuntu-latest", "self-hosted")) + .concat(`\n# ${TRUSTED_MAPPING}\n`); + + expect(validateE2eWorkflow(workflow)).toContain(EXPECTED_ERROR); + }); + + it("rejects a dead approved case block before unsafe target routing", () => { + const { controllerMatrix, workflow } = fixture(); + const run = controllerMatrix.run!; + const caseStart = run.indexOf('case "${TARGETS}" in'); + const caseEnd = run.indexOf("\nesac", caseStart) + "\nesac".length; + requireFixture(caseStart >= 0, "trusted target fixture case is missing"); + requireFixture(caseEnd > caseStart, "trusted target fixture case terminator is missing"); + const deadApprovedCase = run.slice(caseStart, caseEnd); + const unsafeRouting = run.replace( + TRUSTED_MAPPING, + TRUSTED_MAPPING.replace("ubuntu-latest", "self-hosted"), + ); + controllerMatrix.run = `${deadApprovedCase}\n${unsafeRouting}`; + + expect(validateE2eWorkflow(workflow)).toContain(EXPECTED_ERROR); + }); + + it("rejects an executable wildcard before approved target routing", () => { + const { controllerMatrix, workflow } = fixture(); + const caseStart = 'case "${TARGETS}" in'; + const unsafeWildcard = [ + "*)", + 'matrix=\'[{"id":"untrusted","runner":"self-hosted","label":"untrusted"}]\'', + ";;", + ].join("\n"); + requireFixture( + controllerMatrix.run?.includes(caseStart), + "trusted target fixture case is missing", + ); + + controllerMatrix.run = controllerMatrix.run!.replace( + caseStart, + `${caseStart}\n${unsafeWildcard}`, + ); + + expect(validateE2eWorkflow(workflow)).toContain(EXPECTED_ERROR); + }); + + it("rejects a matrix override after approved target routing", () => { + const { controllerMatrix, workflow } = fixture(); + expect(validateE2eWorkflow(workflow)).not.toContain(EXPECTED_ERROR); + const output = `printf 'matrix=%s\\n' "\${matrix}" >> "\${GITHUB_OUTPUT}"`; + requireFixture( + controllerMatrix.run?.includes(output), + "trusted target fixture output is missing", + ); + const unsafeOverride = + 'matrix=\'[{"id":"untrusted","runner":"self-hosted","label":"untrusted"}]\''; + controllerMatrix.run = controllerMatrix.run!.replace(output, `${unsafeOverride}\n${output}`); + + expect(validateE2eWorkflow(workflow)).toContain(EXPECTED_ERROR); + }); +}); diff --git a/test/pr-e2e-gate-typed-target.test.ts b/test/pr-e2e-gate-typed-target.test.ts index 895bb8205e..111e8a779e 100644 --- a/test/pr-e2e-gate-typed-target.test.ts +++ b/test/pr-e2e-gate-typed-target.test.ts @@ -17,6 +17,7 @@ const BASE_SHA = "b".repeat(40); const WORKFLOW_SHA = "d".repeat(40); const CORRELATION_ID = "12345678-1234-4123-8123-123456789abc"; const DCODE_TARGET = PR_E2E_TYPED_TARGET_IDS[0]; +const POST_REBOOT_TARGET = PR_E2E_TYPED_TARGET_IDS[1]; const DCODE_CHECK = "test/e2e/e2e-cloud-experimental/checks/07-deepagents-code-headless-inference.sh"; @@ -96,4 +97,18 @@ describe("PR E2E typed-target gate (#7031)", () => { }), ).rejects.toThrow(/Controller dispatch inputs are invalid/u); }); + + it("accepts the trusted post-reboot target in an exact-head plan (#7824)", () => { + const plan = buildRiskPlan({ + headSha: HEAD_SHA, + changedFiles: ["src/lib/actions/sandbox/status-snapshot.ts"], + }); + + expect(plan.requiredTargets).toEqual([ + expect.objectContaining({ + id: POST_REBOOT_TARGET, + matchedFiles: ["src/lib/actions/sandbox/status-snapshot.ts"], + }), + ]); + }); }); diff --git a/test/pr-risk-plan.test.ts b/test/pr-risk-plan.test.ts index 636ca5db37..377b01c2e9 100644 --- a/test/pr-risk-plan.test.ts +++ b/test/pr-risk-plan.test.ts @@ -28,7 +28,7 @@ describe("deterministic PR risk plan", () => { const second = plan("src/lib/onboard.ts", "src/lib/state/registry.ts"); expect(first).toEqual(second); - expect(first.version).toBe(7); + expect(first.version).toBe(8); expect(first.headSha).toBe(HEAD_SHA); expect(first.planHash).toMatch(/^[a-f0-9]{64}$/u); expect(first.changedFiles).toEqual(["src/lib/onboard.ts", "src/lib/state/registry.ts"]); @@ -150,8 +150,11 @@ describe("deterministic PR risk plan", () => { "test/e2e/e2e-cloud-experimental/checks/08-deepagents-code-secret-boundary.sh", ); - expect(PR_E2E_TYPED_TARGET_IDS).toEqual(["ubuntu-repo-cloud-langchain-deepagents-code"]); - expect(riskPlanRequiredTargetIds(result)).toEqual(PR_E2E_TYPED_TARGET_IDS); + expect(PR_E2E_TYPED_TARGET_IDS).toEqual([ + "ubuntu-repo-cloud-langchain-deepagents-code", + "ubuntu-repo-docker-post-reboot-recovery", + ]); + expect(riskPlanRequiredTargetIds(result)).toEqual([PR_E2E_TYPED_TARGET_IDS[0]]); expect(result.requiredTargets).toEqual([ expect.objectContaining({ id: PR_E2E_TYPED_TARGET_IDS[0], @@ -162,7 +165,7 @@ describe("deterministic PR risk plan", () => { expect(result.families).toContainEqual( expect.objectContaining({ id: "focused-e2e", - requiredTargets: [...PR_E2E_TYPED_TARGET_IDS], + requiredTargets: [PR_E2E_TYPED_TARGET_IDS[0]], }), ); expect(riskPlanRequiredTargetIds(adjacentCheck)).toEqual([]); @@ -185,7 +188,7 @@ describe("deterministic PR risk plan", () => { "test/langchain-deepagents-code-managed-model-params.test.ts", ); - expect(riskPlanRequiredTargetIds(result)).toEqual(PR_E2E_TYPED_TARGET_IDS); + expect(riskPlanRequiredTargetIds(result)).toEqual([PR_E2E_TYPED_TARGET_IDS[0]]); expect(result.requiredTargets).toEqual([ expect.objectContaining({ id: PR_E2E_TYPED_TARGET_IDS[0], @@ -197,6 +200,24 @@ describe("deterministic PR risk plan", () => { expect(riskPlanRequiredTargetIds(docsAndTestsOnly)).toEqual([]); }); + it("selects post-reboot recovery for status delivery recovery changes (#7824)", () => { + const changedFile = "src/lib/actions/sandbox/status-snapshot.ts"; + const result = plan(changedFile); + const adjacentStatusFile = plan("src/lib/actions/sandbox/status-text.ts"); + + expect(riskPlanRequiredTargetIds(result)).toEqual([PR_E2E_TYPED_TARGET_IDS[1]]); + expect(result.requiredTargets).toEqual([ + expect.objectContaining({ + id: PR_E2E_TYPED_TARGET_IDS[1], + families: ["focused-e2e"], + matchedFiles: [changedFile], + }), + ]); + expect(riskPlanRequiredTargetIds(adjacentStatusFile)).toEqual([]); + expect(result.planHash).not.toBe(adjacentStatusFile.planHash); + expect(requiresCredentialedE2eAuthorization(result)).toBe(false); + }); + it("does not infer security or inference risk from unrelated path substrings", () => { const result = plan("src/lib/actions/sandbox/mcp-bridge-provider.ts", "src/lib/secretary.ts"); diff --git a/tools/advisors/risk-plan.mts b/tools/advisors/risk-plan.mts index 6adbad8c72..71c00ec11d 100644 --- a/tools/advisors/risk-plan.mts +++ b/tools/advisors/risk-plan.mts @@ -3,14 +3,18 @@ import { createHash } from "node:crypto"; -export const RISK_PLAN_VERSION = 7 as const; +export const RISK_PLAN_VERSION = 8 as const; -export const PR_E2E_TYPED_TARGET_IDS = ["ubuntu-repo-cloud-langchain-deepagents-code"] as const; +export const PR_E2E_TYPED_TARGET_IDS = [ + "ubuntu-repo-cloud-langchain-deepagents-code", + "ubuntu-repo-docker-post-reboot-recovery", +] as const; const PR_E2E_TYPED_TARGET_ID_SET = new Set(PR_E2E_TYPED_TARGET_IDS); const DEEPAGENTS_HEADLESS_INFERENCE_CHECK = "test/e2e/e2e-cloud-experimental/checks/07-deepagents-code-headless-inference.sh"; const DEEPAGENTS_CODE_RUNTIME_ROOT = "agents/langchain-deepagents-code/"; +const POST_REBOOT_STATUS_RUNTIME = "src/lib/actions/sandbox/status-snapshot.ts"; export type RiskTier = 0 | 1 | 2 | 3; export type RiskFamilyId = @@ -122,21 +126,34 @@ export function isPrE2eTypedTargetId(value: string): boolean { export function focusedPrE2eTargetsForChangedFiles( changedFiles: readonly string[], ): TrustedFocusedE2eTarget[] { - const matchedFiles = stableUnique( + const deepAgentsMatchedFiles = stableUnique( changedFiles.filter( (file) => file === DEEPAGENTS_HEADLESS_INFERENCE_CHECK || (file.startsWith(DEEPAGENTS_CODE_RUNTIME_ROOT) && isRuntimeRelevant(file)), ), ); - return matchedFiles.length > 0 - ? [ - { - id: PR_E2E_TYPED_TARGET_IDS[0], - matchedFiles, - }, - ] + const postRebootMatchedFiles = changedFiles.includes(POST_REBOOT_STATUS_RUNTIME) + ? [POST_REBOOT_STATUS_RUNTIME] : []; + return [ + ...(deepAgentsMatchedFiles.length > 0 + ? [ + { + id: PR_E2E_TYPED_TARGET_IDS[0], + matchedFiles: deepAgentsMatchedFiles, + }, + ] + : []), + ...(postRebootMatchedFiles.length > 0 + ? [ + { + id: PR_E2E_TYPED_TARGET_IDS[1], + matchedFiles: postRebootMatchedFiles, + }, + ] + : []), + ]; } export const RISK_RULES: readonly RiskRule[] = [ diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index 5b2a34a5fb..adb12602b8 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -383,7 +383,10 @@ function validatePrGateDispatch(errors: string[], workflow: OperationsWorkflow): '"$CORRELATION_ID" =~ ^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$', '"$PR_NUMBER" =~ ^[1-9][0-9]*$', '[[ -n "$JOBS" || -n "$TARGETS" ]]', - '[[ -z "$TARGETS" || "$TARGETS" == "ubuntu-repo-cloud-langchain-deepagents-code" ]]', + 'case "$TARGETS" in', + "ubuntu-repo-cloud-langchain-deepagents-code", + "ubuntu-repo-docker-post-reboot-recovery", + "PR E2E target is not approved by the trusted controller", "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}", "'.state'", "'.head.repo.full_name // \"\"'", diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index b98c469b85..e2035913b5 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -4500,8 +4500,38 @@ export function validateE2eWorkflow(workflowValue: unknown): string[] { } requireRunContains(errors, controllerMatrix, 'case "${TARGETS}" in'); requireRunContains(errors, controllerMatrix, "matrix='[]'"); - requireRunContains(errors, controllerMatrix, "ubuntu-repo-cloud-langchain-deepagents-code"); - if (!stringValue(controllerMatrix?.run).includes('"runner":"ubuntu-latest"')) { + const controllerMatrixScript = stringValue(controllerMatrix?.run); + const deepAgentsTarget = "ubuntu-repo-cloud-langchain-deepagents-code"; + const postRebootTarget = "ubuntu-repo-docker-post-reboot-recovery"; + const deepAgentsMapping = `{"id":"${deepAgentsTarget}","runner":"ubuntu-latest","label":"${deepAgentsTarget}"}`; + const postRebootMapping = `{"id":"${postRebootTarget}","runner":"ubuntu-latest","label":"${postRebootTarget}"}`; + const trustedControllerMatrixScript = [ + "set -euo pipefail", + 'case "${TARGETS}" in', + '"")', + "matrix='[]'", + ";;", + `${deepAgentsTarget})`, + `matrix='[${deepAgentsMapping}]'`, + ";;", + `${postRebootTarget})`, + `matrix='[${postRebootMapping}]'`, + ";;", + `${deepAgentsTarget},${postRebootTarget})`, + `matrix='[${deepAgentsMapping},${postRebootMapping}]'`, + ";;", + "*)", + 'echo "::error::PR E2E target is not approved by the trusted controller" >&2', + "exit 1", + ";;", + "esac", + `printf 'matrix=%s\\n' "\${matrix}" >> "\${GITHUB_OUTPUT}"`, + ]; + const controllerMatrixLines = controllerMatrixScript + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + if (!isDeepStrictEqual(controllerMatrixLines, trustedControllerMatrixScript)) { errors.push("trusted controller matrix must pin typed target runner to ubuntu-latest"); } requireRunContains(