Skip to content

Commit 183c595

Browse files
committed
fix(onboard): persist managed OpenClaw agent identity
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
1 parent 183a9c8 commit 183c595

2 files changed

Lines changed: 122 additions & 2 deletions

File tree

src/lib/onboard/sandbox-registration.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { createHash } from "node:crypto";
45
import { createRequire } from "node:module";
56
import { afterEach, describe, expect, it, vi } from "vitest";
67

8+
import { managedStartupE2eProfile } from "../../../scripts/checks/generate-managed-startup-profile-fixture.mts";
79
import {
810
serializedHostLocalInferenceReceipt,
911
serializedLlamaCppHostLocalInferenceReceipt,
1012
} from "../../../test/helpers/host-local-inference-receipt";
13+
import type { SandboxWorkloadReceipt } from "../state/registry/types";
1114
import { createSandboxHostLocalInferenceProvenance } from "../state/registry/host-local-inference";
15+
import {
16+
MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION,
17+
MANAGED_IMAGE_REPOSITORIES,
18+
MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION,
19+
type ManagedImageAgent,
20+
} from "./managed-image/contract";
21+
import { encodeManagedStartupProfile } from "./managed-startup/profile";
1222

1323
const requireDist = createRequire(import.meta.url);
1424
const onboardSession = requireDist("../state/onboard-session.js");
@@ -31,7 +41,103 @@ const runtimeFields = {
3141
openshellVersion: "0.1.2",
3242
};
3343

44+
function managedWorkloadReceipt(
45+
agent: ManagedImageAgent,
46+
): Extract<SandboxWorkloadReceipt, { readonly kind: "managed-image" }> {
47+
const encodedProfile = encodeManagedStartupProfile(managedStartupE2eProfile(agent));
48+
const digest = agent === "openclaw" ? "a" : "b";
49+
return {
50+
schemaVersion: 1,
51+
kind: "managed-image",
52+
reference: `${MANAGED_IMAGE_REPOSITORIES[agent]}@sha256:${digest.repeat(64)}`,
53+
platform: "linux/amd64",
54+
release: "v0.0.100",
55+
sourceRevision: "d".repeat(40),
56+
sourceCohort: "ghrun-9356-1",
57+
capabilityContractVersion: MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION,
58+
startupProfileContractVersion: MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION,
59+
encodedProfile,
60+
startupProfileSha256: createHash("sha256").update(encodedProfile, "utf8").digest("hex"),
61+
credentialProxyReplayRequired: false,
62+
shared: true,
63+
};
64+
}
65+
66+
function createdRegistryEntryInput(
67+
overrides: Partial<Parameters<typeof buildCreatedSandboxRegistryEntry>[0]> = {},
68+
): Parameters<typeof buildCreatedSandboxRegistryEntry>[0] {
69+
return {
70+
sandboxName: "demo",
71+
inferenceSelection: {
72+
model: "llama",
73+
provider: "openai-compatible",
74+
endpointUrl: null,
75+
credentialEnv: null,
76+
preferredInferenceApi: null,
77+
compatibleEndpointReasoning: null,
78+
compatibleEndpointReasoningEffort: null,
79+
nimContainer: null,
80+
},
81+
runtimeFields,
82+
agent: null,
83+
agentVersionKnown: true,
84+
imageTag: null,
85+
appliedPolicies: [],
86+
plannedMessagingState: undefined,
87+
hermesToolGateways: [],
88+
hermesDashboardState: { enabled: false, config: null },
89+
dashboardPort: 18789,
90+
gatewayName: "nemoclaw",
91+
gatewayPort: 8080,
92+
...overrides,
93+
};
94+
}
95+
3496
describe("buildCreatedSandboxRegistryEntry", () => {
97+
it("records explicit OpenClaw identity for a managed workload receipt (#9356)", () => {
98+
const workload = managedWorkloadReceipt("openclaw");
99+
const entry = buildCreatedSandboxRegistryEntry(
100+
createdRegistryEntryInput({ imageTag: workload.reference, workload }),
101+
);
102+
const authority = requireDist(
103+
"./workload/authority.ts",
104+
) as typeof import("./workload/authority");
105+
106+
expect(entry.agent).toBe("openclaw");
107+
expect(authority.readManagedWorkloadAuthority(entry)?.agent).toBe("openclaw");
108+
});
109+
110+
it("keeps the legacy OpenClaw registry identity for a custom image (#9356)", () => {
111+
const entry = buildCreatedSandboxRegistryEntry(
112+
createdRegistryEntryInput({
113+
agentVersionKnown: false,
114+
fromDockerfile: "/tmp/Dockerfile.custom",
115+
imageTag: "custom-openclaw:latest",
116+
workload: {
117+
schemaVersion: 1,
118+
kind: "legacy-dockerfile",
119+
reference: "custom-openclaw:latest",
120+
shared: false,
121+
},
122+
}),
123+
);
124+
125+
expect(entry.agent).toBeNull();
126+
});
127+
128+
it("rejects a managed receipt for a different agent before registry mutation (#9356)", () => {
129+
const workload = managedWorkloadReceipt("hermes");
130+
const registerSandbox = vi.fn();
131+
132+
expect(() =>
133+
registerCreatedSandbox({
134+
...createdRegistryEntryInput({ imageTag: workload.reference, workload }),
135+
registerSandbox,
136+
}),
137+
).toThrow(/agent identity does not match its managed workload receipt/u);
138+
expect(registerSandbox).not.toHaveBeenCalled();
139+
});
140+
35141
it("copies matching session profile provenance into the durable registry (#8246)", () => {
36142
const provenance = {
37143
schemaVersion: 1,

src/lib/onboard/sandbox-registration.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { DEFAULT_TOOL_DISCLOSURE, type ToolDisclosure } from "../tool-disclosure
2626
import type { DcodeAutoApprovalMode } from "./dcode-auto-approval";
2727
import { cloneSandboxHostMounts } from "../state/registry/host-mount";
2828
import { resolveOnboardHermesApiPort } from "./hermes-api-port";
29+
import { isManagedImageAgent, MANAGED_IMAGE_REPOSITORIES } from "./managed-image/contract";
2930
import {
3031
getHermesDashboardRegistryFields,
3132
type HermesDashboardOnboardState,
@@ -37,7 +38,7 @@ import {
3738
requireRuntimeProviderBundleForSandbox,
3839
requireRuntimeProviderMutationAuthority,
3940
} from "./runtime-provider/access";
40-
import { getSandboxAgentRegistryFields } from "./sandbox-agent";
41+
import { getRequestedSandboxAgentName, getSandboxAgentRegistryFields } from "./sandbox-agent";
4142

4243
export type CreatedSandboxRuntimeFields = Pick<
4344
SandboxEntry,
@@ -224,13 +225,26 @@ export function buildCreatedSandboxRegistryEntry(
224225
hostLocalInferenceReceipt,
225226
);
226227
}
228+
const agentFields = getSandboxAgentRegistryFields(input.agent, input.agentVersionKnown);
229+
if (workload?.kind === "managed-image") {
230+
const requestedAgent = getRequestedSandboxAgentName(input.agent);
231+
if (
232+
!isManagedImageAgent(requestedAgent) ||
233+
!workload.reference.startsWith(`${MANAGED_IMAGE_REPOSITORIES[requestedAgent]}@sha256:`)
234+
) {
235+
throw new RuntimeProviderSelectionError(
236+
"Sandbox agent identity does not match its managed workload receipt.",
237+
);
238+
}
239+
agentFields.agent = requestedAgent;
240+
}
227241

228242
return {
229243
name: input.sandboxName,
230244
servingProfileProvenance,
231245
...inferenceSelectionRegistryFields(input.inferenceSelection),
232246
...input.runtimeFields,
233-
...getSandboxAgentRegistryFields(input.agent, input.agentVersionKnown),
247+
...agentFields,
234248
imageTag: input.imageTag,
235249
workload,
236250
...(hostLocalInferenceReceipt !== undefined ? { hostLocalInferenceReceipt } : {}),

0 commit comments

Comments
 (0)