diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index a07734cf40b..eb0b0a0d719 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -6,7 +6,7 @@ # Layers PR-specific code (plugin, config, startup script) on top of the # pre-built Hermes base image. Mirrors the OpenClaw Dockerfile structure. -ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:fa05221f5c7bcafea7e263c84e5d06f87e37d1ccb78dc28c113f1a4066aa544c +ARG BASE_IMAGE=ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:c925afe7c0742474166a4813a59f5961a93da4d130e9b35fb04088a384702142 # Group repository-owned files outside the final image so BuildKit can collapse # related files without invalidating earlier final-image work. diff --git a/test/e2e/README.md b/test/e2e/README.md index a851c0e7210..8dcb554d175 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -538,10 +538,15 @@ through cold onboarding and a real first turn. The repository-root `Dockerfile.base` remains in only the `platform-install` family. It selects `cloud-onboard` and does not trigger the cold `full-e2e` path. -The Deep Agents Code headless-inference check additionally selects the exact -`ubuntu-repo-cloud-langchain-deepagents-code` typed target. That target is -hashed into the risk plan beside the control-plane floor jobs, so the -controller dispatches both selector types in one correlated workflow run. +Non-documentation runtime changes under `agents/langchain-deepagents-code/` +and changes to the Deep Agents Code headless-inference check select the exact +`ubuntu-repo-cloud-langchain-deepagents-code` typed target. Documentation and +ordinary test changes alone do not select it. The target is hashed into the +risk plan beside any control-plane floor jobs, so the controller dispatches +both selector types in one correlated workflow run. Fork revisions whose plans +select credential-bearing jobs or targets instead require explicit protected +credentialed-E2E approval. Plans with no selected jobs or targets can complete +without an E2E run. 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/pr-e2e-gate-fork-approval.test.ts b/test/pr-e2e-gate-fork-approval.test.ts index 856d8efe828..a840c89cfa8 100644 --- a/test/pr-e2e-gate-fork-approval.test.ts +++ b/test/pr-e2e-gate-fork-approval.test.ts @@ -28,6 +28,7 @@ const CI_RUN_ID = 99; const CI_RUN_ATTEMPT = 3; const GATE_RUN_ID = 77; const APPROVAL_RUN_ID = 123; +const DCODE_PATCH = "agents/langchain-deepagents-code/patch-managed-deepagents-code.py"; afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); @@ -324,7 +325,7 @@ function successfulApprovedForkRoutes(approvals: unknown, requests: RecordedGitH } describe("PR E2E controller fork credentialed E2E approval safety", () => { - it("requires protected approval before a risky fork can run credentialed E2E", async () => { + it("requires the selected DCode target and protected approval before a risky fork can run credentialed E2E (#7463)", async () => { const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pr-e2e-gate-fork-")); const outputPath = path.join(workDir, "github-output"); fs.writeFileSync(outputPath, "", { mode: 0o600 }); @@ -350,7 +351,7 @@ describe("PR E2E controller fork credentialed E2E approval safety", () => { ), githubFetchRoute( ({ url }) => url.includes("/pulls/42/files?"), - () => githubResponse([{ filename: "src/lib/onboard.ts" }]), + () => githubResponse([{ filename: DCODE_PATCH }]), ), githubFetchRoute( ({ url, method }) => url.endsWith("/check-runs/17") && method === "PATCH", @@ -388,7 +389,7 @@ describe("PR E2E controller fork credentialed E2E approval safety", () => { expect(JSON.stringify(pending?.body)).toContain("head repository `contributor/NemoClaw`"); expect(JSON.stringify(pending?.body)).toContain(`head SHA \`${HEAD_SHA}\``); expect(JSON.stringify(pending?.body)).toContain(`base SHA \`${BASE_SHA}\``); - expect(JSON.stringify(pending?.body)).toContain("jobs:"); + expect(JSON.stringify(pending?.body)).toContain("targets:"); expect(JSON.stringify(pending?.body)).toContain("deterministic plan"); expect(fs.readFileSync(outputPath, "utf8")).toContain( [ diff --git a/test/pr-risk-plan.test.ts b/test/pr-risk-plan.test.ts index 1cc6e672114..97d55e18400 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(5); + expect(first.version).toBe(6); 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"]); @@ -115,6 +115,33 @@ describe("deterministic PR risk plan", () => { expect(requiresCredentialedE2eAuthorization(result)).toBe(true); }); + it("selects the Deep Agents Code target for its managed runtime changes (#7463)", () => { + const changedFiles = [ + "agents/langchain-deepagents-code/dependency-review.md", + "agents/langchain-deepagents-code/patch-managed-deepagents-code.py", + "test/langchain-deepagents-code-managed-model-params.test.ts", + "test/langchain-deepagents-code-nemotron-profile-plugin.test.ts", + ]; + const result = buildRiskPlan({ headSha: HEAD_SHA, changedFiles }); + const docsAndTestsOnly = plan( + "agents/langchain-deepagents-code/dependency-review.md", + "agents/langchain-deepagents-code/runtime-notes.mdx", + "agents/langchain-deepagents-code/resolver.test.ts", + "test/langchain-deepagents-code-managed-model-params.test.ts", + ); + + expect(riskPlanRequiredTargetIds(result)).toEqual(PR_E2E_TYPED_TARGET_IDS); + expect(result.requiredTargets).toEqual([ + expect.objectContaining({ + id: PR_E2E_TYPED_TARGET_IDS[0], + families: ["focused-e2e"], + matchedFiles: ["agents/langchain-deepagents-code/patch-managed-deepagents-code.py"], + }), + ]); + expect(result.tier).toBe(2); + expect(riskPlanRequiredTargetIds(docsAndTestsOnly)).toEqual([]); + }); + 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 169d325ebe4..6890e5ea8d0 100644 --- a/tools/advisors/risk-plan.mts +++ b/tools/advisors/risk-plan.mts @@ -3,13 +3,14 @@ import { createHash } from "node:crypto"; -export const RISK_PLAN_VERSION = 5 as const; +export const RISK_PLAN_VERSION = 6 as const; export const PR_E2E_TYPED_TARGET_IDS = ["ubuntu-repo-cloud-langchain-deepagents-code"] 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/"; export type RiskTier = 0 | 1 | 2 | 3; export type RiskFamilyId = @@ -108,10 +109,10 @@ const RISK_RELEVANT_TEST_FILES = new Set([ "test/e2e/risk-signal-reporter.ts", ]); const FOCUSED_E2E_SUMMARY = - "Changed workflow-wired E2E tests must execute through their trusted canonical jobs or typed targets."; + "Changed runtime surfaces and workflow-wired E2E tests must execute through their trusted canonical jobs or typed targets."; const FOCUSED_E2E_INVARIANTS = [ - "the changed test remains wired to a job or typed-target selector declared by the trusted workflow", - "the canonical execution path runs the changed test rather than treating it as advisory coverage", + "the selected job or typed target exercises the changed runtime surface or test", + "the canonical execution path runs the required coverage rather than treating it as advisory", ] as const; export function isPrE2eTypedTargetId(value: string): boolean { @@ -121,11 +122,18 @@ export function isPrE2eTypedTargetId(value: string): boolean { export function focusedPrE2eTargetsForChangedFiles( changedFiles: readonly string[], ): TrustedFocusedE2eTarget[] { - return changedFiles.includes(DEEPAGENTS_HEADLESS_INFERENCE_CHECK) + const matchedFiles = 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: [DEEPAGENTS_HEADLESS_INFERENCE_CHECK], + matchedFiles, }, ] : [];