Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
02d10a1
fix(policy): bind managed authority to creation receipts
apurvvkumaria Aug 26, 2026
97cd487
fix(onboard): complete verified create checkpoint
apurvvkumaria Aug 26, 2026
a612f54
fix(onboard): complete receipt-bound policy checks
apurvvkumaria Aug 26, 2026
57e671c
docs(onboard): make recovery guidance fail closed
apurvvkumaria Aug 26, 2026
ac152bf
fix(onboard): defer managed authority publication
apurvvkumaria Aug 26, 2026
5aac792
test(blueprint): cover policy authority failures
apurvvkumaria Aug 26, 2026
a16f86b
fix(state): remove redundant backup state update
apurvvkumaria Aug 26, 2026
7b7cea8
test(e2e): configure blueprint apply policy
apurvvkumaria Aug 26, 2026
aa75a75
test(e2e): model policy mutation receipts
apurvvkumaria Aug 26, 2026
73e4691
fix(policy): revalidate managed creation authority
apurvvkumaria Aug 26, 2026
79ee109
fix(onboard): qualify policy receipt verification
apurvvkumaria Aug 26, 2026
f09e2bd
merge(onboard): reconcile current main
apurvvkumaria Aug 26, 2026
60bc235
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
e5f39f7
fix(sandbox): bind deletion to pending identity
apurvvkumaria Aug 26, 2026
7f25a66
merge: reconcile remote branch
apurvvkumaria Aug 26, 2026
6ef8ba6
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
cc38d51
fix(onboard): preserve reserved route staging
apurvvkumaria Aug 26, 2026
fc5cd57
merge: reconcile remote branch
apurvvkumaria Aug 26, 2026
69eaf73
fix(onboard): narrow managed bootstrap surface
apurvvkumaria Aug 26, 2026
200ec53
fix(onboard): revalidate provider attachments
apurvvkumaria Aug 26, 2026
e9d9613
fix(onboard): preserve failed cancel recovery
apurvvkumaria Aug 26, 2026
81e3425
fix(onboard): gate normal sandbox effects
apurvvkumaria Aug 26, 2026
828dedf
test(policy): cover active global read
apurvvkumaria Aug 26, 2026
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
301 changes: 107 additions & 194 deletions nemoclaw/src/blueprint/runner-identity.test.ts

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion nemoclaw/src/blueprint/runner-mock-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface RunnerFsEntry {
export const FAKE_HOME = "/fakehome";

/** The deterministic UUID every runner suite mocks crypto.randomUUID() to. */
export const FIXED_RUN_UUID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
export const FIXED_RUN_UUID = "aaaaaaaa-bbbb-4ccc-addd-eeeeeeeeeeee";

/** An in-memory filesystem store with its seeding helpers. */
export interface RunnerFsStore {
Expand Down Expand Up @@ -59,6 +59,8 @@ export interface InMemoryFsOptions {
*/
export function inMemoryFsMethods(store: Map<string, RunnerFsEntry>, options?: InMemoryFsOptions) {
const spy = options?.spy ?? (<T>(fn: T): T => fn);
let nextFd = 100;
const openFiles = new Map<number, string>();
return {
existsSync: (p: string) => store.has(p),
mkdirSync: spy((p: string) => void store.set(p, { type: "dir" })),
Expand All @@ -69,6 +71,18 @@ export function inMemoryFsMethods(store: Map<string, RunnerFsEntry>, options?: I
writeFileSync: spy((p: string, data: string) => {
store.set(p, { type: "file", content: String(data) });
}),
openSync: spy((p: string) => {
if (!store.has(p)) return missingEntry(p);
const fd = nextFd++;
openFiles.set(fd, p);
return fd;
}),
fsyncSync: spy((fd: number) => {
if (!openFiles.has(fd)) throw new Error(`EBADF: ${fd}`);
}),
closeSync: spy((fd: number) => {
if (!openFiles.delete(fd)) throw new Error(`EBADF: ${fd}`);
}),
renameSync: spy((source: string, destination: string) => {
const entry = store.get(source);
if (!entry) return missingEntry(source);
Expand Down Expand Up @@ -141,3 +155,12 @@ export function createStdoutCapture(): StdoutCapture {
},
};
}

/** Returns a callback that throws on one numbered invocation. */
export function throwOnCall(callNumber: number, error: Error): () => void {
let calls = 0;
return () => {
calls += 1;
if (calls === callNumber) throw error;
};
}
65 changes: 38 additions & 27 deletions nemoclaw/src/blueprint/runner-name-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
minimalBlueprint,
resultWithBlueprintPolicyAuthority,
successResult,
TEST_SANDBOX_POLICY,
TEST_SANDBOX_POLICY_PATH,
} from "./runner-test-fixtures.js";

const { store, addFile, addDir } = createRunnerFsStore();
Expand All @@ -28,7 +30,8 @@ vi.mock("node:os", () => ({
homedir: () => FAKE_HOME,
}));

vi.mock("node:crypto", () => ({
vi.mock("node:crypto", async (importOriginal) => ({
...(await importOriginal<typeof import("node:crypto")>()),
randomUUID: () => FIXED_RUN_UUID,
}));

Expand All @@ -38,7 +41,10 @@ vi.mock("node:fs", async (importOriginal) => {
return {
...original,
existsSync: memory.existsSync,
closeSync: memory.closeSync,
fsyncSync: memory.fsyncSync,
mkdirSync: memory.mkdirSync,
openSync: memory.openSync,
readFileSync: memory.readFileSync,
renameSync: memory.renameSync,
writeFileSync: memory.writeFileSync,
Expand Down Expand Up @@ -116,6 +122,8 @@ describe("blueprint name validation (fail-closed integration)", () => {

beforeEach(() => {
store.clear();
addFile(TEST_SANDBOX_POLICY_PATH, TEST_SANDBOX_POLICY);
vi.stubEnv("OPENSHELL_SANDBOX_POLICY", TEST_SANDBOX_POLICY_PATH);
stdout.reset();
vi.clearAllMocks();
vi.spyOn(process.stdout, "write").mockImplementation(stdout.write);
Expand All @@ -126,35 +134,38 @@ describe("blueprint name validation (fail-closed integration)", () => {

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
});

it.each(
INVALID_NAME_CASES,
)("apply rejects a malformed %s and creates no sandbox", async (_label, corrupt, expected) => {
const bp = minimalBlueprint();
corrupt(bp.components as BlueprintComponents);

await expect(actionApply("default", bp)).rejects.toThrow(expected);
// Validation runs before any command, so no provider or sandbox command
// may have executed — not merely no sandbox create.
expect(mockExeca).not.toHaveBeenCalled();
expect(createCalls()).toEqual([]);
});
it.each(INVALID_NAME_CASES)(
"apply rejects a malformed %s and creates no sandbox",
async (_label, corrupt, expected) => {
const bp = minimalBlueprint();
corrupt(bp.components as BlueprintComponents);

await expect(actionApply("default", bp)).rejects.toThrow(expected);
// Validation runs before any command, so no provider or sandbox command
// may have executed — not merely no sandbox create.
expect(mockExeca).not.toHaveBeenCalled();
expect(createCalls()).toEqual([]);
},
);

it.each(
VALID_PROVIDER_NAMES,
)("apply accepts the supported provider name '%s'", async (providerName) => {
const bp = minimalBlueprint();
const components = bp.components as BlueprintComponents;
components.inference.profiles.default.provider_name = providerName;

await expect(actionApply("default", bp)).resolves.toBeUndefined();
expect(mockExeca).toHaveBeenCalledWith(
"openshell",
expect.arrayContaining(["provider", "create", "--name", providerName]),
expect.any(Object),
);
});
it.each(VALID_PROVIDER_NAMES)(
"apply accepts the supported provider name '%s'",
async (providerName) => {
const bp = minimalBlueprint();
const components = bp.components as BlueprintComponents;
components.inference.profiles.default.provider_name = providerName;

await expect(actionApply("default", bp)).resolves.toBeUndefined();
expect(mockExeca).toHaveBeenCalledWith(
"openshell",
expect.arrayContaining(["provider", "create", "--name", providerName]),
expect.any(Object),
);
},
);

it("rollback rejects a plan whose sandbox_name is not OpenShell-compatible", async () => {
const runDir = `${RUNS_DIR}/nc-run-1`;
Expand Down
Loading
Loading