Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
35 changes: 34 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,36 @@ 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();
// 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: () => ({
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");
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