diff --git a/src/lib/onboard/sandbox-registration.test.ts b/src/lib/onboard/sandbox-registration.test.ts index 062c132093..b1a341447b 100644 --- a/src/lib/onboard/sandbox-registration.test.ts +++ b/src/lib/onboard/sandbox-registration.test.ts @@ -1,14 +1,24 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { createHash } from "node:crypto"; import { createRequire } from "node:module"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { managedStartupE2eProfile } from "../../../scripts/checks/generate-managed-startup-profile-fixture.mts"; import { serializedHostLocalInferenceReceipt, serializedLlamaCppHostLocalInferenceReceipt, } from "../../../test/helpers/host-local-inference-receipt"; +import type { SandboxWorkloadReceipt } from "../state/registry/types"; import { createSandboxHostLocalInferenceProvenance } from "../state/registry/host-local-inference"; +import { + MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, + MANAGED_IMAGE_REPOSITORIES, + MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, + type ManagedImageAgent, +} from "./managed-image/contract"; +import { encodeManagedStartupProfile } from "./managed-startup/profile"; const requireDist = createRequire(import.meta.url); const onboardSession = requireDist("../state/onboard-session.js"); @@ -31,7 +41,103 @@ const runtimeFields = { openshellVersion: "0.1.2", }; +function managedWorkloadReceipt( + agent: ManagedImageAgent, +): Extract { + const encodedProfile = encodeManagedStartupProfile(managedStartupE2eProfile(agent)); + const digest = agent === "openclaw" ? "a" : "b"; + return { + schemaVersion: 1, + kind: "managed-image", + reference: `${MANAGED_IMAGE_REPOSITORIES[agent]}@sha256:${digest.repeat(64)}`, + platform: "linux/amd64", + release: "v0.0.100", + sourceRevision: "d".repeat(40), + sourceCohort: "ghrun-9356-1", + capabilityContractVersion: MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, + startupProfileContractVersion: MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, + encodedProfile, + startupProfileSha256: createHash("sha256").update(encodedProfile, "utf8").digest("hex"), + credentialProxyReplayRequired: false, + shared: true, + }; +} + +function createdRegistryEntryInput( + overrides: Partial[0]> = {}, +): Parameters[0] { + return { + sandboxName: "demo", + inferenceSelection: { + model: "llama", + provider: "openai-compatible", + endpointUrl: null, + credentialEnv: null, + preferredInferenceApi: null, + compatibleEndpointReasoning: null, + compatibleEndpointReasoningEffort: null, + nimContainer: null, + }, + runtimeFields, + agent: null, + agentVersionKnown: true, + imageTag: null, + appliedPolicies: [], + plannedMessagingState: undefined, + hermesToolGateways: [], + hermesDashboardState: { enabled: false, config: null }, + dashboardPort: 18789, + gatewayName: "nemoclaw", + gatewayPort: 8080, + ...overrides, + }; +} + describe("buildCreatedSandboxRegistryEntry", () => { + it("records explicit OpenClaw identity for a managed workload receipt (#9356)", () => { + const workload = managedWorkloadReceipt("openclaw"); + const entry = buildCreatedSandboxRegistryEntry( + createdRegistryEntryInput({ imageTag: workload.reference, workload }), + ); + const authority = requireDist( + "./workload/authority.ts", + ) as typeof import("./workload/authority"); + + expect(entry.agent).toBe("openclaw"); + expect(authority.readManagedWorkloadAuthority(entry)?.agent).toBe("openclaw"); + }); + + it("keeps the legacy OpenClaw registry identity for a custom image (#9356)", () => { + const entry = buildCreatedSandboxRegistryEntry( + createdRegistryEntryInput({ + agentVersionKnown: false, + fromDockerfile: "/tmp/Dockerfile.custom", + imageTag: "custom-openclaw:latest", + workload: { + schemaVersion: 1, + kind: "legacy-dockerfile", + reference: "custom-openclaw:latest", + shared: false, + }, + }), + ); + + expect(entry.agent).toBeNull(); + }); + + it("rejects a managed receipt for a different agent before registry mutation (#9356)", () => { + const workload = managedWorkloadReceipt("hermes"); + const registerSandbox = vi.fn(); + + expect(() => + registerCreatedSandbox({ + ...createdRegistryEntryInput({ imageTag: workload.reference, workload }), + registerSandbox, + }), + ).toThrow(/agent identity does not match its managed workload receipt/u); + expect(registerSandbox).not.toHaveBeenCalled(); + }); + it("copies matching session profile provenance into the durable registry (#8246)", () => { const provenance = { schemaVersion: 1, diff --git a/src/lib/onboard/sandbox-registration.ts b/src/lib/onboard/sandbox-registration.ts index 9c955abb61..2d4128484d 100644 --- a/src/lib/onboard/sandbox-registration.ts +++ b/src/lib/onboard/sandbox-registration.ts @@ -26,6 +26,7 @@ import { DEFAULT_TOOL_DISCLOSURE, type ToolDisclosure } from "../tool-disclosure import type { DcodeAutoApprovalMode } from "./dcode-auto-approval"; import { cloneSandboxHostMounts } from "../state/registry/host-mount"; import { resolveOnboardHermesApiPort } from "./hermes-api-port"; +import { isManagedImageAgent, MANAGED_IMAGE_REPOSITORIES } from "./managed-image/contract"; import { getHermesDashboardRegistryFields, type HermesDashboardOnboardState, @@ -37,7 +38,7 @@ import { requireRuntimeProviderBundleForSandbox, requireRuntimeProviderMutationAuthority, } from "./runtime-provider/access"; -import { getSandboxAgentRegistryFields } from "./sandbox-agent"; +import { getRequestedSandboxAgentName, getSandboxAgentRegistryFields } from "./sandbox-agent"; export type CreatedSandboxRuntimeFields = Pick< SandboxEntry, @@ -224,13 +225,26 @@ export function buildCreatedSandboxRegistryEntry( hostLocalInferenceReceipt, ); } + const agentFields = getSandboxAgentRegistryFields(input.agent, input.agentVersionKnown); + if (workload?.kind === "managed-image") { + const requestedAgent = getRequestedSandboxAgentName(input.agent); + if ( + !isManagedImageAgent(requestedAgent) || + !workload.reference.startsWith(`${MANAGED_IMAGE_REPOSITORIES[requestedAgent]}@sha256:`) + ) { + throw new RuntimeProviderSelectionError( + "Sandbox agent identity does not match its managed workload receipt.", + ); + } + agentFields.agent = requestedAgent; + } return { name: input.sandboxName, servingProfileProvenance, ...inferenceSelectionRegistryFields(input.inferenceSelection), ...input.runtimeFields, - ...getSandboxAgentRegistryFields(input.agent, input.agentVersionKnown), + ...agentFields, imageTag: input.imageTag, workload, ...(hostLocalInferenceReceipt !== undefined ? { hostLocalInferenceReceipt } : {}), diff --git a/test/helpers/managed-image-buildless-e2e.ts b/test/helpers/managed-image-buildless-e2e.ts index 7549771d51..52d4483d4b 100644 --- a/test/helpers/managed-image-buildless-e2e.ts +++ b/test/helpers/managed-image-buildless-e2e.ts @@ -729,7 +729,7 @@ function assertManagedLaunch( result.payload.registerCalls, )}`, ).toBeDefined(); - expect(registration?.agent).toBe(agent === "openclaw" ? null : agent); + expect(registration?.agent).toBe(agent); if (agent === "langchain-deepagents-code") { expect(registration?.dashboardPort).toBe(0); }