Skip to content
Merged
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
38 changes: 35 additions & 3 deletions test/e2e-private-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";

import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
appendPrivateRegularFile,
readPrivateRegularFile,
writePrivateRegularFile,
} from "../tools/e2e/private-file.ts";
} from "../tools/e2e/private-file.mts";

describe("private E2E controller files", () => {
it("writes private regular files without following links or truncating hardlink targets", () => {
Expand Down Expand Up @@ -50,7 +50,7 @@ describe("private E2E controller files", () => {
const fifo = path.join(directory, "state.json");
try {
execFileSync("mkfifo", [fifo]);
const moduleUrl = pathToFileURL(path.resolve("tools/e2e/private-file.ts")).href;
const moduleUrl = pathToFileURL(path.resolve("tools/e2e/private-file.mts")).href;
const read = spawnSync(
process.execPath,
[
Expand All @@ -74,11 +74,43 @@ describe("private E2E controller files", () => {

expect(read.error).toBeUndefined();
expect(read.status).not.toBe(0);
expect(read.stderr).toContain(`Error: ${fifo} must be a private regular file`);
expect(write.error).toBeUndefined();
expect(write.status).not.toBe(0);
expect(write.stderr).toContain("Error: ENXIO:");
expect(write.stderr).toContain(`open '${fifo}'`);
for (const output of [read.stderr, write.stderr]) {
expect(output).not.toMatch(
/ERR_(?:MODULE_NOT_FOUND|UNKNOWN_FILE_EXTENSION|UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING)|Cannot find module|Unknown file extension|bad option: --experimental-strip-types|SyntaxError/u,
);
}
expect(fs.lstatSync(fifo).isFIFO()).toBe(true);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});

it("rejects a regular file that grows beyond maxBytes after fstat", () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-private-growth-"));
const file = path.join(directory, "state.json");
const originalFstatSync = fs.fstatSync;
let grewAfterFstat = false;
const fstatSync = vi.spyOn(fs, "fstatSync").mockImplementation((descriptor) => {
const stat = originalFstatSync(descriptor);
fs.appendFileSync(file, "x");
grewAfterFstat = true;
return stat;
});

try {
fs.writeFileSync(file, "12345678");
expect(() => readPrivateRegularFile(file, { maxBytes: 8 })).toThrow(
`${file} exceeds 8 bytes`,
);
expect(grewAfterFstat).toBe(true);
} finally {
fstatSync.mockRestore();
fs.rmSync(directory, { recursive: true, force: true });
}
});
});
105 changes: 105 additions & 0 deletions test/e2e/live/bedrock-runtime-compatible-anthropic-leaks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export const SNAPSHOT_PROBE_PID_PREFIX = "@@NEMOCLAW_E2E_PROBE_PID@@ ";
export const SNAPSHOT_FILE_PREFIX = "@@NEMOCLAW_E2E_FILE@@ ";
export const SNAPSHOT_DATA_PREFIX = "@@NEMOCLAW_E2E_DATA@@ ";
const PID_PATTERN = /^[1-9][0-9]*$/u;

export interface ForbiddenLeakPattern {
name: string;
value: string;
allowInSnapshotProbeEnvironment?: boolean;
}

export interface ForbiddenLeakScan {
leaks: string[];
snapshotProbeEnvironmentExemptions: Array<{ name: string; location: string }>;
}

export function frameSnapshotFile(location: string, contents: string): string {
if (!location || /[\r\n]/u.test(location)) {
throw new Error("snapshot file location must be a non-empty single line");
}
return [
`${SNAPSHOT_FILE_PREFIX}${location}`,
...contents.split("\n").map((line) => `${SNAPSHOT_DATA_PREFIX}${line}`),
].join("\n");
}

function isSnapshotProbeEnvironment(location: string, probePid: string | undefined): boolean {
return probePid !== undefined && location === `/proc/${probePid}/environ`;
}

/**
* Find forbidden values while distinguishing the one-shot snapshot process
* from the sandbox workloads it observes. `src/lib/onboard/bedrock-runtime.ts`
* registers the adapter credential as an attached generic OpenShell provider.
* OpenShell, outside this repository, projects that provider's placeholder
* name into an ad-hoc `sandbox exec` child, so the observer sees the name in
* its own environment. Only patterns explicitly marked for that exact
* PID/environment location are exempt; raw token values and every match in
* other files or processes still fail the scan.
*
* The live test requires this exemption to be observed. Remove the flag, that
* assertion, and this exception when OpenShell stops projecting attached
* provider placeholders into inspection children or offers provider-free
* sandbox inspection.
*/
export function scanForbiddenLeaks(
text: string,
label: string,
patterns: readonly ForbiddenLeakPattern[],
): ForbiddenLeakScan {
const locations: string[] = [];
const exemptions: Array<{ name: string; location: string }> = [];
let current: string | undefined;
let probePid: string | undefined;
let firstNonEmptyLineSeen = false;

for (const line of text.split("\n")) {
if (!firstNonEmptyLineSeen && line.length > 0) {
firstNonEmptyLineSeen = true;
if (line.startsWith(SNAPSHOT_PROBE_PID_PREFIX)) {
const candidate = line.slice(SNAPSHOT_PROBE_PID_PREFIX.length);
if (PID_PATTERN.test(candidate)) probePid = candidate;
continue;
}
}
if (line.startsWith(SNAPSHOT_FILE_PREFIX)) {
current = line.slice(SNAPSHOT_FILE_PREFIX.length);
continue;
}
if (!line.startsWith(SNAPSHOT_DATA_PREFIX)) continue;
const data = line.slice(SNAPSHOT_DATA_PREFIX.length);
const location = current ?? label;
for (const pattern of patterns) {
if (!pattern.value || !data.includes(pattern.value)) continue;
if (
pattern.allowInSnapshotProbeEnvironment &&
isSnapshotProbeEnvironment(location, probePid)
) {
exemptions.push({ name: pattern.name, location });
continue;
}
locations.push(`${pattern.name}: ${location}`);
}
}
return {
leaks: [...new Set(locations)].sort(),
snapshotProbeEnvironmentExemptions: exemptions.filter(
(entry, index, entries) =>
entries.findIndex(
(candidate) => candidate.name === entry.name && candidate.location === entry.location,
) === index,
),
};
}

export function findForbiddenLeaks(
text: string,
label: string,
patterns: readonly ForbiddenLeakPattern[],
): string[] {
return scanForbiddenLeaks(text, label, patterns).leaks;
}
74 changes: 35 additions & 39 deletions test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ import {
type RawArtifactOutputMode,
summarizeSandboxSnapshot,
} from "./bedrock-runtime-compatible-anthropic-artifacts.ts";
import {
type ForbiddenLeakPattern,
findForbiddenLeaks,
frameSnapshotFile,
SNAPSHOT_DATA_PREFIX,
SNAPSHOT_FILE_PREFIX,
SNAPSHOT_PROBE_PID_PREFIX,
scanForbiddenLeaks,
} from "./bedrock-runtime-compatible-anthropic-leaks.ts";
import {
BEDROCK_PRE_CONTRACT_ENDPOINT_VALIDATION_INVALID_STATE,
BEDROCK_PRE_CONTRACT_ENDPOINT_VALIDATION_REMOVAL_CONDITION,
Expand Down Expand Up @@ -1153,13 +1162,14 @@ function assertAdapterLogBreadcrumbs(home: string, agent: AgentName): void {

const SNAPSHOT_SCRIPT = trustedSandboxShellScript(`
set +e
printf '${SNAPSHOT_PROBE_PID_PREFIX}%s\\n' "$$"
emit_file() {
path="$1"
[ -r "$path" ] || return 0
size=$(wc -c <"$path" 2>/dev/null || echo 0)
[ "$size" -le 1048576 ] || return 0
printf '\\n@@NEMOCLAW_E2E_FILE@@ %s\\n' "$path"
tr '\\000' '\\n' <"$path" 2>/dev/null || true
printf '\\n${SNAPSHOT_FILE_PREFIX}%s\\n' "$path"
tr '\\000' '\\n' <"$path" 2>/dev/null | sed 's/^/${SNAPSHOT_DATA_PREFIX}/' || true
}

for root in /sandbox/.openclaw /sandbox/.hermes /etc/nemoclaw /tmp; do
Expand All @@ -1180,25 +1190,6 @@ for proc_dir in /proc/[0-9]*; do
done
`);

function findForbiddenLeaks(
text: string,
label: string,
patterns: Array<[string, string]>,
): string[] {
const locations: string[] = [];
let current = label;
for (const line of text.split("\n")) {
if (line.startsWith("@@NEMOCLAW_E2E_FILE@@ ")) {
current = line.slice("@@NEMOCLAW_E2E_FILE@@ ".length);
continue;
}
for (const [name, value] of patterns) {
if (value && line.includes(value)) locations.push(`${name}: ${current}`);
}
}
return [...new Set(locations)].sort();
}

function isPreContractEndpointValidationRateLimit(options: {
mock: MockBedrockRuntime | undefined;
onboarding: RawRunResult;
Expand Down Expand Up @@ -1255,12 +1246,16 @@ async function assertNoBedrockLeaks(options: {
redact: (text: string, extraValues?: string[]) => string;
}): Promise<void> {
const adapterToken = readAdapterToken(options.home);
const patterns: Array<[string, string]> = [
["fake user key", COMPATIBLE_KEY],
["adapter token", adapterToken],
["AWS bearer env name", "AWS_BEARER_TOKEN_BEDROCK"],
["adapter token env name", "NEMOCLAW_BEDROCK_RUNTIME_ADAPTER_TOKEN"],
["raw Bedrock hostname", BEDROCK_HOSTNAME],
const patterns: ForbiddenLeakPattern[] = [
{ name: "fake user key", value: COMPATIBLE_KEY },
{ name: "adapter token", value: adapterToken },
{ name: "AWS bearer env name", value: "AWS_BEARER_TOKEN_BEDROCK" },
{
name: "adapter token env name",
value: "NEMOCLAW_BEDROCK_RUNTIME_ADAPTER_TOKEN",
allowInSnapshotProbeEnvironment: true,
},
{ name: "raw Bedrock hostname", value: BEDROCK_HOSTNAME },
];
const snapshot = await runRawCommand(
"openshell",
Expand All @@ -1278,27 +1273,28 @@ async function assertNoBedrockLeaks(options: {
? fs.readFileSync(adapterLogPath(options.home), "utf8")
: "";
const hostLogs = [
"@@NEMOCLAW_E2E_FILE@@ onboard stdout",
options.onboarding.stdout,
"@@NEMOCLAW_E2E_FILE@@ onboard stderr",
options.onboarding.stderr,
"@@NEMOCLAW_E2E_FILE@@ adapter log",
adapterLog,
"@@NEMOCLAW_E2E_FILE@@ fake Bedrock mock log",
options.mock.logs.join("\n"),
frameSnapshotFile("onboard stdout", options.onboarding.stdout),
frameSnapshotFile("onboard stderr", options.onboarding.stderr),
frameSnapshotFile("adapter log", adapterLog),
frameSnapshotFile("fake Bedrock mock log", options.mock.logs.join("\n")),
].join("\n");
await options.artifacts.writeText(
"host-bedrock-runtime-logs.txt",
options.redact(hostLogs, [COMPATIBLE_KEY, adapterToken]),
);

const leaks = [
...findForbiddenLeaks(snapshot.stdout, "sandbox snapshot", patterns),
...findForbiddenLeaks(hostLogs, "host logs", patterns),
];
const sandboxLeakScan = scanForbiddenLeaks(snapshot.stdout, "sandbox snapshot", patterns);
expect(
sandboxLeakScan.snapshotProbeEnvironmentExemptions.some(
(entry) => entry.name === "adapter token env name",
),
"OpenShell no longer projects the adapter placeholder into the snapshot child; remove the probe-environment exemption",
).toBe(true);
const leaks = [...sandboxLeakScan.leaks, ...findForbiddenLeaks(hostLogs, "host logs", patterns)];
await options.artifacts.writeJson("sandbox-snapshot-bedrock-runtime-summary.json", {
...summarizeSandboxSnapshot(snapshot.stdout),
forbiddenLeakCount: leaks.length,
snapshotProbeEnvironmentExemptions: sandboxLeakScan.snapshotProbeEnvironmentExemptions,
rawContentPublished: false,
});
expect(leaks).toEqual([]);
Expand Down
Loading