Skip to content

refactor(cli): make runner helpers argv-only by default - #2584

Merged
cv merged 5 commits into
mainfrom
refactor/run-argv-only-shell-helpers
Apr 28, 2026
Merged

refactor(cli): make runner helpers argv-only by default#2584
cv merged 5 commits into
mainfrom
refactor/run-argv-only-shell-helpers

Conversation

@cv

@cv cv commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR makes run() and runInteractive() argv-only so shell parsing is no longer implicit in the default runner APIs. It adds explicit shell-only helpers for the few cases that still need bash -c, and migrates existing callers to either argv execution or those explicit shell boundaries.

Changes

  • Make src/lib/runner.ts reject string input for run() and runInteractive(), and add explicit runShell() / runInteractiveShell() helpers.
  • Convert deploy, onboard, and CLI gateway-cleanup paths to argv execution where shell parsing is not required, including ssh, scp, rsync, docker inspect, docker volume, and kill calls.
  • Keep the true shell cases explicit in onboarding (backgrounded Ollama startup and curl | sh) and update runner/CLI/onboard tests to cover the new contracts.

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)

AI Disclosure

  • AI-assisted — tool: pi coding agent

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

Summary by CodeRabbit

  • Refactor

    • Hardened command execution by moving from shell-interpolated strings to structured argv-style invocations, and introduced explicit shell-only execution paths for commands that need shell features, improving reliability and security.
    • Safer Docker volume cleanup via explicit enumeration and bulk deletion to avoid fragile shell pipelines.
  • Tests

    • Updated and tightened tests to enforce argv usage, validate shell-only helpers, and reflect the new command-invocation patterns.

@cv cv added the security label Apr 28, 2026
@cv cv self-assigned this Apr 28, 2026
@coderabbitai

coderabbitai Bot commented Apr 28, 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: e139ef62-399c-486e-a2c3-fbc034ae6a25

📥 Commits

Reviewing files that changed from the base of the PR and between 4678520 and 81f37cd.

📒 Files selected for processing (3)
  • src/lib/onboard.ts
  • src/nemoclaw.ts
  • test/cli.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/nemoclaw.ts
  • test/cli.test.ts
  • src/lib/onboard.ts

📝 Walkthrough

Walkthrough

Replaces shell-interpolated command strings with structured argv arrays across runner APIs and call sites; adds explicit shell entry points (runShell, runInteractiveShell); updates deploy/onboard flows, Docker volume cleanup, and tests to use tokenized command invocation.

Changes

Cohort / File(s) Summary
Runner core & API
src/lib/runner.ts
run/runInteractive now accept only readonly string[]; added runShell/runInteractiveShell for string-based shell execution; runArrayCmd extended with caller-specific error messages and stdio/default handling.
Deploy surface
src/lib/deploy.ts
DeployExecutionOptions signatures changed: run/runInteractive now take argv arrays; all deploy invocations converted from interpolated shell strings to structured argv calls (e.g., brev create/refresh, ssh, rsync, scp, interactive ssh -t).
Onboard & volume cleanup
src/lib/onboard.ts, src/nemoclaw.ts
Introduced removeGatewayClusterVolumes(); replaced shell-pipeline volume cleanup with runCapture("docker", "volume", "ls", "-q", --filter...) + run(["docker","volume","rm", ...]); moved shell-only operations to runShell; converted docker/kill/inspect calls to argv form.
Tests — runner & argv behavior
src/lib/runner-argv.test.ts, test/runner.test.ts
Tests updated to require argv-array usage for run/runInteractive; string inputs now rejected; added coverage for runShell/runInteractiveShell; adjusted deploy-related assertions to tokenized args.
Tests — onboard & selection
test/onboard-selection.test.ts
Mocks extended to stub runner.runShell (in addition to run/runCapture) and record shell commands for assertions (e.g., Ollama install path).
Tests — gateway, CLI & cleanup
test/gateway-cleanup.test.ts, test/gateway-liveness-probe.test.ts, test/cli.test.ts
Assertions updated to expect tokenized Docker argument lists (volume ls -q --filter, inspect --type container tokens), check for removeGatewayClusterVolumes usage, and stub docker in PATH for destroy tests.

Sequence Diagram(s)

sequenceDiagram
  participant Dev as Developer/CLI
  participant Runner as Local Runner
  participant LocalFS as Local FS / rsync/scp
  participant Remote as Remote Host (ssh)
  participant Docker as Docker Engine

  Dev->>Runner: start deploy (argv array)
  Runner->>LocalFS: run rsync/scp (argv)
  LocalFS-->>Runner: transfer result
  Runner->>Remote: ssh (argv) run remote setup/install
  Remote->>Docker: manage volumes/services (argv docker ...)
  Remote-->>Runner: exit status
  Runner-->>Dev: report result
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I swapped my strings for tidy argv rows,
hopping through tokens where cleaner logic flows.
runShell sings softly for the bash-bound part,
while arrays march on—precise, brave, and smart.
carrots of clarity, crunchy in charts.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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 PR title clearly summarizes the primary change: converting runner helper functions to accept argv-only input by default, which is the central refactoring objective across multiple files.
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/run-argv-only-shell-helpers

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/deploy.ts (1)

414-444: ⚠️ Potential issue | 🔴 Critical

Remove shellQuote() from rsync and scp remote targets.

The run() function uses spawnSync() without shell interpretation—arguments are passed directly to the executable as argv. When shellQuote() is applied to remote paths like ${name}:${shellQuote(...)}, the single quotes become literal characters in the argument, causing rsync and scp to fail when trying to access paths with embedded quotes.

Keep shell quoting only in ssh command strings (lines 413, 444, 464, 478, 502), where commands are executed by the remote shell.

Suggested fix
-      `${name}:${shellQuote(`${remoteDir}/`)}`,
+      `${name}:${remoteDir}/`,
@@
-      run(["scp", "-q", ...sshArgs, envTmp, `${name}:${shellQuote(`${remoteDir}/.env`)}`]);
+      run(["scp", "-q", ...sshArgs, envTmp, `${name}:${remoteDir}/.env`]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/deploy.ts` around lines 414 - 444, The rsync/scp remote target
arguments are being wrapped with shellQuote (e.g.,
`${name}:${shellQuote(`${remoteDir}/`)}` and
`${name}:${shellQuote(`${remoteDir}/.env`)}`) even though run() invokes commands
via spawnSync without a shell, which makes the single-quote characters literal
and breaks rsync/scp; update the call sites that build remote targets (the rsync
invocation and the scp invocation that uses envTmp) to pass unquoted remote
target strings (combine name + ":" + remoteDir/remote file path directly) and
leave shellQuote only where commands are sent to the remote shell via ssh (the
ssh run() calls), ensuring run(["rsync", ... , `${name}:${remoteDir}/`]) and
run(["scp", ... , `${name}:${remoteDir}/.env`]) style arguments instead of using
shellQuote.
🤖 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 173-182: The docker volume name filter is substring-based and may
match unrelated volumes; update removeGatewayClusterVolumes to compute the
prefix const prefix = `openshell-cluster-${NEMOCLAW_GATEWAY_NAME}`, then after
obtaining names from _runCapture only keep those that start with that prefix
(use startsWith) before calling run(["docker","volume","rm",...]). Keep the
existing _runCapture and run calls but add the prefix-based filter and an early
return when the filtered list is empty to avoid deleting non-prefixed volumes.

---

Outside diff comments:
In `@src/lib/deploy.ts`:
- Around line 414-444: The rsync/scp remote target arguments are being wrapped
with shellQuote (e.g., `${name}:${shellQuote(`${remoteDir}/`)}` and
`${name}:${shellQuote(`${remoteDir}/.env`)}`) even though run() invokes commands
via spawnSync without a shell, which makes the single-quote characters literal
and breaks rsync/scp; update the call sites that build remote targets (the rsync
invocation and the scp invocation that uses envTmp) to pass unquoted remote
target strings (combine name + ":" + remoteDir/remote file path directly) and
leave shellQuote only where commands are sent to the remote shell via ssh (the
ssh run() calls), ensuring run(["rsync", ... , `${name}:${remoteDir}/`]) and
run(["scp", ... , `${name}:${remoteDir}/.env`]) style arguments instead of using
shellQuote.
🪄 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: 02801d94-8f6a-4f98-bb4f-20ffcabdaf3b

📥 Commits

Reviewing files that changed from the base of the PR and between 0b49851 and 50962aa.

📒 Files selected for processing (10)
  • src/lib/deploy.ts
  • src/lib/onboard.ts
  • src/lib/runner-argv.test.ts
  • src/lib/runner.ts
  • src/nemoclaw.ts
  • test/cli.test.ts
  • test/gateway-cleanup.test.ts
  • test/gateway-liveness-probe.test.ts
  • test/onboard-selection.test.ts
  • test/runner.test.ts

Comment thread src/nemoclaw.ts
@cv cv added the v0.0.29 label Apr 28, 2026
@cv
cv requested a review from ericksoa April 28, 2026 14:05

@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.

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)

4605-4614: ⚠️ Potential issue | 🟠 Major

WSL install-ollama path is inconsistent with the WSL-safe startup logic.

Line [4605] always binds Ollama to 0.0.0.0 and the block always starts the auth proxy, but the regular Ollama path explicitly avoids that on WSL. This can break reachability on WSL installs and applies the wrong post-install flow.

💡 Proposed fix
-        runShell(`OLLAMA_HOST=0.0.0.0:${OLLAMA_PORT} ollama serve > /dev/null 2>&1 &`, {
+        const installOllamaEnv = isWsl() ? "" : `OLLAMA_HOST=0.0.0.0:${OLLAMA_PORT} `;
+        runShell(`${installOllamaEnv}ollama serve > /dev/null 2>&1 &`, {
           ignoreError: true,
         });
         sleep(2);
-        if (!startOllamaAuthProxy()) {
-          process.exit(1);
-        }
-        console.log(
-          `  ✓ Using Ollama on localhost:${OLLAMA_PORT} (proxy on :${OLLAMA_PROXY_PORT})`,
-        );
+        if (isWsl()) {
+          console.log(`  ✓ Using Ollama on localhost:${OLLAMA_PORT}`);
+        } else {
+          if (!startOllamaAuthProxy()) {
+            process.exit(1);
+          }
+          console.log(
+            `  ✓ Using Ollama on localhost:${OLLAMA_PORT} (proxy on :${OLLAMA_PROXY_PORT})`,
+          );
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 4605 - 4614, The code always binds Ollama to
0.0.0.0 and always calls startOllamaAuthProxy() which is inconsistent with the
WSL-safe path; change the runShell invocation and proxy startup to be
conditional on the WSL check used elsewhere (e.g., use the same isWSL or isWsl
detection), so that when running on WSL you bind to localhost (127.0.0.1) and
skip startOllamaAuthProxy(), otherwise bind to 0.0.0.0:${OLLAMA_PORT} and start
the proxy; update the console message to reflect the correct host/port and only
reference ${OLLAMA_PROXY_PORT} when the proxy was actually started.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/lib/onboard.ts`:
- Around line 4605-4614: The code always binds Ollama to 0.0.0.0 and always
calls startOllamaAuthProxy() which is inconsistent with the WSL-safe path;
change the runShell invocation and proxy startup to be conditional on the WSL
check used elsewhere (e.g., use the same isWSL or isWsl detection), so that when
running on WSL you bind to localhost (127.0.0.1) and skip
startOllamaAuthProxy(), otherwise bind to 0.0.0.0:${OLLAMA_PORT} and start the
proxy; update the console message to reflect the correct host/port and only
reference ${OLLAMA_PROXY_PORT} when the proxy was actually started.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 32b117d0-b088-4934-982f-e8bb4938f5ff

📥 Commits

Reviewing files that changed from the base of the PR and between 58bfc8e and 4678520.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

@brandonpelfrey

Copy link
Copy Markdown
Collaborator

Automated PR review summary

Reviewed PR #2584: refactor(cli): make runner helpers argv-only by default

Recommendation

  • Recommendation: PASS
  • Highest observed severity: low
  • Block merge: no
  • Why: The highest-risk downside if merged would be incomplete shell-hardening that still allowed implicit shell parsing or metacharacter injection. The direct runtime probes I ran did not find such a bypass in the installed build, and the remaining explicit shell entry points behaved as intentionally opt-in.
  • Reviewer summary: Reviewed PR refactor(cli): make runner helpers argv-only by default #2584 with PR-specific adversarial probes against the installed build and real OpenShell sandbox context. The default runner APIs rejected shell strings as claimed, explicit shell helpers preserved shell-only behavior, and a deploy-focused injection probe did not execute metacharacter payloads during exercised argv paths.

Installation and setup findings

  • Things went well: Installed the locally checked out NemoClaw via the repo root installer entrypoint with local source overrides, then verified the NemoClaw-managed sandbox was usable over OpenShell SSH and that in-sandbox OpenClaw inference worked.

What was validated

  • The PR revision was checked out in an isolated review environment.
  • The local checkout was installed using the repository installer flow as closely as the environment allowed.
  • Adversarial, PR-specific probes were then run against the installed environment and relevant repository context.
  • Diff summary:
 src/lib/deploy.ts                   | 64 +++++++++++++++++++++++++------------
 src/lib/onboard.ts                  | 41 +++++++++++++++---------
 src/lib/runner-argv.test.ts         | 40 +++++++++++++++++++++--
 src/lib/runner.ts                   | 56 ++++++++++++++++++++++----------
 src/nemoclaw.ts                     | 17 +++++++---
 test/cli.test.ts                    | 18 +++++------
 test/gateway-cleanup.test.ts        |  4 ++-
 test/gateway-liveness-probe.test.ts |  2 +-
 test/onboard-selection.test.ts      |  3 ++
 test/runner.test.ts                 | 20 ++++++------
 10 files changed, 185 insertions(+), 80 deletions(-)

Failing tests and unresolved impact

  • No failing adversarial tests were captured.

Passing tests and why they mattered

Passing test 1: Runner rejects implicit shell strings while explicit shell helpers still work

  • What was tested: run() and runInteractive() now reject string commands by default, while runShell() and runInteractiveShell() preserve intentional shell parsing.
  • Why it mattered: If false, the PR would leave a silent shell-injection surface or break legitimate shell-only call sites despite claiming safer defaults.
  • Observed result: Installed dist/lib/runner.js threw argv-only errors for string inputs to run and runInteractive; runShell('printf ok | tr o O') returned status 0 with stdout Ok; runInteractiveShell('printf shellok') returned status 0 with stdout shellok.
  • Command: node /tmp/pr2584_test1.js
  • Recommended follow-up coverage: Yes—keep direct runner regression tests covering rejected string inputs and explicit shell helper behavior, because this is the central contract of the PR.

Passing test 2: Deploy argv migration resists metacharacter injection in exercised command construction

  • What was tested: Refactored deploy execution passes untrusted values as argv arrays rather than through shell parsing, so shell metacharacters in instance/provider/GPU values do not execute.
  • Why it mattered: If false, attacker-controlled deploy names or environment knobs could achieve host command injection during deployment.
  • Observed result: A stubbed call into installed dist/lib/deploy.js used metacharacter-laden instanceName, NEMOCLAW_BREV_PROVIDER, and NEMOCLAW_GPU. The probe exited via deploy validation flow, but /tmp/pr2584_injected and /tmp/pr2584_injected_gpu were both absent, showing the exercised path did not execute injected shell fragments.
  • Command: bash /tmp/pr2584_injection_probe.sh
  • Recommended follow-up coverage: Yes—add an integration/regression test asserting metacharacter-bearing values remain inert when routed through deploy runner calls.

Passing test 3: Real OpenShell sandbox remained reachable for end-to-end review context

  • What was tested: The installed environment still supports sandbox-backed runtime probing after the refactor, enabling PR-relevant end-to-end verification rather than source-only review.
  • Why it mattered: If false, important runtime security and behavior claims in this area would be harder to validate before merge.
  • Observed result: Connected successfully to sandbox nmclaw-local-install; /sandbox/.openclaw/workspace existed and openclaw agent was present inside the sandbox. This provided real OpenShell context for the review, though this test itself was supporting evidence rather than the main PR claim.
  • Command: ssh -F /tmp/sshcfg openshell-nmclaw-local-install 'ls -d /sandbox/.openclaw/workspace && openclaw agent --help >/dev/null; echo $?'
  • Recommended follow-up coverage: No automated unit test needed; this was an end-to-end environment confirmation used to support the adversarial review.

Bottom line

  • Based on the install evidence and adversarial probes, this PR looks reasonable to approve.

@cv
cv merged commit 4876d24 into main Apr 28, 2026
17 of 18 checks passed
DemianHeyGen pushed a commit to DemianHeyGen/NemoClaw that referenced this pull request Apr 30, 2026
## Summary
This PR makes `run()` and `runInteractive()` argv-only so shell parsing
is no longer implicit in the default runner APIs. It adds explicit
shell-only helpers for the few cases that still need `bash -c`, and
migrates existing callers to either argv execution or those explicit
shell boundaries.

## Changes
- Make `src/lib/runner.ts` reject string input for `run()` and
`runInteractive()`, and add explicit `runShell()` /
`runInteractiveShell()` helpers.
- Convert deploy, onboard, and CLI gateway-cleanup paths to argv
execution where shell parsing is not required, including `ssh`, `scp`,
`rsync`, `docker inspect`, `docker volume`, and `kill` calls.
- Keep the true shell cases explicit in onboarding (backgrounded Ollama
startup and `curl | sh`) and update runner/CLI/onboard tests to cover
the new contracts.

## 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)

## AI Disclosure
- [x] AI-assisted — tool: pi coding agent

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

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

* **Refactor**
* Hardened command execution by moving from shell-interpolated strings
to structured argv-style invocations, and introduced explicit shell-only
execution paths for commands that need shell features, improving
reliability and security.
* Safer Docker volume cleanup via explicit enumeration and bulk deletion
to avoid fragile shell pipelines.

* **Tests**
* Updated and tightened tests to enforce argv usage, validate shell-only
helpers, and reflect the new command-invocation patterns.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@cv
cv deleted the refactor/run-argv-only-shell-helpers 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