Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 199 additions & 16 deletions src/lib/onboard/managed-startup-image-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,29 @@ import { mapManagedStartupProfileToAgentEnvironment } from "./managed-startup/ag
import {
applyManagedStartupCommandEnvironmentPlan,
applyManagedStartupImageProfile,
applyManagedStartupRootRequest,
buildManagedStartupImageActionPlan,
MANAGED_STARTUP_COMPLETION_FILE,
MANAGED_STARTUP_MERGED_CA_FILE,
MANAGED_STARTUP_PROFILE_ENV,
MANAGED_STARTUP_RUNTIME_ENV_FILE,
type ManagedStartupImageActionPlanInput,
normalizeHermesManagedConfigDescriptor,
readStableRegularFile,
serializeManagedStartupCompletionMarker,
serializeManagedStartupRuntimeEnvironment,
verifyManagedStartupImageCompletion,
} from "./managed-startup/image-runtime";
import {
encodeManagedStartupProfile,
fingerprintManagedStartupProfile,
MANAGED_STARTUP_AGENTS,
type ManagedStartupAgent,
type ManagedStartupDashboard,
type ManagedStartupProfile,
validateManagedStartupProfile,
} from "./managed-startup/profile";
import { createManagedStartupRootApplyRequest } from "./managed-startup/root-apply";

function dashboard(agent: ManagedStartupAgent): ManagedStartupDashboard {
switch (agent) {
Expand Down Expand Up @@ -323,7 +329,10 @@ describe("managed startup image runtime", () => {
"/var/lib",
"/var/lib/nemoclaw",
]);
let runtimeFileWritten = false;
const files = new Map<string, Buffer>();
const descriptorTargets = new Map<number, string>();
const pendingFiles = new Map<string, Buffer>();
let nextDescriptor = 91;
const stat = (kind: "directory" | "file", mode: number) =>
({
gid: 0,
Expand All @@ -334,47 +343,104 @@ describe("managed startup image runtime", () => {
nlink: 1,
uid: 0,
}) as fs.Stats;
const bigFileStat = (bytes: Buffer) =>
({
ctimeNs: 1n,
dev: 1n,
gid: 0n,
ino: 2n,
isFile: () => true,
mode: 0o100444n,
mtimeNs: 1n,
nlink: 1n,
size: BigInt(bytes.length),
uid: 0n,
}) as fs.BigIntStats;
const missing = (): never => {
throw Object.assign(new Error("missing"), { code: "ENOENT" });
};
const allocateDescriptor = (resolved: string): number => {
const descriptor = nextDescriptor;
nextDescriptor += 1;
descriptorTargets.set(descriptor, resolved);
return descriptor;
};

vi.spyOn(process, "geteuid").mockReturnValue(0);
vi.spyOn(fs, "lstatSync").mockImplementation(((target: fs.PathLike) => {
const resolved = String(target);
return directories.has(resolved)
? stat("directory", 0o755)
: resolved === MANAGED_STARTUP_RUNTIME_ENV_FILE && runtimeFileWritten
? stat("file", 0o400)
: files.has(resolved)
? stat("file", 0o444)
: missing();
}) as typeof fs.lstatSync);
vi.spyOn(fs, "mkdirSync").mockImplementation(() => undefined);
vi.spyOn(fs, "chownSync").mockImplementation(() => undefined);
vi.spyOn(fs, "chmodSync").mockImplementation(() => undefined);
vi.spyOn(fs, "existsSync").mockReturnValue(false);
vi.spyOn(fs, "openSync").mockReturnValue(91);
vi.spyOn(fs, "openSync").mockImplementation(((target: fs.PathLike) => {
const resolved = String(target);
return (resolved === MANAGED_STARTUP_RUNTIME_ENV_FILE ||
resolved === MANAGED_STARTUP_COMPLETION_FILE) &&
!files.has(resolved)
? missing()
: allocateDescriptor(resolved);
}) as typeof fs.openSync);
vi.spyOn(fs, "fstatSync").mockImplementation(((descriptor: number) => {
const target = descriptorTargets.get(descriptor);
const bytes = target === undefined ? undefined : files.get(target);
return bytes === undefined ? missing() : bigFileStat(bytes);
}) as typeof fs.fstatSync);
vi.spyOn(fs, "readSync").mockImplementation(((
descriptor: number,
buffer: NodeJS.ArrayBufferView,
offset: number,
length: number,
position: number | null,
) => {
const target = descriptorTargets.get(descriptor);
const bytes = (target === undefined ? undefined : files.get(target)) ?? missing();
const start = position ?? 0;
const count = Math.min(length, Math.max(0, bytes.length - start));
bytes.copy(buffer as Buffer, offset, start, start + count);
return count;
}) as typeof fs.readSync);
vi.spyOn(fs, "fchownSync").mockImplementation(() => undefined);
vi.spyOn(fs, "writeFileSync").mockImplementation(((target: fs.PathOrFileDescriptor, value) => {
runtimeWrites.push(...(target === 91 ? [String(value)] : []));
const resolved =
(typeof target === "number" ? descriptorTargets.get(target) : undefined) ?? missing();
pendingFiles.set(
resolved,
Buffer.isBuffer(value) ? Buffer.from(value) : Buffer.from(String(value), "utf8"),
);
}) as typeof fs.writeFileSync);
vi.spyOn(fs, "fchmodSync").mockImplementation(() => undefined);
vi.spyOn(fs, "fsyncSync").mockImplementation(() => undefined);
vi.spyOn(fs, "closeSync").mockImplementation(() => undefined);
vi.spyOn(fs, "renameSync").mockImplementation((_source, target) => {
runtimeFileWritten ||= String(target) === MANAGED_STARTUP_RUNTIME_ENV_FILE;
vi.spyOn(fs, "renameSync").mockImplementation((source, target) => {
const pending = pendingFiles.get(String(source)) ?? missing();
files.set(String(target), pending);
pendingFiles.delete(String(source));
runtimeWrites.push(
...(String(target) === MANAGED_STARTUP_RUNTIME_ENV_FILE ? [pending.toString("utf8")] : []),
);
});
vi.spyOn(fs, "unlinkSync").mockImplementation(missing);
}

it("rejects invalid OpenClaw launch controls before filesystem or coordinator mutation", async () => {
const profile = managedStartupE2eProfile("openclaw");
const request = createManagedStartupRootApplyRequest({
agent: profile.agent,
encodedProfile: encodeManagedStartupProfile(profile),
});
const lstat = vi.spyOn(fs, "lstatSync");
vi.spyOn(process, "geteuid").mockReturnValue(0);

await expect(
applyManagedStartupImageProfile("openclaw", {
NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION: "1",
applyManagedStartupRootRequest(request, {
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS: "NaN",
[MANAGED_STARTUP_PROFILE_ENV]: encodeManagedStartupProfile(profile),
}),
).rejects.toThrow(/finite positive seconds/u);
expect(lstat).not.toHaveBeenCalled();
Expand Down Expand Up @@ -406,20 +472,69 @@ describe("managed startup image runtime", () => {
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS: "3",
[MANAGED_STARTUP_PROFILE_ENV]: encodedProfile,
});
const second = await applyManagedStartupImageProfile("openclaw", {
NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION: "1",
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS: "5",
[MANAGED_STARTUP_PROFILE_ENV]: encodedProfile,
});
const second = await applyManagedStartupRootRequest(
createManagedStartupRootApplyRequest({
agent: profile.agent,
encodedProfile,
}),
{
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS: "5",
},
);

expect(first).toMatchObject({ adapterApplied: false, fingerprint });
expect(second).toMatchObject({ adapterApplied: false, fingerprint });
expect(second).toMatchObject({
adapterApplied: false,
fingerprint,
transactionPending: false,
});
expect(runtimeWrites).toHaveLength(2);
expect(runtimeWrites[0]).toContain("export NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS='3'");
expect(runtimeWrites[1]).toContain("export NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS='5'");
expect(coordinatorMock.coordinateManagedStartupApplication).toHaveBeenCalledTimes(2);
});

function writeCompletionFixture(
profile: ManagedStartupProfile,
corporateCaMerged = false,
): {
readonly agent: ManagedStartupAgent;
readonly completionFile: string;
readonly fingerprint: string;
readonly runtimeEnvironmentFile: string;
} {
const mapped = mapManagedStartupProfileToAgentEnvironment(profile);
const runtimeEnvironment = serializeManagedStartupRuntimeEnvironment(
mapped.runtimeEnvironment,
corporateCaMerged,
mapped.configurationEnvironment,
);
const fingerprint = fingerprintManagedStartupProfile(profile);
const completionFile = path.join(temporaryDirectory(), "managed-startup-complete.json");
const runtimeEnvironmentFile = path.join(temporaryDirectory(), "managed-startup-runtime.env");
fs.writeFileSync(runtimeEnvironmentFile, runtimeEnvironment, { mode: 0o444 });
fs.chmodSync(runtimeEnvironmentFile, 0o444);
fs.writeFileSync(
completionFile,
serializeManagedStartupCompletionMarker({
schemaVersion: 1,
agent: profile.agent,
profileFingerprint: fingerprint,
runtimeEnvironmentSha256: createHash("sha256")
.update(runtimeEnvironment, "utf8")
.digest("hex"),
corporateCaMerged,
}),
{ mode: 0o444 },
);
fs.chmodSync(completionFile, 0o444);
return {
agent: profile.agent,
completionFile,
fingerprint,
runtimeEnvironmentFile,
};
}
it.each(
MANAGED_STARTUP_AGENTS,
)("maps the complete %s profile into the reviewed image command contract", (agent) => {
Expand Down Expand Up @@ -450,6 +565,74 @@ describe("managed startup image runtime", () => {
);
});

it.each(
MANAGED_STARTUP_AGENTS,
)("accepts the root completion marker and exact runtime handoff for %s", (agent) => {
const fixture = writeCompletionFixture(managedStartupE2eProfile(agent));
mockDescriptorOwnership(0n, 0n);
expect(
verifyManagedStartupImageCompletion(
agent,
fixture.fingerprint,
fixture.completionFile,
fixture.runtimeEnvironmentFile,
),
).toEqual({ agent, fingerprint: fixture.fingerprint });
});

it("rejects a changed profile against the root completion fingerprint", () => {
const initial = writeCompletionFixture(managedStartupE2eProfile("openclaw"));
const changedProfile = managedStartupE2eProfile("openclaw", true);
mockDescriptorOwnership(0n, 0n);
expect(() =>
verifyManagedStartupImageCompletion(
"openclaw",
fingerprintManagedStartupProfile(changedProfile),
initial.completionFile,
initial.runtimeEnvironmentFile,
),
).toThrow(/completion marker does not match the requested profile/u);
});

it("rejects runtime handoff drift after a matching completion", () => {
const fixture = writeCompletionFixture(managedStartupE2eProfile("hermes"));
mockDescriptorOwnership(0n, 0n);
fs.chmodSync(fixture.runtimeEnvironmentFile, 0o644);
fs.appendFileSync(fixture.runtimeEnvironmentFile, "export NEMOCLAW_MODEL='tampered/model'\n");
fs.chmodSync(fixture.runtimeEnvironmentFile, 0o444);

expect(() =>
verifyManagedStartupImageCompletion(
"hermes",
fixture.fingerprint,
fixture.completionFile,
fixture.runtimeEnvironmentFile,
),
).toThrow(/runtime environment digest mismatch/u);
});

it("accepts merged CA paths without putting the CA payload in the readable handoff", () => {
const fixture = writeCompletionFixture(
managedStartupE2eProfile("langchain-deepagents-code", false, true),
true,
);
mockDescriptorOwnership(0n, 0n);
expect(
verifyManagedStartupImageCompletion(
"langchain-deepagents-code",
fixture.fingerprint,
fixture.completionFile,
fixture.runtimeEnvironmentFile,
),
).toEqual({
agent: "langchain-deepagents-code",
fingerprint: fixture.fingerprint,
});
expect(fs.readFileSync(fixture.runtimeEnvironmentFile, "utf8")).not.toContain(
"NEMOCLAW_CORPORATE_CA_B64",
);
});
Comment on lines +614 to +634

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The CA-leak assertion cannot fail.

writeCompletionFixture builds the runtime environment string itself at Line 507. It passes only mapped.runtimeEnvironment and mapped.configurationEnvironment. The corporate CA base64 never enters that input, because the profile carries only corporateCa.bundleSha256. The assertion at Line 631 therefore checks the fixture, not the production write path at image-runtime.ts Line 1381.

Assert the property through the public boundary instead. Apply a profile that carries a corporate CA through applyManagedStartupRootRequest with corporateCaB64 set, then read the written runtime environment file. Alternatively, narrow the test title to the completion-marker claim it does verify.

As per path instructions: "Prefer observable outcomes through the public boundary over source-text, private-shape, or mock-call assertions" and "Flag ... conditionals that make a test pass without exercising its claim."

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 630-630: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(fixture.runtimeEnvironmentFile, "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/managed-startup-image-runtime.test.ts` around lines 614 -
634, Replace the ineffective CA-leak assertion in the test around
writeCompletionFixture with a public-boundary test: apply a managed startup root
request via applyManagedStartupRootRequest using a profile with corporateCaB64
set, then read the produced runtime environment file and assert it omits the CA
payload. Ensure the test exercises the production write path in image-runtime
rather than only inspecting fixture-generated content; otherwise narrow the test
to the completion-marker behavior it actually verifies.

Source: Path instructions


it("binds the real corporate-CA fixture into every agent profile by exact digest", () => {
expect(() => new X509Certificate(MANAGED_STARTUP_E2E_CORPORATE_CA_PEM)).not.toThrow();
const digest = createHash("sha256").update(MANAGED_STARTUP_E2E_CORPORATE_CA_PEM).digest("hex");
Expand Down
Loading
Loading