Skip to content

refactor(cli): extract upgrade sandboxes action - #2897

Merged
cv merged 9 commits into
mainfrom
refactor/oclif-extract-upgrade-sandboxes-action
May 4, 2026
Merged

refactor(cli): extract upgrade sandboxes action#2897
cv merged 9 commits into
mainfrom
refactor/oclif-extract-upgrade-sandboxes-action

Conversation

@cv

@cv cv commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Extract upgrade-sandboxes orchestration from src/nemoclaw.ts into a dedicated action module. This removes the final command action from the transitional runtime bridge while preserving stale-version detection and rebuild-loop behavior.

Stack Navigation

Changes

  • Added src/lib/upgrade-sandboxes-action.ts for stale/unknown version classification and optional rebuild orchestration.
  • Updated src/lib/global-cli-actions.ts to call the extracted upgrade action.
  • Removed upgradeSandboxes from src/nemoclaw.ts and emptied the NemoClawRuntimeBridge action surface.
  • Removed the remaining runtime-bridge dependency from src/lib/sandbox-runtime-actions.ts.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Verification

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • make docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Carlos Villela cvillela@nvidia.com

Summary by CodeRabbit

  • Refactor
    • Internal reorganization of the sandbox upgrade system to improve code modularity and maintainability. The upgrade functionality, including sandbox stale detection and batch rebuild operations with --check, --auto, and --yes flag support, remains available as before.

@cv cv self-assigned this May 3, 2026
@copy-pr-bot

copy-pr-bot Bot commented May 3, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c1a2a325-326e-41fe-9006-bbc03aa2c15f

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf1958 and b387696.

📒 Files selected for processing (2)
  • src/lib/sandbox-runtime-actions.ts
  • src/nemoclaw.ts
💤 Files with no reviewable changes (2)
  • src/nemoclaw.ts
  • src/lib/sandbox-runtime-actions.ts

📝 Walkthrough

Walkthrough

This PR refactors the CLI sandbox runtime bridge by extracting action implementations from the centralized NemoClawRuntimeBridge interface into dedicated action modules. The bridge interface is emptied, and all call sites now dynamically require their respective action modules instead of delegating through the bridge.

Changes

Sandbox Action Module Extraction

Layer / File(s) Summary
Interface Simplification
src/lib/nemoclaw-runtime-bridge.ts
NemoClawRuntimeBridge is changed from a 7-method interface to an empty interface, removing method signatures for sandbox operations.
Action Module Creation
src/lib/upgrade-sandboxes-action.ts
New module exports upgradeSandboxes(args?: string[]) to scan registered/running sandboxes, classify them as stale or unknown, display status, support --check mode, and rebuild with optional confirmation.
Facade Rewiring
src/lib/sandbox-runtime-actions.ts
All sandbox action facades (connectSandbox, showSandboxStatus, showSandboxLogs, destroySandbox, rebuildSandbox, installSandboxSkill, runSandboxSnapshot) now dynamically require() their corresponding action modules at call time; SandboxConnectOptions import source moved to ./sandbox-connect-action.
Global CLI Action Update
src/lib/global-cli-actions.ts
runUpgradeSandboxesAction now dynamically requires ./upgrade-sandboxes-action and calls its exported upgradeSandboxes function instead of delegating to the runtime bridge.
Source Cleanup
src/nemoclaw.ts
Removed upgradeSandboxes function implementation, deleted runtimeBridge export, and cleaned up related imports (askPrompt, sandboxRebuild, sandboxVersion).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • NVIDIA/NemoClaw#2892: Applies the same refactoring pattern to extract sandbox actions into standalone modules and remove methods from NemoClawRuntimeBridge.
  • NVIDIA/NemoClaw#2896: Related refactor of runtime bridge wiring and sandbox operation extraction into dedicated action modules, affecting the same interface and call-site patterns.

Poem

🐰 The bridge once held all paths so grand,
Now action modules span the land,
Each sandbox walks its own bright way,
Dynamic calls save the day! ✨
Refactored clean, the code runs free,
A rabbit's dream of clarity! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main refactoring: extracting the upgrade sandboxes action into a dedicated module, which is the primary objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/oclif-extract-upgrade-sandboxes-action

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@cv cv added the v0.0.34 label May 4, 2026
cv added a commit that referenced this pull request May 4, 2026
## Summary
Extract sandbox rebuild orchestration from `src/nemoclaw.ts` into a
dedicated action module. This removes `sandboxRebuild` from the
transitional runtime bridge while preserving rebuild preflight, backup,
recreate, restore, policy preset restore, and post-upgrade checks.

## Stack Navigation
- Position: 7 of 60
- Previous PR: [#2895 — refactor(cli): extract sandbox destroy
action](#2895)
- Next PR: [#2897 — refactor(cli): extract upgrade sandboxes
action](#2897)

## Changes
- Added `src/lib/sandbox-rebuild-action.ts` for rebuild confirmation,
credential preflight, backup, delete/recreate, restore, policy preset
replay, and post-restore checks.
- Updated `src/lib/sandbox-runtime-actions.ts` to call the extracted
rebuild action.
- Updated `upgrade-sandboxes` to call the extracted rebuild action
directly.
- Removed `sandboxRebuild` from `src/nemoclaw.ts` and
`NemoClawRuntimeBridge`.

## Type of Change
- [x] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Verification
- [x] `npx prek run --all-files` passes
- [x] `npm test` passes
- [ ] Tests added or updated for new or changed behavior
- [x] No secrets, API keys, or credentials committed
- [ ] Docs updated for user-facing behavior changes
- [ ] `make docs` builds without warnings (doc changes only)
- [ ] 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)

---
Signed-off-by: Carlos Villela <cvillela@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Safer, user-facing sandbox rebuild flow with interactive
confirmations, SSH-session warnings, credential preflight, workspace
backup/restore, and clear completion/recovery messages.

* **Refactor**
* Moved sandbox rebuild responsibilities to a dedicated action; runtime
bridge no longer exposes a direct rebuild method.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Co-authored-by: Prekshi Vyas <34834085+prekshivyas@users.noreply.github.com>
@cv
cv changed the base branch from refactor/oclif-extract-sandbox-rebuild-action to main May 4, 2026 20:18
@cv
cv marked this pull request as ready for review May 4, 2026 20:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/lib/sandbox-doctor-action.ts (1)

429-430: 🏗️ Heavy lift

Break runSandboxDoctor into smaller diagnostic phases.

Line 429 is already suppressing complexity on a brand-new command entrypoint. Please extract the host, gateway, sandbox, inference, and local-service sections into helpers so runSandboxDoctor stays orchestration-only. As per coding guidelines, "Keep function complexity low; existing complexity hotspots are tracked separately".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sandbox-doctor-action.ts` around lines 429 - 430, The
runSandboxDoctor function is doing too much; extract each diagnostic section
into small helper functions so the top-level function becomes
orchestration-only. Create helpers like diagnoseHost(), diagnoseGateway(),
diagnoseSandbox(), diagnoseInference(), and diagnoseLocalService() and move the
corresponding logic (host checks, gateway checks, sandbox checks, inference
checks, local service checks) from runSandboxDoctor into those helpers; have
runSandboxDoctor simply call these helpers in sequence, forward args/state as
needed, and preserve existing return/error behavior and logging (refer to
runSandboxDoctor and any local variables it uses to pass state into the new
helpers).
src/lib/sandbox-rebuild-action.ts (1)

35-506: 🏗️ Heavy lift

Split rebuildSandbox() into smaller phases before this grows further.

This extracted action still packs confirmation, credential preflight, backup/delete, recreate rollback handling, restore, preset replay, doctor, and registry updates into one long function. Pulling those into phase helpers would make the destructive paths much easier to reason about and test.

As per coding guidelines: "Keep function complexity low; existing complexity hotspots are tracked separately."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sandbox-rebuild-action.ts` around lines 35 - 506, rebuildSandbox() is
too large and should be split into clear phase helpers; extract logical sections
into smaller functions (e.g. confirmRebuildPrompt(),
preflightCredentialCheck(session, sandboxName),
ensureSandboxLiveAndBackup(sandboxName),
deleteSandboxAndPrepareRecreate(sandboxName, backupManifest, sbMeta),
runOnboardWithExitInterception(onboardArgs), restoreStateAndPresets(sandboxName,
backupManifest), postRestoreAgentMigration(sandboxName, agent, backupManifest)
and updateRegistryVersion(sandboxName, agentDef)) preserving existing behavior
(use the same bail handler, logging via log/_rebuildLog, and session updates).
Move the long try/catch/process.exit interception block into
runOnboardWithExitInterception so rollback and onboardFailed handling lives with
the recreate step, and have each helper return structured results (e.g.
backupManifest, restore result, onboardExitCode) so the top-level rebuildSandbox
coordinates phases and handles error paths unchanged.
src/lib/sandbox-status-action.ts (1)

37-38: 🏗️ Heavy lift

Please split showSandboxStatus instead of exempting it from the complexity rule.

The extracted action now owns registry rendering, gateway reconciliation, recovery guidance, and in-sandbox health checks in one function. Breaking those into small helpers will keep this module from becoming the next monolith and removes the need for a new inline complexity suppression. As per coding guidelines, "Keep function complexity low; existing complexity hotspots are tracked separately".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sandbox-status-action.ts` around lines 37 - 38, The function
showSandboxStatus is doing too many responsibilities (registry rendering,
gateway reconciliation, recovery guidance, and in-sandbox health checks);
instead of disabling complexity, extract each responsibility into small helper
functions (e.g., renderRegistryStatus(sandboxName),
reconcileGatewayForSandbox(sandboxName), provideRecoveryGuidance(sandboxName,
context), runInSandboxHealthChecks(sandboxName)) and call them from
showSandboxStatus, move any large inline blocks into those helpers, and then
remove the "// eslint-disable-next-line complexity" comment so the function's
cyclomatic complexity is reduced and each helper can be unit-tested
independently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/sandbox-doctor-cli-command.ts`:
- Around line 11-19: Declare an oclif argument instead of reading this.argv: add
a static args = { sandboxName: Args.string({ required: true }) } on the
SandboxDoctor command and change static usage to use oclif templates (e.g. "<%=
config.bin %> <%= command.id %> [--json]"). In run(), call
this.parse(SandboxDoctor) and pull sandboxName from the parsed args (do not
destructure this.argv), and forward that sandboxName (and any flags) to
runSandboxDoctor; ensure you reference the class name SandboxDoctor, the static
args, and the runSandboxDoctor invocation when making these edits.

In `@src/lib/sandbox-process-recovery-action.ts`:
- Around line 238-245: ensureSandboxPortForward currently ignores the result of
the "forward start" call so callers may report success even if OpenShell failed;
modify ensureSandboxPortForward to capture the return/result (or thrown error)
from runOpenshell(["forward","start",...]) and only consider the port-forward
restored when that call succeeds (e.g., return true/false or rethrow on
failure), and ensure callers use that boolean/exception to decide whether to
print "Dashboard port forward re-established". Update references to runOpenshell
in ensureSandboxPortForward and adjust its signature/return value or error
behavior so success is reliably indicated.
- Around line 50-52: The tmpFile name created with path.join(os.tmpdir(),
`nemoclaw-ssh-${process.pid}-${Date.now()}.conf`) is predictable and
fs.writeFileSync follows existing symlinks, causing a race/symlink attack;
change the implementation in sandbox-process-recovery-action.ts so you create an
exclusive temp artifact (either create a dedicated random temp directory via
fs.mkdtempSync and write the config file inside it, or open the file with an
exclusive flag like fs.openSync/tmp file creation with flag 'wx' before writing)
instead of plain writeFileSync, then write sshConfigResult.output with mode
0o600 and ensure you atomically close and remove the temp file/dir on cleanup or
error.

In `@src/lib/sandbox-rebuild-action.ts`:
- Around line 83-84: The post-rebuild logic still uses the pre-sync session
agent captured in agent = agentRuntime.getSessionAgent(sandboxName) and
agentName, which can cause wrong post-restore branches and agentVersion
persistence; update the code to derive post-restore decisions from the agent
returned by rebuildAgent (or by reloading the sandbox metadata after await
onboard(...)) instead of the earlier agent variable—replace uses of agent and
agentName in the Step 6/7 block (and the other occurrences around the commented
ranges) with the rebuilt agent/its display name (or freshly loaded sandbox
metadata) so the final status text and agentVersion are set from the recreated
sandbox.

In `@src/lib/sandbox-skill-install-action.ts`:
- Around line 176-180: The current write of tmpSshConfig using a predictable
path and fs.writeFileSync is vulnerable to symlink/race attacks; instead create
a private temporary directory (e.g., using fs.mkdtemp with os.tmpdir() and a
unique prefix based on process.pid/Date.now()), write the SSH config file inside
that directory using fs.writeFileSync or fs.promises.writeFile with the flag
"wx" and mode 0o600 to ensure exclusive creation, and then remove the file and
directory in a finally block; apply the same pattern to the other SSH-config
temp write in this file that uses sshConfigResult.output so both uses are
created exclusively and cleaned up.

In `@src/lib/skill-install.ts`:
- Around line 148-151: The JSDoc claims sshExec() uses the same SSH flags as
executeSandboxCommand(), but sshExec() currently sets ConnectTimeout=10 while
executeSandboxCommand() sets ConnectTimeout=5; update code so both helpers use
the same SSH options (e.g., unify ConnectTimeout to 5 or 10) or change the
comment to say they use similar SSH settings. Locate sshExec and
executeSandboxCommand and either align their SSH flag values (including
ConnectTimeout) or soften the JSDoc to avoid asserting exact parity.

---

Nitpick comments:
In `@src/lib/sandbox-doctor-action.ts`:
- Around line 429-430: The runSandboxDoctor function is doing too much; extract
each diagnostic section into small helper functions so the top-level function
becomes orchestration-only. Create helpers like diagnoseHost(),
diagnoseGateway(), diagnoseSandbox(), diagnoseInference(), and
diagnoseLocalService() and move the corresponding logic (host checks, gateway
checks, sandbox checks, inference checks, local service checks) from
runSandboxDoctor into those helpers; have runSandboxDoctor simply call these
helpers in sequence, forward args/state as needed, and preserve existing
return/error behavior and logging (refer to runSandboxDoctor and any local
variables it uses to pass state into the new helpers).

In `@src/lib/sandbox-rebuild-action.ts`:
- Around line 35-506: rebuildSandbox() is too large and should be split into
clear phase helpers; extract logical sections into smaller functions (e.g.
confirmRebuildPrompt(), preflightCredentialCheck(session, sandboxName),
ensureSandboxLiveAndBackup(sandboxName),
deleteSandboxAndPrepareRecreate(sandboxName, backupManifest, sbMeta),
runOnboardWithExitInterception(onboardArgs), restoreStateAndPresets(sandboxName,
backupManifest), postRestoreAgentMigration(sandboxName, agent, backupManifest)
and updateRegistryVersion(sandboxName, agentDef)) preserving existing behavior
(use the same bail handler, logging via log/_rebuildLog, and session updates).
Move the long try/catch/process.exit interception block into
runOnboardWithExitInterception so rollback and onboardFailed handling lives with
the recreate step, and have each helper return structured results (e.g.
backupManifest, restore result, onboardExitCode) so the top-level rebuildSandbox
coordinates phases and handles error paths unchanged.

In `@src/lib/sandbox-status-action.ts`:
- Around line 37-38: The function showSandboxStatus is doing too many
responsibilities (registry rendering, gateway reconciliation, recovery guidance,
and in-sandbox health checks); instead of disabling complexity, extract each
responsibility into small helper functions (e.g.,
renderRegistryStatus(sandboxName), reconcileGatewayForSandbox(sandboxName),
provideRecoveryGuidance(sandboxName, context),
runInSandboxHealthChecks(sandboxName)) and call them from showSandboxStatus,
move any large inline blocks into those helpers, and then remove the "//
eslint-disable-next-line complexity" comment so the function's cyclomatic
complexity is reduced and each helper can be unit-tested independently.
🪄 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: Enterprise

Run ID: 564dcf19-e814-4e23-9a1b-32bc2df75d69

📥 Commits

Reviewing files that changed from the base of the PR and between 92f4cd3 and 8bf1958.

📒 Files selected for processing (21)
  • src/lib/global-cli-actions.ts
  • src/lib/legacy-oclif-dispatch.test.ts
  • src/lib/legacy-oclif-dispatch.ts
  • src/lib/nemoclaw-runtime-bridge.ts
  • src/lib/oclif-commands.ts
  • src/lib/sandbox-connect-action.ts
  • src/lib/sandbox-destroy-action.ts
  • src/lib/sandbox-doctor-action.ts
  • src/lib/sandbox-doctor-cli-command.ts
  • src/lib/sandbox-gateway-state-action.ts
  • src/lib/sandbox-process-recovery-action.ts
  • src/lib/sandbox-rebuild-action.ts
  • src/lib/sandbox-runtime-actions.ts
  • src/lib/sandbox-skill-install-action.ts
  • src/lib/sandbox-status-action.ts
  • src/lib/share-command-deps.ts
  • src/lib/skill-install.ts
  • src/lib/terminal-style.ts
  • src/lib/upgrade-sandboxes-action.ts
  • src/nemoclaw.ts
  • test/image-cleanup.test.ts

Comment on lines +11 to +19
static id = "sandbox:doctor";
static strict = false;
static summary = "Diagnose sandbox and gateway health";
static description = "Run host, gateway, sandbox, inference, messaging, and local service diagnostics.";
static usage = ["<name> doctor [--json]"];

public async run(): Promise<void> {
const [sandboxName, ...actionArgs] = this.argv;
await runSandboxDoctor(sandboxName, actionArgs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In @oclif/core 4.10.5, what is the recommended way to declare a required positional argument for a Command, and how should the usage/help text be defined for a command with id "sandbox:doctor"?

💡 Result:

Recommended way to declare a required positional argument (in @oclif/core 4.10.5) Use the static args property on your Command class and mark the positional argument as required with required: true on the Args.* helper, e.g.: class SandboxDoctor extends Command { static args = { // required positional argument commandId: Args.string({ required: true, name: 'command-id', description: 'ID of the command to diagnose', }), } async run { const {args} = await this.parse(SandboxDoctor) // args.commandId } } The key points are: positional arguments are declared under static args, and “required positional argument” is expressed via Args.({... required: true ...}). How to define usage/help text for a command with id "sandbox:doctor" Define either (a) static description / static summary (and/or static examples), and/or (b) override the help “USAGE” line with static usage. If you want the usage line tailored to the command id, use the oclif template <%= command.id %>. For example: export class DoctorSandbox extends Command { static summary = 'Health checks and quick fixes for the sandbox' static description = 'Run health checks and apply safe repair steps for sandbox mode.' // Overrides the default usage line in --help static usage = '<%= config.bin %> <%= command.id %> [--repair]' static examples = [ '<%= config.bin %> <%= command.id %> --help', '<%= config.bin %> <%= command.id %> --repair', ] static args = { // example required positional argument target: Args.string({ required: true, description: 'Resource to check' }), } async run {} } With id "sandbox:doctor", <%= command.id %> resolves to sandbox:doctor in the rendered help/usage text.

Citations:


Declare sandboxName as a required oclif argument instead of manually parsing this.argv.

The current code destructures this.argv directly (line 18), which bypasses oclif's argument validation. This causes:

  • sandbox:doctor --json to treat --json as the sandbox name instead of a flag
  • sandbox:doctor (no args) to pass undefined as the sandbox name
  • The usage string (line 15) to advertise the legacy <name> doctor syntax instead of the registered command id

Use static args with Args.string({ required: true }) and parse via this.parse(SandboxDoctor). Update the usage string to use oclif templates (e.g., <%= config.bin %> <%= command.id %> [--json]) to match the actual command surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sandbox-doctor-cli-command.ts` around lines 11 - 19, Declare an oclif
argument instead of reading this.argv: add a static args = { sandboxName:
Args.string({ required: true }) } on the SandboxDoctor command and change static
usage to use oclif templates (e.g. "<%= config.bin %> <%= command.id %>
[--json]"). In run(), call this.parse(SandboxDoctor) and pull sandboxName from
the parsed args (do not destructure this.argv), and forward that sandboxName
(and any flags) to runSandboxDoctor; ensure you reference the class name
SandboxDoctor, the static args, and the runSandboxDoctor invocation when making
these edits.

Comment on lines +50 to +52
const tmpFile = path.join(os.tmpdir(), `nemoclaw-ssh-${process.pid}-${Date.now()}.conf`);
fs.writeFileSync(tmpFile, sshConfigResult.output, { mode: 0o600 });
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use an exclusive temp file for the SSH config.

This filename is predictable, and writeFileSync will follow a pre-created symlink in /tmp, so a local user can race this into overwriting another file before the SSH call runs. Create a dedicated random temp directory or open the file with an exclusive flag (wx) before writing.

Proposed hardening
-  const tmpFile = path.join(os.tmpdir(), `nemoclaw-ssh-${process.pid}-${Date.now()}.conf`);
-  fs.writeFileSync(tmpFile, sshConfigResult.output, { mode: 0o600 });
+  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-ssh-"));
+  const tmpFile = path.join(tmpDir, "config");
+  fs.writeFileSync(tmpFile, sshConfigResult.output, { mode: 0o600, flag: "wx" });
   try {
     const result = spawnSync(
       "ssh",
@@
   } finally {
     try {
-      fs.unlinkSync(tmpFile);
+      fs.rmSync(tmpDir, { recursive: true, force: true });
     } catch {
       /* ignore */
     }
   }
📝 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.

Suggested change
const tmpFile = path.join(os.tmpdir(), `nemoclaw-ssh-${process.pid}-${Date.now()}.conf`);
fs.writeFileSync(tmpFile, sshConfigResult.output, { mode: 0o600 });
try {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-ssh-"));
const tmpFile = path.join(tmpDir, "config");
fs.writeFileSync(tmpFile, sshConfigResult.output, { mode: 0o600, flag: "wx" });
try {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sandbox-process-recovery-action.ts` around lines 50 - 52, The tmpFile
name created with path.join(os.tmpdir(),
`nemoclaw-ssh-${process.pid}-${Date.now()}.conf`) is predictable and
fs.writeFileSync follows existing symlinks, causing a race/symlink attack;
change the implementation in sandbox-process-recovery-action.ts so you create an
exclusive temp artifact (either create a dedicated random temp directory via
fs.mkdtempSync and write the config file inside it, or open the file with an
exclusive flag like fs.openSync/tmp file creation with flag 'wx' before writing)
instead of plain writeFileSync, then write sshConfigResult.output with mode
0o600 and ensure you atomically close and remove the temp file/dir on cleanup or
error.

Comment thread src/lib/sandbox-process-recovery-action.ts Outdated
Comment thread src/lib/sandbox-rebuild-action.ts
Comment on lines +176 to +180
const tmpSshConfig = path.join(
os.tmpdir(),
`nemoclaw-ssh-skill-${process.pid}-${Date.now()}.conf`,
);
fs.writeFileSync(tmpSshConfig, sshConfigResult.output, { mode: 0o600 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use an exclusive temp file for the SSH config.

This path is predictable in /tmp, and writeFileSync() will follow a pre-existing file or symlink. That makes this flow vulnerable to local file clobbering and attacker-controlled SSH config reuse. Create a private temp dir and write the config with flag: "wx" before removing that directory in finally.

🔒 Suggested hardening
-  const tmpSshConfig = path.join(
-    os.tmpdir(),
-    `nemoclaw-ssh-skill-${process.pid}-${Date.now()}.conf`,
-  );
-  fs.writeFileSync(tmpSshConfig, sshConfigResult.output, { mode: 0o600 });
+  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-ssh-skill-"));
+  const tmpSshConfig = path.join(tmpDir, "ssh.conf");
+  fs.writeFileSync(tmpSshConfig, sshConfigResult.output, {
+    mode: 0o600,
+    flag: "wx",
+  });
…
   } finally {
     try {
-      fs.unlinkSync(tmpSshConfig);
+      fs.rmSync(tmpDir, { recursive: true, force: true });
     } catch {
       /* ignore */
     }
   }

Also applies to: 217-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sandbox-skill-install-action.ts` around lines 176 - 180, The current
write of tmpSshConfig using a predictable path and fs.writeFileSync is
vulnerable to symlink/race attacks; instead create a private temporary directory
(e.g., using fs.mkdtemp with os.tmpdir() and a unique prefix based on
process.pid/Date.now()), write the SSH config file inside that directory using
fs.writeFileSync or fs.promises.writeFile with the flag "wx" and mode 0o600 to
ensure exclusive creation, and then remove the file and directory in a finally
block; apply the same pattern to the other SSH-config temp write in this file
that uses sshConfigResult.output so both uses are created exclusively and
cleaned up.

Comment thread src/lib/skill-install.ts
Comment on lines 148 to 151
/**
* Run a command on the sandbox via SSH with optional stdin content.
* Uses the same SSH flags as executeSandboxCommand in nemoclaw.ts.
* Uses the same SSH flags as executeSandboxCommand in sandbox-process-recovery-action.ts.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The new JSDoc is already out of sync with the implementation.

sshExec() still uses ConnectTimeout=10, while executeSandboxCommand() uses ConnectTimeout=5, so “same SSH flags” is false today. Either align the options or soften the comment to say the helpers use similar SSH invocation settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/skill-install.ts` around lines 148 - 151, The JSDoc claims sshExec()
uses the same SSH flags as executeSandboxCommand(), but sshExec() currently sets
ConnectTimeout=10 while executeSandboxCommand() sets ConnectTimeout=5; update
code so both helpers use the same SSH options (e.g., unify ConnectTimeout to 5
or 10) or change the comment to say they use similar SSH settings. Locate
sshExec and executeSandboxCommand and either align their SSH flag values
(including ConnectTimeout) or soften the JSDoc to avoid asserting exact parity.

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
@cv
cv enabled auto-merge (squash) May 4, 2026 20:41

@prekshivyas prekshivyas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Clean extraction of upgradeSandboxes into src/lib/upgrade-sandboxes-action.ts. Public surface preserved (--check/--auto/--yes, all output strings byte-identical, exit codes unchanged). The literal / chars vs prior / escapes are identical bytes — no behavior change.

Strong cleanup this round:

  • NemoClawRuntimeBridge is now empty (final action removed); next PR (#2898) drops the bridge entirely.
  • The sandboxRebuild alias #2896 left in nemoclaw.ts for upgradeSandboxes is gone now that its consumer moved.
  • sandboxVersion, captureOpenshell, parseLiveSandboxNames, askPrompt imports also removed.
  • sandbox-runtime-actions.ts drops its last getNemoClawRuntimeBridge import.

Zero new orphans introduced — the cumulative ~16 orphans + 2 stale comments in src/nemoclaw.ts remain pending the separate cleanup PR.

CI: first fully-green hash in the stack. pr.yaml + pr-self-hosted (build-sandbox-images, build-sandbox-images-arm64, test-e2e-sandbox, test-e2e-gateway-isolation, test-e2e-port-overrides, test-e2e-ollama-proxy) all SUCCESS.

@cv
cv merged commit b832004 into main May 4, 2026
17 checks passed
cv added a commit that referenced this pull request May 4, 2026
## Summary
Remove the now-empty transitional NemoClaw runtime bridge from the oclif
CLI layer. Credentials tests now inject gateway/OpenShell hooks directly
through `global-cli-actions` instead of stubbing `../nemoclaw`.

## Stack Navigation
- Position: 9 of 60
- Previous PR: [#2897 — refactor(cli): extract upgrade sandboxes
action](#2897)
- Next PR: [#2899 — refactor(cli): remove legacy dispatch
fallbacks](#2899)

## Changes
- Deleted `src/lib/nemoclaw-runtime-bridge.ts`.
- Added explicit test/runtime hook injection to
`src/lib/global-cli-actions.ts` for credential command gateway recovery
and provider operations.
- Updated credentials CLI tests to use the explicit global action hooks
instead of stubbing `dist/nemoclaw.js`.
- Verified no `src/lib` code imports or requires `../nemoclaw`.

## Type of Change
- [x] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Verification
- [x] `npx prek run --all-files` passes
- [x] `npm test` passes
- [x] Tests added or updated for new or changed behavior
- [x] No secrets, API keys, or credentials committed
- [ ] Docs updated for user-facing behavior changes
- [ ] `make docs` builds without warnings (doc changes only)
- [ ] 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)

---
Signed-off-by: Carlos Villela <cvillela@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Refactored internal runtime architecture to streamline dependencies
and enhance code modularity.

* **Tests**
* Enhanced test infrastructure with improved runtime mocking mechanism
for better test isolation and control.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
@cv
cv deleted the refactor/oclif-extract-upgrade-sandboxes-action branch May 27, 2026 21:18
@wscurran wscurran added the refactor PR restructures code without intended behavior change label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor PR restructures code without intended behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants