Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8e06dc4
fix(shields): keep shields status truthful when the permissive policy…
jason-ma-nv Aug 4, 2026
11def3c
fix(shields): keep auto-restore authoritative if rollback state write…
jason-ma-nv Aug 4, 2026
3bd6802
Merge branch 'main' into fix/8198-shields-down-status-integrity
cv Aug 4, 2026
503f345
merge: resolve conflicts with main
github-actions[bot] Aug 4, 2026
b2bff27
Merge branch 'main' into fix/8198-shields-down-status-integrity
apurvvkumaria Aug 4, 2026
26fa93b
Merge remote-tracking branch 'origin/main' into fix/8198-shields-down…
jason-ma-nv Aug 4, 2026
5a2588e
test(shields): cover the rollback state-write failure branch (#8198)
jason-ma-nv Aug 4, 2026
00213ee
test(shields): preserve timer authority evidence
apurvvkumaria Aug 5, 2026
668ffe9
merge(main): align shields rollback tests
apurvvkumaria Aug 5, 2026
23ff4c4
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
f65c195
Merge branch 'main' into fix/8198-shields-down-status-integrity
senthilr-nv Aug 5, 2026
36990f9
test(shields): cover rejected policy recovery fallback
senthilr-nv Aug 5, 2026
99688d5
test(shields): satisfy test flow guardrail
senthilr-nv Aug 5, 2026
cf1dcdf
Merge remote-tracking branch 'upstream/main' into fix/8198-shields-do…
senthilr-nv Aug 5, 2026
460aefd
docs(shields): explain rejected policy recovery
senthilr-nv Aug 5, 2026
e12bb44
Merge remote-tracking branch 'upstream/main' into fix/8198-shields-do…
senthilr-nv Aug 5, 2026
1465aa7
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
a4e58dd
test(shields): remove duplicate timer control binding
apurvvkumaria Aug 5, 2026
1512b75
merge(main): refresh shields status fix
apurvvkumaria Aug 5, 2026
e6e4aab
fix(shields): distinguish rejected policy transitions
apurvvkumaria Aug 5, 2026
4a3b0ae
docs(shields): clarify rejected transition fallback
apurvvkumaria Aug 5, 2026
67e70b2
fix(shields): deny mutations during incomplete rejection
apurvvkumaria Aug 5, 2026
70740eb
merge: refresh PR branch from main
apurvvkumaria Aug 5, 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
35 changes: 35 additions & 0 deletions src/lib/shields/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,41 @@ describe("shields command flow", () => {
expect(permissiveRuntimeDirs()).toEqual(before);
});

it("shields down clears state and reports the sandbox as up when the permissive policy is rejected (#8198)", () => {
// OpenShell refuses the live policy change — e.g. a Deep Agents sandbox
// whose landlock policy is sealed at startup — so the permissive
// `policy set` exits non-zero and nothing is actually weakened.
const harness = createHarness({
run: (cmd) => {
const argv = Array.isArray(cmd) ? cmd.map(String) : [];
return argv.includes("policy") && argv.includes("set") ? { status: 1 } : { status: 0 };
},
fork: () => ({
pid: 4242,
disconnect: vi.fn(),
unref: vi.fn(),
send: vi.fn(() => true),
kill: vi.fn(() => true),
}),
});

expect(() =>
harness.shieldsDown("openclaw", {
timeout: "5m",
reason: "verify",
throwOnError: true,
}),
).toThrow(/Could not apply/);

// The unlock never took effect, so status must not claim DOWN/permissive.
expect(harness.isShieldsDown("openclaw")).toBe(false);
const statePath = path.join(tmpDir, ".nemoclaw", "state", "shields-openclaw.json");
const stateOnDisk = fs.existsSync(statePath)
? JSON.parse(fs.readFileSync(statePath, "utf-8"))
: { shieldsDown: false };
expect(stateOnDisk.shieldsDown).toBe(false);
});

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
32 changes: 31 additions & 1 deletion src/lib/shields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3025,11 +3025,41 @@ function shieldsDownWithoutHostLock(sandboxName: string, opts: ShieldsDownOpts =
}

console.log(` Applying ${policyName} policy...`);
let policySetResult: ReturnType<typeof run>;
try {
run(buildPolicySetCommand(policyFile, sandboxName));
policySetResult = run(buildPolicySetCommand(policyFile, sandboxName), {
ignoreError: true,
});
} finally {
cleanupRuntimePolicyFile();
}
if (policySetResult.status !== 0) {
// The permissive policy was rejected before it applied — e.g. OpenShell
// refuses a live landlock change on a sandbox whose policy is sealed at
// startup (Deep Agents). Nothing was weakened: config is still locked and
// the restrictive policy is unchanged. The shields-down state persisted
// above is therefore false, so clear it (and cancel the now-pointless
// auto-restore timer/transition) and fail closed. Otherwise `shields
// status` would report DOWN/permissive for an unlock that never happened.
// See #8198.
saveShieldsState(sandboxName, {
shieldsDown: false,
shieldsDownAt: null,
shieldsDownTimeout: null,
shieldsDownReason: null,
shieldsDownPolicy: null,
shieldsPolicySnapshotPath: null,
});
if (transition) clearShieldsDownTransition(sandboxName, transition.processToken);
killTimer(sandboxName);
Comment thread
apurvvkumaria marked this conversation as resolved.
Outdated
console.error(
` ERROR: Could not apply the ${policyName} policy; the sandbox remains shielded.`,
);
console.error(
" Shields down did not take effect. `shields status` continues to report the sandbox as up.",
);
return failShieldsCommand(`Could not apply ${policyName} policy`, opts.throwOnError);
}

// 2b. Return config to default mutable state.
// OpenClaw uses sandbox:sandbox 0660/2770 here so the gateway UID, which
Expand Down
Loading