fix(destroy): clean up orphaned sandbox Docker images#2176
Conversation
Add imageTag field to SandboxEntry to track which openshell/sandbox-from:<timestamp> image belongs to each sandbox. Remove the image during destroy, rebuild, and onboard recreate flows. Add `nemoclaw gc` command to find and remove orphaned images not associated with any registered sandbox. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix removeSandboxImage to only log success when docker rmi succeeds (the try/catch was dead code since run() with ignoreError never throws). Reuse previousEntry in onboard recreate path instead of a redundant registry lookup. Fix test regex for sandboxRebuild body extraction. Add nemoclaw gc to docs/reference/commands.md.
📝 WalkthroughWalkthroughRecords sandbox build image tags during onboarding, removes host-side sandbox images when destroying or rebuilding sandboxes, and adds a new Changes
Sequence DiagramsequenceDiagram
actor User
participant CLI as Nemoclaw CLI
participant Registry as Sandbox Registry
participant Docker as Docker Daemon
User->>CLI: nemoclaw gc
CLI->>Docker: docker images openshell/sandbox-from:*
Docker-->>CLI: host image list
CLI->>Registry: listSandboxes()
Registry-->>CLI: sandboxes with imageTag
CLI->>CLI: compute orphaned = hostTags - registry.imageTags
CLI->>User: show orphaned (or --dry-run report)
alt user confirms or --yes/--force
CLI->>Docker: docker rmi <orphaned_tag>
Docker-->>CLI: removal result
end
CLI-->>User: report success/failure counts
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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: 4
🧹 Nitpick comments (2)
test/image-cleanup.test.ts (2)
92-125: Add at least one behavior-level test fornemoclaw gc.These assertions only verify source text, so they will still pass if the Docker parsing, prompt flow, or exit-status behavior regresses. A small unit around
garbageCollectImages()with mockedspawnSync,registry.listSandboxes(), andaskPrompt()would protect the actual contract.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/image-cleanup.test.ts` around lines 92 - 125, Add a behavior-level unit test that invokes the async function garbageCollectImages with controlled mocks instead of only asserting source text: stub child_process.spawnSync (or the module symbol used) to capture docker calls, stub registry.listSandboxes to return a set of in-use tags, and stub askPrompt to simulate user responses for both dry-run and --yes flows; then assert that garbageCollectImages queries the registry, calls spawnSync to remove only images not returned by registry.listSandboxes (or only logs them in dry-run), and that the prompt short-circuits deletions when the user declines. Target the function garbageCollectImages and the symbols spawnSync / registry.listSandboxes / askPrompt in your test so the behavior (deletion calls, dry-run, and prompt/yes handling) is verified rather than just source presence.
4-6: Remove the GitHub issue URL from the source comment.Please replace this with a plain issue ID/reference or local context. As per coding guidelines "Do not add links to third-party code repositories, community collections, or unofficial resources."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/image-cleanup.test.ts` around lines 4 - 6, The top-of-file comment that ends with the GitHub URL should not include external links; update the comment following "Verify that sandbox lifecycle operations clean up host-side Docker images." to remove the full GitHub URL and replace it with a plain issue reference or local context (e.g., "See issue `#2086`" or "internal ticket NEMO-2086") so the comment remains informative without an external link.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.agents/skills/nemoclaw-user-reference/references/commands.md:
- Line 207: The change was made to a generated skill file
(.agents/skills/nemoclaw-user-reference/references/commands.md) which will be
overwritten; instead, update the canonical documentation under docs/ (edit the
source doc that contains the command description) and then regenerate the skill
files using the provided script: run python3 scripts/docs-to-skills.py docs/
.agents/skills/ --prefix nemoclaw-user to propagate your edits to
.agents/skills/nemoclaw-user-*/*.md (apply the same fix for the other affected
ranges noted).
In `@docs/reference/commands.md`:
- Around line 522-529: Update the `nemoclaw gc` synopsis to include the
`--force` alias so it matches the flag table; change the usage line that
currently reads "$ nemoclaw gc [--dry-run] [--yes]" to include `--force` (e.g.
"$ nemoclaw gc [--dry-run] [--yes|--force]" or list both flags) so the CLI
reference and flag table are consistent.
In `@src/lib/onboard.ts`:
- Around line 3401-3405: The current recreate path calls run(["docker", "rmi",
previousEntry.imageTag], { ignoreError: true }) which can silently fail and
leave an orphaned image while the registry entry is removed; change this to
attempt image removal without ignoreError (or capture and check the run
result/throw) and only proceed with removing/updating the registry entry if the
docker rmi succeeds; specifically, update the block around runOpenshell("sandbox
delete", ...) and run(... "docker rmi" ...) to call run/docker rmi non-ignored,
check for error/exit code (or catch the thrown error) and on failure log the
error and abort/return so the registry entry is not removed, using the existing
symbols sandboxName, previousEntry.imageTag, runOpenshell and run to locate and
implement the check.
In `@src/nemoclaw.ts`:
- Around line 2704-2724: The cleanup loop over orphans uses spawnSync("docker",
["rmi", img.tag"], ...) and tallies removed and failed but always lets the
process exit zero; modify the end of the gc routine to exit non-zero when any
removal failed: after the loop that updates removed and failed (variables
removed, failed and the for..of loop over orphans), call process.exit(1) (or
throw an Error) when failed > 0 so the command returns a non-zero status; keep
existing log messages but ensure success still exits 0 by only exiting non-zero
on failed > 0.
---
Nitpick comments:
In `@test/image-cleanup.test.ts`:
- Around line 92-125: Add a behavior-level unit test that invokes the async
function garbageCollectImages with controlled mocks instead of only asserting
source text: stub child_process.spawnSync (or the module symbol used) to capture
docker calls, stub registry.listSandboxes to return a set of in-use tags, and
stub askPrompt to simulate user responses for both dry-run and --yes flows; then
assert that garbageCollectImages queries the registry, calls spawnSync to remove
only images not returned by registry.listSandboxes (or only logs them in
dry-run), and that the prompt short-circuits deletions when the user declines.
Target the function garbageCollectImages and the symbols spawnSync /
registry.listSandboxes / askPrompt in your test so the behavior (deletion calls,
dry-run, and prompt/yes handling) is verified rather than just source presence.
- Around line 4-6: The top-of-file comment that ends with the GitHub URL should
not include external links; update the comment following "Verify that sandbox
lifecycle operations clean up host-side Docker images." to remove the full
GitHub URL and replace it with a plain issue reference or local context (e.g.,
"See issue `#2086`" or "internal ticket NEMO-2086") so the comment remains
informative without an external link.
🪄 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: ea4bc554-6213-44b9-8e89-bfb1d3e8b2b3
📒 Files selected for processing (7)
.agents/skills/nemoclaw-user-reference/references/commands.mddocs/reference/commands.mdsrc/lib/onboard.tssrc/lib/registry.tssrc/nemoclaw.tstest/image-cleanup.test.tstest/registry.test.ts
- Include --force alias in nemoclaw gc synopsis for consistency - Warn when docker rmi fails in onboard recreate path - Exit non-zero from nemoclaw gc when any image removal fails - Include docker stderr in gc failure messages
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/onboard.ts (1)
3762-3774:⚠️ Potential issue | 🟠 MajorLegacy reused sandboxes still look orphaned to the new GC flow.
imageTagis only written on freshregisterSandbox()calls. The reuse branch returns earlier, so older registry entries keepimageTag: null. On an upgraded host,nemoclaw gcwill misclassify those still-liveopenshell/sandbox-from:*images as orphaned and hit deterministicdocker rmifailures, which now makegcexit non-zero. Please backfillimageTagfor reused sandboxes, or make GC exclude images referenced by live sandboxes before relying on registry association alone.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/onboard.ts` around lines 3762 - 3774, The reuse branch currently returns before setting imageTag, leaving legacy registry entries with imageTag:null and causing GC to misidentify live images as orphaned; modify the reuse path (the code that skips calling registry.registerSandbox when reusing sandboxes) to backfill the sandbox's imageTag field to `openshell/sandbox-from:${buildId}` (or update the existing registry entry) so that registry entries for reused sandboxes get imageTag populated; look for the reuse-return logic near registry.registerSandbox and ensure the same buildId-based imageTag is written for reused sandboxes before returning.
🤖 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/onboard.ts`:
- Around line 3598-3604: The code generates a synthetic buildId and calls
patchStagedDockerfile(stagedDockerfile, model, chatUiUrl, buildId, provider) but
then persists an image tag regardless of whether the staged Dockerfile actually
consumed the ARG; modify onboard.ts to verify that patchStagedDockerfile
performed the replacement (e.g., have patchStagedDockerfile return a boolean or
the patched content and then check for "ARG NEMOCLAW_BUILD_ID" or the buildId
string) and only compute/persist imageTag (openshell/sandbox-from:${buildId})
after confirmation; if the replacement did not occur, throw/fail fast and do not
record the synthetic tag (apply the same check around the other occurrence
referenced at lines ~3762-3770).
In `@src/nemoclaw.ts`:
- Around line 2791-2793: The help text for the "nemoclaw gc" synopsis is missing
the --force alias; update the Cleanup/help block where "nemoclaw gc
Remove orphaned sandbox Docker images (--yes, --dry-run)" is defined to list
both "--yes" and "--force" (e.g., "(- -yes, --force, --dry-run)") so the help
output matches the parser that accepts --force; modify the string/constant that
renders the Cleanup section for the "nemoclaw gc" command accordingly (search
for the Cleanup block or the help-generation routine that contains "nemoclaw gc"
to locate the exact symbol to edit).
- Around line 1981-1988: The helper removeSandboxImage currently swallows docker
rmi failures; update it to surface failures by checking the result when
run(["docker","rmi", sb.imageTag"], { ignoreError: true }) returns a non-zero
status and emit a clear warning (e.g., via console.warn or processLogger.warn)
that includes the sandboxName, sb.imageTag and any error output from the result
(stdout/stderr) so callers of removeSandboxImage (and flows like
destroy/rebuild) can see why image cleanup failed; keep the early-return when
!sb?.imageTag and retain the success log when status === 0.
---
Outside diff comments:
In `@src/lib/onboard.ts`:
- Around line 3762-3774: The reuse branch currently returns before setting
imageTag, leaving legacy registry entries with imageTag:null and causing GC to
misidentify live images as orphaned; modify the reuse path (the code that skips
calling registry.registerSandbox when reusing sandboxes) to backfill the
sandbox's imageTag field to `openshell/sandbox-from:${buildId}` (or update the
existing registry entry) so that registry entries for reused sandboxes get
imageTag populated; look for the reuse-return logic near
registry.registerSandbox and ensure the same buildId-based imageTag is written
for reused sandboxes before returning.
🪄 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: f9040e8c-8152-49fb-bf4d-b64dd1eb8127
📒 Files selected for processing (4)
.agents/skills/nemoclaw-user-reference/references/commands.mddocs/reference/commands.mdsrc/lib/onboard.tssrc/nemoclaw.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/reference/commands.md
| const buildId = String(Date.now()); | ||
| patchStagedDockerfile( | ||
| stagedDockerfile, | ||
| model, | ||
| chatUiUrl, | ||
| String(Date.now()), | ||
| buildId, | ||
| provider, |
There was a problem hiding this comment.
Don't persist a synthetic image tag unless the staged Dockerfile actually consumed buildId.
The --from path accepts arbitrary Dockerfiles, and patchStagedDockerfile() only does regex replacement. If a custom Dockerfile does not declare ARG NEMOCLAW_BUILD_ID=..., this code still records openshell/sandbox-from:${buildId} even though that tag was never produced. Destroy/gc will then target the wrong image and leak the real one. Please fail fast when that replacement does not happen, or only store imageTag after confirming the staged Dockerfile contains the expected build-id arg.
Also applies to: 3762-3770
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/onboard.ts` around lines 3598 - 3604, The code generates a synthetic
buildId and calls patchStagedDockerfile(stagedDockerfile, model, chatUiUrl,
buildId, provider) but then persists an image tag regardless of whether the
staged Dockerfile actually consumed the ARG; modify onboard.ts to verify that
patchStagedDockerfile performed the replacement (e.g., have
patchStagedDockerfile return a boolean or the patched content and then check for
"ARG NEMOCLAW_BUILD_ID" or the buildId string) and only compute/persist imageTag
(openshell/sandbox-from:${buildId}) after confirmation; if the replacement did
not occur, throw/fail fast and do not record the synthetic tag (apply the same
check around the other occurrence referenced at lines ~3762-3770).
There was a problem hiding this comment.
Dismissing — the image tag openshell/sandbox-from:${buildId} is assigned by OpenShell during sandbox create, not by the Dockerfile itself. The NEMOCLAW_BUILD_ID ARG is a cache-busting mechanism; whether or not a custom --from Dockerfile declares it, OpenShell still tags the built image with the same naming convention. The recorded imageTag will match what Docker has on disk regardless of the Dockerfile content.
There was a problem hiding this comment.
@ericksoa, thanks for the clarification — that makes sense. Since OpenShell assigns the openshell/sandbox-from:<buildId> tag as part of its sandbox create convention (independent of the Dockerfile ARG), the recorded imageTag will always correspond to the actual Docker image on disk. My concern about the --from path was based on a wrong assumption about who controls the naming. Noted.
✏️ Learnings added
Learnt from: ericksoa
Repo: NVIDIA/NemoClaw PR: 2176
File: src/lib/onboard.ts:3598-3604
Timestamp: 2026-04-21T13:06:45.715Z
Learning: In `src/lib/onboard.ts` (NemoClaw, TypeScript), the image tag `openshell/sandbox-from:${buildId}` is assigned by OpenShell during `sandbox create`, not derived from the Dockerfile's `ARG NEMOCLAW_BUILD_ID`. The `NEMOCLAW_BUILD_ID` ARG is solely a Docker layer cache-busting mechanism. The recorded `imageTag` in the sandbox registry will always match what Docker has on disk regardless of whether a custom `--from` Dockerfile declares the ARG. Do not flag the imageTag persistence as unsafe when the Dockerfile doesn't contain `ARG NEMOCLAW_BUILD_ID`.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: KimBioInfoStudio
Repo: NVIDIA/NemoClaw PR: 0
File: :0-0
Timestamp: 2026-04-21T04:56:00.062Z
Learning: In `test/policies.test.ts` (NemoClaw, TypeScript), the `NEMOCLAW_OPENSHELL_BIN` environment variable set-then-`delete` pattern around `buildPolicySetCommand` tests is pre-existing on `main` and not introduced by any single PR. Do not flag missing test hermeticity (e.g., missing `afterEach`/`beforeEach` teardown) in that test block as a new issue in PRs that only reformat assertions in that area.
Learnt from: laitingsheng
Repo: NVIDIA/NemoClaw PR: 2139
File: src/nemoclaw.ts:1669-1746
Timestamp: 2026-04-21T04:21:52.946Z
Learning: In `src/nemoclaw.ts` (NemoClaw, TypeScript), sandbox-scoped action handlers such as `sandboxChannelsAdd`, `sandboxChannelsRemove`, `sandboxPolicyAdd`, and `sandboxPolicyRemove` intentionally do not validate unknown flags or extra positional arguments. They follow a shared convention of using `args.find((arg) => !arg.startsWith("-"))` to extract the first positional arg, mirroring the `policy add`/`policy remove` pattern. Do not flag missing strict arg validation in these handlers as an issue.
Learnt from: KimBioInfoStudio
Repo: NVIDIA/NemoClaw PR: 0
File: :0-0
Timestamp: 2026-04-21T04:56:00.062Z
Learning: In `src/nemoclaw.ts` and `src/lib/policies.ts` (NemoClaw, TypeScript), the `--dry-run` flag for policy commands intentionally previews only the endpoints that would be opened/removed without rendering a merged-policy diff. This contract is shared between `policy-add` and `policy-remove` for consistency. Any enhancement to show a merged-policy diff should be done in a dedicated follow-up that updates both commands together. Do not flag the absence of a merged-policy diff under `--dry-run` as an issue.
Surface docker rmi failures in removeSandboxImage() so destroy/rebuild flows show a warning with a nemoclaw gc hint. Add --force alias to the gc help text to match the parser and docs.
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/nemoclaw.ts`:
- Around line 2641-2654: The failure branch in garbageCollectImages currently
logs a generic error when spawnSync for Docker images fails; update that branch
to include imagesResult.stderr (and imagesResult.stdout if present) in the
process output so daemon/permission/path errors are visible—locate the
imagesResult variable in garbageCollectImages and change the console.error call
to print a descriptive message plus imagesResult.stderr and imagesResult.stdout
(or their fallbacks) before calling process.exit(1).
🪄 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: 5d04861a-6b12-47e5-b52d-70df19bf0091
📒 Files selected for processing (1)
src/nemoclaw.ts
| async function garbageCollectImages(args = []) { | ||
| const dryRun = args.includes("--dry-run"); | ||
| const skipConfirm = args.includes("--yes") || args.includes("--force"); | ||
|
|
||
| // 1. List all openshell/sandbox-from images on the host | ||
| const imagesResult = spawnSync( | ||
| "docker", | ||
| ["images", "--filter", "reference=openshell/sandbox-from", "--format", "{{.Repository}}:{{.Tag}}\t{{.Size}}"], | ||
| { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }, | ||
| ); | ||
| if (imagesResult.status !== 0) { | ||
| console.error(" Failed to query Docker images. Is Docker running?"); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
Include Docker error details on image discovery failure.
At Line 2651, the failure path prints a generic message only. Include stderr/stdout to make daemon/permission/path issues actionable.
Suggested patch
if (imagesResult.status !== 0) {
- console.error(" Failed to query Docker images. Is Docker running?");
- process.exit(1);
+ const details = `${imagesResult.stderr || imagesResult.stdout || ""}`.trim();
+ console.error(
+ ` Failed to query Docker images. Is Docker running?${details ? `: ${details}` : ""}`,
+ );
+ process.exit(imagesResult.status || 1);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/nemoclaw.ts` around lines 2641 - 2654, The failure branch in
garbageCollectImages currently logs a generic error when spawnSync for Docker
images fails; update that branch to include imagesResult.stderr (and
imagesResult.stdout if present) in the process output so daemon/permission/path
errors are visible—locate the imagesResult variable in garbageCollectImages and
change the console.error call to print a descriptive message plus
imagesResult.stderr and imagesResult.stdout (or their fallbacks) before calling
process.exit(1).
## Summary Bumps the published doc version to `0.0.22` and documents the user-visible CLI behavior changes to `nemoclaw <name> connect` that landed since v0.0.21. Drafted via the `nemoclaw-contributor-update-docs` skill against commits in `v0.0.21..origin/main`, filtered through `docs/.docs-skip`. ## Changes - **`docs/project.json`** and **`docs/versions1.json`**: bump the published version from `0.0.20` to `0.0.22`; insert a `0.0.21` entry into the version list so the history stays contiguous. - **`docs/reference/commands.md`** → `nemoclaw <name> connect`: document two new behaviors. - Readiness poll with `NEMOCLAW_CONNECT_TIMEOUT` (integer seconds; default `120`) that replaces the silent hang when the sandbox is not yet `Ready` — right after onboarding, while the 2.4 GB image is still pulling (#466). - Post-connect hint is now agent-aware, names the correct TUI command for the sandbox's agent, and tells you to use `/exit` to leave the chat before `exit` returns you to the host shell (#2080). Feature PRs that shipped their own docs in the same commit are intentionally not re-documented here: - `channels list/add/remove` (#2139) — command reference and the "`openclaw channels` blocked inside the sandbox" troubleshooting entry landed with the feature. - `nemoclaw gc` (#2176) — documented as part of the destroy/rebuild image cleanup PR. Skipped per `docs/.docs-skip`: - `e6bad533 fix(shields): verify config lock and fail hard on re-lock failure (#2066)` — matched `skip-features: src/lib/shields.ts`. Other commits in the range (#2141 OpenShell version bump, #1819 plugin banner live inference probe, #2085 / #2146 Slack Socket Mode fixes, #2110 axios proxy fix, #1818 NIM curl timeouts, #1824 onboard gateway bootstrap recovery, and assorted CI / test / install plumbing) are internal behavior refinements with no doc-relevant surface change. ## Type of Change - [ ] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [x] Doc only (includes code sample changes) ## Verification - [x] `npx prek run --all-files` passes for the modified files via the pre-commit hook, including `Regenerate agent skills from docs` (source ↔ generated parity confirmed) - [ ] `npm test` passes — skipped; the one pre-existing `test/cli.test.ts > unknown command exits 1` failure on `origin/main` is unrelated to these markdown/JSON-only changes - [ ] Tests added or updated for new or changed behavior — n/a, doc-only - [x] No secrets, API keys, or credentials committed - [x] Docs updated for user-facing behavior changes - [ ] `make docs` builds without warnings (doc changes only) — not run locally - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) — n/a, no new pages ## AI Disclosure - [x] AI-assisted — tool: Claude Code --- Signed-off-by: Miyoung Choi <miyoungc@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * `connect` now displays the sandbox phase while waiting for readiness and honors a configurable timeout via NEMOCLAW_CONNECT_TIMEOUT (default 120s). * TTY hints are agent-aware and instruct using `/exit` before returning to the host shell. * **Documentation** * Command docs updated to describe polling, timeout, and TTY guidance. * Project/docs metadata updated for versions 0.0.21 and 0.0.22 (package version bumped to 0.0.22). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
imageTagfield onSandboxEntry) so they can be cleaned up ondestroyandrebuildnemoclaw gccommand to find and remove orphanedopenshell/sandbox-from:*images not associated with any registered sandboxremoveSandboxImage()to only report success whendocker rmiactually succeeds, and remove redundant registry lookup in the onboard recreate pathTest plan
test/image-cleanup.test.ts— 13 structural tests verifying cleanup wiring (all pass)test/registry.test.ts— 3 new tests forimageTagstorage, default null, andupdateSandbox(all pass)nemoclaw gc --dry-runon a host with orphaned imagesnemoclaw <name> destroyremoves the sandbox imageSummary by CodeRabbit
New Features
nemoclaw gcwith--dry-run,--yes, and--forceto discover and remove orphaned sandbox images.Improvements
nemoclaw <name> destroynow also removes the host-side Docker image produced during onboarding.nemoclaw <name> rebuildremoves the old sandbox image before recreating.Documentation
Tests
gccommand.