fix(cli): preserve manually-added policy presets across sandbox rebuild#2023
fix(cli): preserve manually-added policy presets across sandbox rebuild#2023
Conversation
…ld (Fixes #1952) During rebuild, policy presets added via `policy-add` after initial onboard were lost because they only existed in the registry's sandbox entry, not in the onboard session. The sandbox deletion wiped them before the resume path could replay them. Now sandboxRebuild() reads applied presets from the registry before deletion and merges them into the session's policyPresets (with dedup). If the sandbox is degraded and getAppliedPresets fails, rebuild falls back to session presets only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdded merging of sandbox-applied policy presets into the onboard session during the rebuild flow, implemented a new helper to perform the merge, and added tests that validate merging, deduplication, error handling, and initialization of session presets. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant CLI as "nemoclaw CLI"
participant Rebuild as "sandboxRebuild"
participant Policies as "policies.mergePresetsIntoSession"
participant Registry as "registry (applied presets)"
participant Session as "onboardSession (sessionMod)"
CLI->>Rebuild: trigger rebuild
Rebuild->>Rebuild: backup workspace
Rebuild->>Policies: mergePresetsIntoSession(sandboxName, sessionMod, log)
Policies->>Registry: getAppliedPresets(sandboxName)
Registry-->>Policies: list of applied presets / error
Policies->>Session: updateSession(...) with merged policyPresets
Session-->>Policies: updated session state
Policies-->>Rebuild: return (success or logged warning)
Rebuild->>Rebuild: continue destroy + onboard + restore
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/nemoclaw.ts`:
- Around line 1814-1816: The catch block that handles failure to read applied
presets currently calls log(...) which is suppressed in non-verbose runs;
replace that call with a visible warning (e.g. console.warn(...) or the app's
warning-level logger method) so users see the fallback to session-only presets.
Locate the catch handling for "could not read applied presets" (the log(...)
call in src/nemoclaw.ts) and change it to console.warn(`Warning: could not read
applied presets (degraded sandbox?): ${err.message}`) or equivalent
logger.warn(...) to ensure the message is always emitted.
In `@test/rebuild-policy-presets.test.ts`:
- Around line 52-56: Tests are reimplementing the merge/session-update logic
instead of exercising the production code, so update the test to call the real
production path: either invoke sandboxRebuild with mocked side-effects (mock
openShell, backup/restore, etc.) or extract the session-merge helper from
src/nemoclaw.ts into a testable function and call that helper from the test;
replace the inline merge block in rebuild-policy-presets.test.ts with assertions
against the production helper or sandboxRebuild invocation and use mocks/stubs
for external IO to keep the test hermetic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 91958686-5a00-42b7-bcf0-19a27a5175e1
📒 Files selected for processing (2)
src/nemoclaw.tstest/rebuild-policy-presets.test.ts
…ning Address CodeRabbit review feedback on PR #2023: 1. The catch block in the preset-merge step used verbose-only `log()`, hiding the fallback warning from normal users. Now emits via `console.error()` so it is always visible. 2. Tests reimplemented the merge logic inline instead of exercising production code. Extracted `mergePresetsIntoSession()` into `src/lib/policies.ts` and rewrote tests to call the production helper with real registry data and permission-error simulation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/policies.ts`:
- Around line 488-506: The try/catch around both getAppliedPresets and the
session merge masks failures from sessionMod.updateSession/loadSession; narrow
the scope so only getAppliedPresets(sandboxName) is inside the try and any error
from that logs the current console.error/log message, then perform
sessionMod.updateSession(...) and sessionMod.loadSession() outside that catch
(or in a separate try/catch) so failures in sessionMod.updateSession or
sessionMod.loadSession produce their own error messages and aren’t misreported
as a preset-read failure; update the log statements to reflect which operation
failed and reference getAppliedPresets, sessionMod.updateSession, and
sessionMod.loadSession in the changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 623daf31-b183-4f96-8ba7-68865dac64ee
📒 Files selected for processing (3)
src/lib/policies.tssrc/nemoclaw.tstest/rebuild-policy-presets.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- test/rebuild-policy-presets.test.ts
- src/nemoclaw.ts
| try { | ||
| const appliedPresets = getAppliedPresets(sandboxName); | ||
| log(`Applied presets before rebuild: ${appliedPresets.join(", ") || "(none)"}`); | ||
| if (appliedPresets.length > 0) { | ||
| sessionMod.updateSession((s) => { | ||
| const sessionPresets = Array.isArray(s.policyPresets) ? s.policyPresets : []; | ||
| s.policyPresets = [...new Set([...sessionPresets, ...appliedPresets])]; | ||
| return s; | ||
| }); | ||
| log(`Session policyPresets updated: ${sessionMod.loadSession()?.policyPresets?.join(", ")}`); | ||
| } | ||
| } catch (err) { | ||
| const reason = err && err.message ? err.message : String(err); | ||
| console.error( | ||
| ` Warning: could not read applied presets; continuing rebuild with session presets only (${reason}).`, | ||
| ); | ||
| log(`Applied preset lookup failed: ${reason}`); | ||
| // Fall back to whatever the session already has — don't block rebuild. | ||
| } |
There was a problem hiding this comment.
Narrow the catch scope to avoid masking session-merge failures.
The current try/catch handles both preset-read and session-update paths, but always reports a read failure. If updateSession/loadSession fails, the real failure is masked and merged presets may be silently dropped.
Proposed fix
function mergePresetsIntoSession(sandboxName, sessionMod, log = () => {}) {
- try {
- const appliedPresets = getAppliedPresets(sandboxName);
- log(`Applied presets before rebuild: ${appliedPresets.join(", ") || "(none)"}`);
- if (appliedPresets.length > 0) {
- sessionMod.updateSession((s) => {
- const sessionPresets = Array.isArray(s.policyPresets) ? s.policyPresets : [];
- s.policyPresets = [...new Set([...sessionPresets, ...appliedPresets])];
- return s;
- });
- log(`Session policyPresets updated: ${sessionMod.loadSession()?.policyPresets?.join(", ")}`);
- }
- } catch (err) {
+ let appliedPresets;
+ try {
+ appliedPresets = getAppliedPresets(sandboxName);
+ } catch (err) {
const reason = err && err.message ? err.message : String(err);
console.error(
` Warning: could not read applied presets; continuing rebuild with session presets only (${reason}).`,
);
log(`Applied preset lookup failed: ${reason}`);
- // Fall back to whatever the session already has — don't block rebuild.
+ return;
+ }
+
+ log(`Applied presets before rebuild: ${appliedPresets.join(", ") || "(none)"}`);
+ if (appliedPresets.length === 0) return;
+
+ try {
+ sessionMod.updateSession((s) => {
+ const sessionPresets = Array.isArray(s.policyPresets) ? s.policyPresets : [];
+ s.policyPresets = [...new Set([...sessionPresets, ...appliedPresets])];
+ return s;
+ });
+ log(`Session policyPresets updated: ${sessionMod.loadSession()?.policyPresets?.join(", ") || "(none)"}`);
+ } catch (err) {
+ const reason = err && err.message ? err.message : String(err);
+ console.error(
+ ` Warning: could not merge applied presets into session; continuing rebuild with session presets only (${reason}).`,
+ );
+ log(`Session policyPresets merge failed: ${reason}`);
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const appliedPresets = getAppliedPresets(sandboxName); | |
| log(`Applied presets before rebuild: ${appliedPresets.join(", ") || "(none)"}`); | |
| if (appliedPresets.length > 0) { | |
| sessionMod.updateSession((s) => { | |
| const sessionPresets = Array.isArray(s.policyPresets) ? s.policyPresets : []; | |
| s.policyPresets = [...new Set([...sessionPresets, ...appliedPresets])]; | |
| return s; | |
| }); | |
| log(`Session policyPresets updated: ${sessionMod.loadSession()?.policyPresets?.join(", ")}`); | |
| } | |
| } catch (err) { | |
| const reason = err && err.message ? err.message : String(err); | |
| console.error( | |
| ` Warning: could not read applied presets; continuing rebuild with session presets only (${reason}).`, | |
| ); | |
| log(`Applied preset lookup failed: ${reason}`); | |
| // Fall back to whatever the session already has — don't block rebuild. | |
| } | |
| function mergePresetsIntoSession(sandboxName, sessionMod, log = () => {}) { | |
| let appliedPresets; | |
| try { | |
| appliedPresets = getAppliedPresets(sandboxName); | |
| } catch (err) { | |
| const reason = err && err.message ? err.message : String(err); | |
| console.error( | |
| ` Warning: could not read applied presets; continuing rebuild with session presets only (${reason}).`, | |
| ); | |
| log(`Applied preset lookup failed: ${reason}`); | |
| return; | |
| } | |
| log(`Applied presets before rebuild: ${appliedPresets.join(", ") || "(none)"`); | |
| if (appliedPresets.length === 0) return; | |
| try { | |
| sessionMod.updateSession((s) => { | |
| const sessionPresets = Array.isArray(s.policyPresets) ? s.policyPresets : []; | |
| s.policyPresets = [...new Set([...sessionPresets, ...appliedPresets])]; | |
| return s; | |
| }); | |
| log(`Session policyPresets updated: ${sessionMod.loadSession()?.policyPresets?.join(", ") || "(none)"}`); | |
| } catch (err) { | |
| const reason = err && err.message ? err.message : String(err); | |
| console.error( | |
| ` Warning: could not merge applied presets into session; continuing rebuild with session presets only (${reason}).`, | |
| ); | |
| log(`Session policyPresets merge failed: ${reason}`); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/policies.ts` around lines 488 - 506, The try/catch around both
getAppliedPresets and the session merge masks failures from
sessionMod.updateSession/loadSession; narrow the scope so only
getAppliedPresets(sandboxName) is inside the try and any error from that logs
the current console.error/log message, then perform
sessionMod.updateSession(...) and sessionMod.loadSession() outside that catch
(or in a separate try/catch) so failures in sessionMod.updateSession or
sessionMod.loadSession produce their own error messages and aren’t misreported
as a preset-read failure; update the log statements to reflect which operation
failed and reference getAppliedPresets, sessionMod.updateSession, and
sessionMod.loadSession in the changes.
Summary
nemoclaw <name> rebuild --yes, policy presets added viapolicy-add(e.g.telegram) were lost because they only existed in the registry's sandbox entry, not in the onboard session. The sandbox deletion wiped them before the resume path could replay them.sandboxRebuild()now reads applied presets from the registry before deletion and merges them (with dedup viaSet) into the session'spolicyPresets, so the onboard resume path replays the full set.getAppliedPresetsfails, rebuild falls back to session presets only (logged warning, no abort).Test plan
policy-add telegram,rebuild --yes— verify Telegram bridge egress works after rebuild🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests