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
4 changes: 2 additions & 2 deletions ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"src/lib/onboard/preflight.test.ts": 1875,
"test/generate-openclaw-config.test.ts": 1907,
"test/install-preflight.test.ts": 3025,
"test/nemoclaw-start.test.ts": 4789,
"test/nemoclaw-start.test.ts": 4785,
"test/onboard-messaging.test.ts": 2033,
"test/onboard-selection.test.ts": 4178
"test/onboard-selection.test.ts": 4177
}
}
89 changes: 40 additions & 49 deletions nemoclaw/src/blueprint/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,15 @@ describe("runner", () => {
expect(plan.timestamp).toBeDefined();
});

it("persists only the explicit safe plan schema", async () => {
it.each([
"credential_env",
"credential_default",
"SECRET_KEY",
"default-secret-value",
"real-secret",
"future-token-value",
"future-authorization",
])("persists only the explicit safe plan schema [%s]", async (leaked) => {
const bp = {
components: {
inference: {
Expand Down Expand Up @@ -902,17 +910,8 @@ describe("runner", () => {
endpoint: "https://api.example.com",
model: "gpt-4",
});
for (const leaked of [
"credential_env",
"credential_default",
"SECRET_KEY",
"default-secret-value",
"real-secret",
"future-token-value",
"future-authorization",
]) {
expect(entry.content).not.toContain(leaked);
}

expect(entry.content).not.toContain(leaked);
});

it("emits all progress milestones", async () => {
Expand Down Expand Up @@ -1184,7 +1183,19 @@ describe("runner", () => {
expect(stdoutText()).toContain('"nc-run-1"');
});

it("re-renders only safe allowlisted fields from plan.json", () => {
it.each([
"credential_env",
"credential_default",
"SECRET_KEY",
"default-secret-value",
"future-token-value",
"future-authorization",
"sandbox-token-value",
"router-authorization",
"top-level-token-value",
"top-level-authorization",
"future-api-key",
])("re-renders only safe allowlisted fields from plan.json [%s]", (leaked) => {
const rid = "nc-run-sensitive";
addDir(`${RUNS_DIR}/${rid}`);
addFile(
Expand Down Expand Up @@ -1253,21 +1264,8 @@ describe("runner", () => {
dry_run: false,
});
const out = stdoutText();
for (const leaked of [
"credential_env",
"credential_default",
"SECRET_KEY",
"default-secret-value",
"future-token-value",
"future-authorization",
"sandbox-token-value",
"router-authorization",
"top-level-token-value",
"top-level-authorization",
"future-api-key",
]) {
expect(out).not.toContain(leaked);
}

expect(out).not.toContain(leaked);
});

it("prints unknown status when plan.json is missing", () => {
Expand All @@ -1288,17 +1286,14 @@ describe("runner", () => {

// ── Path traversal rejection ──────────────────────────────────

it.each([
"../../etc",
"../tmp",
"valid.with.dots",
"foo\x00bar",
"/absolute/path",
])("rejects malicious run ID: %j", (rid) => {
expect(() => {
actionStatus(rid);
}).toThrow(/Invalid run ID/);
});
it.each(["../../etc", "../tmp", "valid.with.dots", "foo\x00bar", "/absolute/path"])(
"rejects malicious run ID: %j",
(rid) => {
expect(() => {
actionStatus(rid);
}).toThrow(/Invalid run ID/);
},
);

it("accepts a legitimate hyphenated run ID", () => {
const rid = "nc-20260406-abc12345";
Expand Down Expand Up @@ -1354,16 +1349,12 @@ describe("runner", () => {

// ── Path traversal rejection ──────────────────────────────────

it.each([
"../../etc",
"../tmp",
"valid.with.dots",
"foo\x00bar",
"/absolute/path",
"",
])("rejects malicious run ID: %j", async (rid) => {
await expect(actionRollback(rid)).rejects.toThrow(/Invalid run ID/);
});
it.each(["../../etc", "../tmp", "valid.with.dots", "foo\x00bar", "/absolute/path", ""])(
"rejects malicious run ID: %j",
async (rid) => {
await expect(actionRollback(rid)).rejects.toThrow(/Invalid run ID/);
},
);

it("throws when rollback plan has no sandbox_name", async () => {
const runDir = `${RUNS_DIR}/nc-run-1`;
Expand Down
135 changes: 75 additions & 60 deletions nemoclaw/src/security/snapshot-sanitizer-failure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,49 @@ describe("migration snapshot sanitizer fallbacks", () => {
mtimeNs: "3",
ctimeNs: "4",
};
const malformedDescriptorOutputs = [
{ label: "non-JSON output", output: "not-json" },
{
label: "array directories",
output: JSON.stringify({ root: identity, directories: [], files: [] }),
},
{
label: "escaping directory path",
output: JSON.stringify({
root: identity,
directories: { "nested\\escape": identity },
files: [],
}),
},
{
label: "null file",
output: JSON.stringify({ root: identity, directories: {}, files: [null] }),
},
{
label: "absolute file path",
output: JSON.stringify({
root: identity,
directories: {},
files: [{ path: "/escape", metadata: identity }],
}),
},
{
label: "null file metadata",
output: JSON.stringify({
root: identity,
directories: {},
files: [{ path: "config.json", metadata: null }],
}),
},
{
label: "non-string file content",
output: JSON.stringify({
root: identity,
directories: {},
files: [{ path: "config.json", metadata: identity, content: 42 }],
}),
},
];

it("fails closed when the descriptor helper is unavailable", () => {
const configPath = path.join(makeRoot(), "openclaw.json");
Expand Down Expand Up @@ -285,35 +328,14 @@ describe("migration snapshot sanitizer fallbacks", () => {
expect(readFileSync(configPath, "utf-8")).toBe(original);
});

it("rejects every malformed descriptor scan boundary", () => {
const root = { canonicalPath: makeRoot(), identity };
const malformedOutputs = [
"not-json",
JSON.stringify({ root: identity, directories: [], files: [] }),
JSON.stringify({ root: identity, directories: { "nested\\escape": identity }, files: [] }),
JSON.stringify({ root: identity, directories: {}, files: [null] }),
JSON.stringify({
root: identity,
directories: {},
files: [{ path: "/escape", metadata: identity }],
}),
JSON.stringify({
root: identity,
directories: {},
files: [{ path: "config.json", metadata: null }],
}),
JSON.stringify({
root: identity,
directories: {},
files: [{ path: "config.json", metadata: identity, content: 42 }],
}),
];

for (const output of malformedOutputs) {
it.each(malformedDescriptorOutputs)(
"rejects a malformed descriptor with $label",
({ output }) => {
const root = { canonicalPath: makeRoot(), identity };
writePythonWrapper([`printf '%s\\n' ${shellQuote(output)}`]);
expect(scanDescriptorSnapshot(root, new Set())).toBeNull();
}
});
},
);

it("rejects unsafe roots and non-canonical helper payloads", () => {
const root = makeRoot();
Expand Down Expand Up @@ -349,47 +371,40 @@ describe("migration snapshot sanitizer fallbacks", () => {
expect(decodeDescriptorSnapshotContent(oversized)).toBeNull();
});

it("preserves canonical base64 and UTF-8 boundary rules", () => {
expect(decodeDescriptorSnapshotContent("")).toBe("");
expect(decodeDescriptorSnapshotContent("Zg==")).toBe("f");
expect(decodeDescriptorSnapshotContent("Zm8=")).toBe("fo");
expect(decodeDescriptorSnapshotContent("Zm9v")).toBe("foo");
expect(decodeDescriptorSnapshotContent("aGVsbG8=")).toBe("hello");
for (const rejected of [
"A",
"AAAAA",
"AA=A",
"A===",
"====",
"YWJj=",
"AB==",
"AAB=",
"/w==",
"YWJj\n",
]) {
it.each(["A", "AAAAA", "AA=A", "A===", "====", "YWJj=", "AB==", "AAB=", "/w==", "YWJj\n"])(
"preserves canonical base64 and UTF-8 boundary rules [%s]",
(rejected) => {
expect(decodeDescriptorSnapshotContent("")).toBe("");
expect(decodeDescriptorSnapshotContent("Zg==")).toBe("f");
expect(decodeDescriptorSnapshotContent("Zm8=")).toBe("fo");
expect(decodeDescriptorSnapshotContent("Zm9v")).toBe("foo");
expect(decodeDescriptorSnapshotContent("aGVsbG8=")).toBe("hello");

expect(decodeDescriptorSnapshotContent(rejected)).toBeNull();
}
});
},
);

it.each(["AB==", "AAB="])(
"rejects non-canonical base64 at the descriptor apply boundary [%s]",
(content) => {
const rootPath = makeRoot();
const configPath = path.join(rootPath, "config.json");
writeFileSync(configPath, "original");
const root = inspectDescriptorSnapshotRoot(rootPath)!;
const scan = scanDescriptorSnapshot(root, new Set())!;
const config = scan.files.find((file) => file.path === "config.json")!;

expect(scan).not.toBeNull();
expect(config).toBeDefined();

it("rejects non-canonical base64 at the descriptor apply boundary", () => {
const rootPath = makeRoot();
const configPath = path.join(rootPath, "config.json");
writeFileSync(configPath, "original");
const root = inspectDescriptorSnapshotRoot(rootPath)!;
const scan = scanDescriptorSnapshot(root, new Set())!;
const config = scan.files.find((file) => file.path === "config.json")!;

expect(scan).not.toBeNull();
expect(config).toBeDefined();
for (const content of ["AB==", "AAB="]) {
expect(
applyDescriptorSnapshotActions(root, scan, [
{ kind: "replace", path: config.path, metadata: config.metadata, content },
]),
).toBe(false);
expect(readFileSync(configPath, "utf-8")).toBe("original");
}
});
},
);

it("fails closed when sanitized output cannot be installed", () => {
const configPath = path.join(makeRoot(), "openclaw.json");
Expand Down
14 changes: 10 additions & 4 deletions src/lib/actions/sandbox/agent/passthrough-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,14 @@ describe("requestedAgentTimeoutSeconds", () => {
expect(agentDispatchDeadlineSeconds(argv)).toBeUndefined();
});

it("refuses a value that cannot be a deadline (#8723)", () => {
for (const raw of ["-5", "1.5", "abc", "", "1e3"]) {
it.each(["-5", "1.5", "abc", "", "1e3"])(
"refuses a value that cannot be a deadline: %j (#8723)",
(raw) => {
expect(requestedAgentTimeoutSeconds(agent("--timeout", raw))).toBeNull();
}
},
);

it("refuses a missing deadline value (#8723)", () => {
expect(requestedAgentTimeoutSeconds(agent("--timeout"))).toBeNull();
});
});
Expand All @@ -241,7 +245,9 @@ describe("agentDispatchDeadlineSeconds", () => {
);
// The buffer would round past the ceiling, so the argv would carry a
// deadline that differs from the one the caller asked for.
expect(agentDispatchDeadlineSeconds(["openclaw", "agent", "--timeout", ceiling])).toBeUndefined();
expect(
agentDispatchDeadlineSeconds(["openclaw", "agent", "--timeout", ceiling]),
).toBeUndefined();
});

it("still bounds the largest deadline that survives the buffer (#8723)", () => {
Expand Down
20 changes: 12 additions & 8 deletions src/lib/actions/sandbox/auto-pair-approval-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,16 @@ describe("buildAutoPairApprovalScript (#4263/#4616)", () => {
expect(module).not.toContain("recover_failed_scope_approval");
});

it("accepts exactly one terminal fixed receipt", () => {
it.each(
Array.from(
[
`${RECEIPT_MARKER}=approved-one\nlater output\n`,
`${RECEIPT_MARKER}=approve-failed\n${RECEIPT_MARKER}=approved-one\n`,
`${RECEIPT_MARKER}=raw-request-id\n`,
],
(value) => [value],
),
)("accepts exactly one terminal fixed receipt [case %#]", (output) => {
for (const receipt of [
"approved-one",
"list-failed",
Expand All @@ -136,12 +145,7 @@ describe("buildAutoPairApprovalScript (#4263/#4616)", () => {
parseAutoPairApprovalReceipt(`ignored setup output\n${RECEIPT_MARKER}=${receipt}\n`),
).toBe(receipt);
}
for (const output of [
`${RECEIPT_MARKER}=approved-one\nlater output\n`,
`${RECEIPT_MARKER}=approve-failed\n${RECEIPT_MARKER}=approved-one\n`,
`${RECEIPT_MARKER}=raw-request-id\n`,
]) {
expect(parseAutoPairApprovalReceipt(output)).toBeNull();
}

expect(parseAutoPairApprovalReceipt(output)).toBeNull();
});
});
Loading
Loading