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
46 changes: 45 additions & 1 deletion src/lib/shields/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type HarnessOptions = {
send: () => boolean;
kill: () => boolean;
};
livePolicyYaml?: string;
run?: (cmd: unknown) => { status: number };
};

Expand Down Expand Up @@ -90,7 +91,9 @@ function createHarness(options: HarnessOptions = {}): ShieldsHarness {
let openClawPosture: "locked" | "mutable" = "mutable";

vi.spyOn(runner, "validateName").mockImplementation((name: unknown) => String(name));
vi.spyOn(runner, "runCapture").mockReturnValue("version: 1\nnetwork_policies:\n test: {}\n");
vi.spyOn(runner, "runCapture").mockReturnValue(
options.livePolicyYaml ?? "version: 1\nnetwork_policies:\n test: {}\n",
);
const runSpy = vi.spyOn(runner, "run").mockImplementation((cmd: unknown) => {
return options.run ? options.run(cmd) : { status: 0 };
});
Expand Down Expand Up @@ -874,6 +877,47 @@ describe("shields command flow", () => {
expect(fs.existsSync(path.join(stateDir, "shields-timer-openclaw.json"))).toBe(true);
});

it("shields down removes the permissive runtime temp directory when the auto-restore timer fails (#7964)", () => {
const tempRoot = os.tmpdir();
const permissiveRuntimeDirs = () =>
fs.readdirSync(tempRoot).filter((name) => name.startsWith("nemoclaw-permissive-runtime-"));
const before = permissiveRuntimeDirs();
// shieldsDown builds the temp policy before it forks the auto-restore timer,
// so the fork mock observes the runtime-policy directory mid-transition. That
// proves the test exercises real temp-policy creation, not only the absence
// of a leak.
let runtimeDirsDuringFork: string[] = [];
// A real `openshell policy get --base` carries filesystem_policy paths, so the
// permissive merge writes a temp policy file instead of returning the static
// base path. That is the state that makes the leak reachable.
const harness = createHarness({
livePolicyYaml:
"version: 1\nfilesystem_policy:\n read_write:\n - /proc\n read_only:\n - /opt/hermes\n",
fork: () => {
runtimeDirsDuringFork = permissiveRuntimeDirs();
return {
pid: 0,
disconnect: vi.fn(),
unref: vi.fn(),
send: vi.fn(() => true),
kill: vi.fn(() => true),
};
},
});

expect(() =>
harness.shieldsDown("openclaw", {
timeout: "5m",
reason: "temp cleanup coverage",
throwOnError: true,
}),
).toThrow("Cannot start auto-restore timer");
// One runtime-policy directory existed during the transition, and none
// remains after the failed shields down.
expect(runtimeDirsDuringFork.length).toBe(before.length + 1);
expect(permissiveRuntimeDirs()).toEqual(before);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

it("shieldsUp refuses to mark lockdown active when the saved restrictive policy snapshot is missing", () => {
const harness = createHarness();
const stateDir = path.join(tmpDir, ".nemoclaw", "state");
Expand Down
17 changes: 14 additions & 3 deletions src/lib/shields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2674,6 +2674,17 @@ function shieldsDownWithoutHostLock(sandboxName: string, opts: ShieldsDownOpts =
return failShieldsCommand(`Unknown policy "${policyName}"`, opts.throwOnError);
}

// Every exit after the permissive merge builds a temp policy directory must
// remove it. The apply below has always cleaned up, but the timer-failure
// and saveShieldsState-failure early exits between here and there historically
// leaked one 0700 nemoclaw-permissive-runtime-* directory per failed
// attempt. Route all three exits through one cleanup. See #7964.
const cleanupRuntimePolicyFile = () => {
if (policyFileIsTemp) {
cleanupTempDir(policyFile, "nemoclaw-permissive-runtime");
}
};

const now = new Date().toISOString();
let transition: ShieldsDownTransition | null = null;

Expand Down Expand Up @@ -2762,6 +2773,7 @@ function shieldsDownWithoutHostLock(sandboxName: string, opts: ShieldsDownOpts =
}
clearTimerMarker(sandboxName);
clearShieldsDownTransition(sandboxName, processToken);
cleanupRuntimePolicyFile();
const message = err instanceof Error ? err.message : String(err);
console.error(` Cannot start auto-restore timer: ${message}`);
return failShieldsCommand(`Cannot start auto-restore timer: ${message}`, opts.throwOnError);
Expand All @@ -2782,16 +2794,15 @@ function shieldsDownWithoutHostLock(sandboxName: string, opts: ShieldsDownOpts =
clearShieldsDownTransition(sandboxName, transition.processToken);
killTimer(sandboxName);
}
cleanupRuntimePolicyFile();
throw error;
}

console.log(` Applying ${policyName} policy...`);
try {
run(buildPolicySetCommand(policyFile, sandboxName));
} finally {
if (policyFileIsTemp) {
cleanupTempDir(policyFile, "nemoclaw-permissive-runtime");
}
cleanupRuntimePolicyFile();
}

// 2b. Return config to default mutable state.
Expand Down
Loading