fix(test): isolate sysbin in install-preflight tests to prevent host PATH leakage - #1628
fix(test): isolate sysbin in install-preflight tests to prevent host PATH leakage#1628TSavo wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughReplaced the fixed Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 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 unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR hardens the install-preflight test environment by preventing the host machine’s /usr/bin/node (and related binaries) from leaking into the test PATH, ensuring the “node missing” / “npm missing” branches are exercised reliably across developer machines.
Changes:
- Replaces the hard-coded
TEST_SYSTEM_PATH = "/usr/bin:/bin"with a module-load helper that builds a temporary isolated bin directory. - Mirrors
/usr/binand/bininto the isolated directory via symlinks while excludingnode,npm, andnpx.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } catch { | ||
| // Ignore EEXIST when /bin is a symlink to /usr/bin (modern Linux) | ||
| // and we already linked the same name on the first pass. |
There was a problem hiding this comment.
The catch {} around fs.symlinkSync currently swallows all errors, but the comment says it's only meant to ignore EEXIST when /bin mirrors /usr/bin. This can hide real failures (e.g., EPERM, EINVAL, ENOENT) and lead to an incomplete isolated bin dir with confusing downstream test failures. Please capture the error and rethrow unless err.code === 'EEXIST' (or explicitly handle any other expected codes).
| } catch { | |
| // Ignore EEXIST when /bin is a symlink to /usr/bin (modern Linux) | |
| // and we already linked the same name on the first pass. | |
| } catch (err) { | |
| // Ignore EEXIST when /bin is a symlink to /usr/bin (modern Linux) | |
| // and we already linked the same name on the first pass. | |
| if (err.code !== "EEXIST") { | |
| throw err; | |
| } |
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 `@test/install-preflight.test.js`:
- Around line 44-49: The generic catch around fs.symlinkSync(path.join(sysDir,
name), path.join(dir, name)) hides all failures; change it to catch the error
object (e) and rethrow unless e.code === 'EEXIST' so only duplicate-entry
collisions are ignored—i.e., replace the bare catch with catch (e) { if (e.code
!== 'EEXIST') throw e } while leaving the existing comment about /bin →
/usr/bin.
🪄 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
Run ID: 6f808728-f704-4484-b8eb-73439599936a
📒 Files selected for processing (1)
test/install-preflight.test.js
5703f8a to
8b717d5
Compare
|
Force-pushed to add a Ready for a maintainer to approve workflow runs whenever you have a moment — first-time-contributor gate. |
…PATH leakage
The "node missing" / "npm missing" runtime preflight tests need a PATH
where the host's real `node` and `npm` are NOT visible, so the error
branches are actually exercised. The previous `TEST_SYSTEM_PATH = "/usr/bin:/bin"`
literal leaks `/usr/bin/node` on any Linux distribution that installs Node
via `apt install nodejs` (i.e. most of them). On those hosts the affected
tests assert the wrong code path — they expect "node missing" but the
preflight finds the system `/usr/bin/node` and reports a version mismatch
instead.
The tests pass on the upstream CI runners because Node is installed under
`/opt/hostedtoolcache/node/...` there (via `actions/setup-node`) rather
than `/usr/bin/`, so the leak is invisible in CI. Developers running
`npm test` locally on Ubuntu hit it consistently.
This replaces the literal with `buildIsolatedSystemPath()`, a small helper
that creates a tmpdir under `os.tmpdir()` at module load and symlinks every
entry from `/usr/bin` and `/bin` into it — except `node`, `npm`, and `npx`,
which are deliberately excluded. The result is a "system bin" that has
everything the tests legitimately need (`git`, `bash`, `sh`, `python`,
`grep`, etc.) but lacks the three binaries the runtime preflight is
supposed to detect as missing.
Tests that need a fake `node` or `npm` continue to work unchanged: they
write a stub into `fakeBin` and prepend it to PATH
(`${fakeBin}:${TEST_SYSTEM_PATH}`); the fake still wins because it comes
first. Tests that use `PATH: TEST_SYSTEM_PATH` directly (without
`fakeBin`) now correctly see neither node nor npm.
The `symlinkSync` catch block ONLY ignores `EEXIST` (the expected case
when /bin is a symlink to /usr/bin and we already linked the same name
on the first pass). Any other error (EPERM, EACCES, EINVAL, ENOENT…)
is re-thrown — swallowing them would leave TEST_SYSTEM_PATH partially
populated and turn into a confusing downstream test failure.
The tmpdir is intentionally not cleaned up — it's tiny (a few hundred
symlinks), the OS reaps it on reboot, and cleanup would require an
`afterAll` hook in every describe block in this file. The on-disk cost
is negligible.
Closes NVIDIA#1621 (sub-item 2 of three).
Signed-off-by: T Savo <evilgenius@nefariousplan.com>
8b717d5 to
9c02f6f
Compare
|
@Copilot @coderabbitai both right — the bare } catch (err) {
// Only swallow EEXIST — the expected case is when /bin is a symlink
// to /usr/bin (modern Linux) and we already linked the same name on
// the first pass. Any other error (EPERM, EACCES, EINVAL, ENOENT…)
// would leave TEST_SYSTEM_PATH partially populated and turn into a
// confusing downstream test failure, so re-throw it.
if (err && err.code === "EEXIST") continue;
throw err;
}The Diff is now +47 / −1 in one file. The 56-test suite still passes locally on a Linux host with |
|
Tip For best results, initiate chat on the files or code changes.
The comment block is clear and explains the rationale well. No further changes needed on this side. One small optional note: the temp dir is still not cleaned up after the test run (per the PR description). That's acceptable for CI since LGTM on the 🧠 Learnings used |
|
✨ Thanks for submitting this PR, which proposes a way to fix a test environment fragility issue by isolating the sysbin in install-preflight tests, preventing host PATH leakage and improving the reliability of the test suite. Possibly related open PRs: Possibly related open issues: |
|
I attempted to port this branch across the JS→TS migration and merge the latest Please start with: git fetch origin
git merge origin/main
npx tsx scripts/ts-migration-assist.ts --base origin/main --write
npm run build:cli
npm run typecheck:cli
npm run lint
npm test |
…1649) <!-- markdownlint-disable MD041 --> ## Summary The sandbox base image (`ghcr.io/nvidia/nemoclaw/sandbox-base`) is missing the `gnupg` package — `gpg --list-keys` (and any other gpg invocation) fails with `bash: gpg: command not found` inside the sandbox. This adds a single pinned `gnupg=2.2.40-1.1+deb12u2` line to the existing `apt-get install` block in `Dockerfile.base`, restoring the binary that the rest of the codebase already assumes is present. ## Related Issue Closes #1640. ## Changes `Dockerfile.base`: add `gnupg=2.2.40-1.1+deb12u2` to the existing `apt-get install` block, slotted right after `git`. Same `--no-install-recommends`, same cleanup tail, same `=<version>` pinning style as every other package in the block. ```diff curl=7.88.1-10+deb12u14 \ git=1:2.39.5-0+deb12u3 \ + gnupg=2.2.40-1.1+deb12u2 \ ca-certificates=20230311+deb12u1 \ ``` The pinned version is the bookworm-stable `2.2.40-1.1+deb12u2`, verified by `apt-cache madison gnupg` against the exact base image SHA `node:22-slim@sha256:4f77a690...`. The package brings in `dirmngr`, `gpg-wks-server`, and `gpg-wks-client` as dependencies. Total layer cost ~3 MB compressed. Diff: **+1 / 0** in 1 file. ### Why this is the right fix (and not "lower the env var" or "remove the test") The fix isn't obvious unless you trace where `GNUPGHOME` came from. Walking that chain: 1. **PR #1121** (`fix(sandbox): restrict /sandbox to read-only via Landlock (#804)`, authored by @prekshivyas, merged 2026-04-08) made the `/sandbox` home directory Landlock-read-only to prevent agents from modifying their own runtime environment. 2. To keep tools that normally write under `~/...` working (gpg, git config, python history, npm prefix, etc.), that PR redirected each tool's homedir to a writable `/tmp/...` path via env vars in `scripts/nemoclaw-start.sh`. The relevant line is at `scripts/nemoclaw-start.sh:53`: ```sh 'GNUPGHOME=/tmp/.gnupg' ``` alongside `HISTFILE=/tmp/.bash_history`, `GIT_CONFIG_GLOBAL=/tmp/.gitconfig`, `PYTHONUSERBASE=/tmp/.local`, etc. 3. PR #1121 also added three matching assertions in `test/service-env.test.js` (lines 177, 191, 347) verifying that the redirect is set: ```js expect(src).toContain("GNUPGHOME=/tmp/.gnupg"); ``` 4. **What PR #1121 didn't do**: add `gnupg` to the `apt-get install` list in `Dockerfile.base`. The env var setup landed and the test assertions landed, but the install line was missed. 5. CI never noticed because `service-env.test.js` only asserts that the env var is *set* in the source — it never spawns a subprocess that actually runs `gpg`. So a working test suite + a missing binary coexist silently. The QA report (this issue, #1640) catches it as a runtime failure on DGX Spark aarch64 because their test step does invoke `gpg --list-keys`. The clear intent of #1121 was to **enable** gpg under a redirected `GNUPGHOME` — you wouldn't redirect the homedir if you wanted gpg blocked. This PR is the matching install line that #1121 should have included, closing a one-line oversight rather than adding new capability or rolling anything back. ### Why not just remove the GNUPGHOME redirect The env var redirect from #1121 is doing real work — without it, any future `apt-get install gnupg` would still leave gpg unable to write to its homedir under Landlock-read-only `/sandbox`. The redirect is the "right" half of the pair; the install is the missing left half. ### Why this isn't a security regression The sandbox runs LLM-driven agents and gpg is a credential-handling tool, so it's worth justifying explicitly: - The redirected `GNUPGHOME=/tmp/.gnupg` is **fresh and empty** per session — no preloaded keys. - Without keys, gpg can hash/check signatures of public material but cannot decrypt or sign anything. - An agent would have to first import a key (which requires the user to provide it — keys are not pulled from anywhere automatically) before gpg becomes capable of any sensitive operation. - This is the same threat model as `git` and `curl`, which are already in the image and could equally be used to fetch arbitrary content. gpg adds no new capability that the existing toolchain doesn't already have. If the project explicitly *did* want gpg unavailable to agents, the right fix would be to remove the GNUPGHOME redirect from #1121 *and* the matching test assertions, not to keep the env wiring while leaving the binary missing — that's just confusing. ## Type of Change - [x] Code change for a new feature, bug fix, or refactor. - [ ] Code change with doc updates. - [ ] Doc only. Prose changes without code sample modifications. - [ ] Doc only. Includes code sample changes. ## Testing Smoke-tested locally by building `Dockerfile.base` with the fix and running the exact failing command from the bug report: ```sh $ docker build -f Dockerfile.base -t nemoclaw-base-test:gnupg . [...] => exporting to image 46.7s done $ docker run --rm nemoclaw-base-test:gnupg gpg --version gpg (GnuPG) 2.2.40 libgcrypt 1.10.1 $ docker run --rm nemoclaw-base-test:gnupg gpg --list-keys gpg: directory '/root/.gnupg' created gpg: keybox '/root/.gnupg/pubring.kbx' created gpg: /root/.gnupg/trustdb.gpg: trustdb created (exit 0) # And with the runtime-redirected GNUPGHOME from nemoclaw-start.sh: $ docker run --rm -e GNUPGHOME=/tmp/.gnupg nemoclaw-base-test:gnupg \ sh -c 'mkdir -p /tmp/.gnupg && chmod 700 /tmp/.gnupg && gpg --list-keys' gpg: keybox '/tmp/.gnupg/pubring.kbx' created (exit 0) ``` Both the default `~/.gnupg` and the runtime-redirected `/tmp/.gnupg` (matching what `nemoclaw-start.sh` exports) work as expected. The exact `gpg --list-keys` failure from the bug report no longer reproduces. - [x] `hadolint Dockerfile.base` — clean (no warnings) - [x] `docker build -f Dockerfile.base` — succeeds, exports to image cleanly - [x] `gpg --version` in built image — works (`gpg (GnuPG) 2.2.40`) - [x] `gpg --list-keys` in built image — works (was `bash: gpg: command not found` before this PR) - [x] `gpg --list-keys` with `GNUPGHOME=/tmp/.gnupg` — works (matches the runtime env from `nemoclaw-start.sh`) - [ ] `npx prek run --all-files` — partial: ran the affected hooks (commitlint, gitleaks, hadolint) which all pass; did NOT run `test-cli` against the full local suite because two pre-existing baseline failures on stock `main` get in the way on a WSL2 dev host (the `shouldPatchCoredns` issue addressed by PR #1626 (merged) and the install-preflight PATH leakage addressed by PR #1628 (open)). Upstream CI runs on Linux GHA runners and doesn't hit either of those, so it'll exercise the full suite normally. - [ ] `npm test` — same caveat as above, ran the relevant projects in isolation - [ ] `make docs` builds without warnings. (for doc-only changes — N/A) ## Checklist ### General - [x] I have read and followed the [contributing guide](https://github.com/NVIDIA/NemoClaw/blob/main/CONTRIBUTING.md). - [ ] I have read and followed the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md). (for doc-only changes — N/A) ### Code Changes - [x] Formatters applied — `hadolint Dockerfile.base` clean. No JS/TS/Python files touched. - [x] Tests added or updated for new or changed behavior — N/A. The existing `service-env.test.js` already asserts the `GNUPGHOME` redirect introduced in #1121; this PR makes the corresponding binary available so those assertions reflect a runtime that actually works. A new test that spawns `gpg` directly inside a container would arguably be worth a follow-up (it would have caught this gap originally), but it's a separate concern from this one-line install fix. - [x] No secrets, API keys, or credentials committed. - [ ] Doc pages updated for any user-facing behavior changes — N/A. The bug report describes the expected behavior; this PR just makes runtime match it. No docs claim gpg is unavailable. ### Doc Changes - N/A (no doc changes) --- Signed-off-by: T Savo <evilgenius@nefariousplan.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Base system image now includes GnuPG as a pinned OS package. * **Bug Fixes / Security** * GnuPG runtime directory is now created in a separate step with stricter permissions and sandbox ownership when applicable, reducing exposure. * **Tests** * Test suite updated to verify the new directory creation and permission/ownership behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: T Savo <evilgenius@nefariousplan.com> Co-authored-by: Carlos Villela <cvillela@nvidia.com> Co-authored-by: Prekshi Vyas <34834085+prekshivyas@users.noreply.github.com>
|
@TSavo can you also resolve conflicts pls ! |
…VIDIA#1649) <!-- markdownlint-disable MD041 --> ## Summary The sandbox base image (`ghcr.io/nvidia/nemoclaw/sandbox-base`) is missing the `gnupg` package — `gpg --list-keys` (and any other gpg invocation) fails with `bash: gpg: command not found` inside the sandbox. This adds a single pinned `gnupg=2.2.40-1.1+deb12u2` line to the existing `apt-get install` block in `Dockerfile.base`, restoring the binary that the rest of the codebase already assumes is present. ## Related Issue Closes NVIDIA#1640. ## Changes `Dockerfile.base`: add `gnupg=2.2.40-1.1+deb12u2` to the existing `apt-get install` block, slotted right after `git`. Same `--no-install-recommends`, same cleanup tail, same `=<version>` pinning style as every other package in the block. ```diff curl=7.88.1-10+deb12u14 \ git=1:2.39.5-0+deb12u3 \ + gnupg=2.2.40-1.1+deb12u2 \ ca-certificates=20230311+deb12u1 \ ``` The pinned version is the bookworm-stable `2.2.40-1.1+deb12u2`, verified by `apt-cache madison gnupg` against the exact base image SHA `node:22-slim@sha256:4f77a690...`. The package brings in `dirmngr`, `gpg-wks-server`, and `gpg-wks-client` as dependencies. Total layer cost ~3 MB compressed. Diff: **+1 / 0** in 1 file. ### Why this is the right fix (and not "lower the env var" or "remove the test") The fix isn't obvious unless you trace where `GNUPGHOME` came from. Walking that chain: 1. **PR NVIDIA#1121** (`fix(sandbox): restrict /sandbox to read-only via Landlock (NVIDIA#804)`, authored by @prekshivyas, merged 2026-04-08) made the `/sandbox` home directory Landlock-read-only to prevent agents from modifying their own runtime environment. 2. To keep tools that normally write under `~/...` working (gpg, git config, python history, npm prefix, etc.), that PR redirected each tool's homedir to a writable `/tmp/...` path via env vars in `scripts/nemoclaw-start.sh`. The relevant line is at `scripts/nemoclaw-start.sh:53`: ```sh 'GNUPGHOME=/tmp/.gnupg' ``` alongside `HISTFILE=/tmp/.bash_history`, `GIT_CONFIG_GLOBAL=/tmp/.gitconfig`, `PYTHONUSERBASE=/tmp/.local`, etc. 3. PR NVIDIA#1121 also added three matching assertions in `test/service-env.test.js` (lines 177, 191, 347) verifying that the redirect is set: ```js expect(src).toContain("GNUPGHOME=/tmp/.gnupg"); ``` 4. **What PR NVIDIA#1121 didn't do**: add `gnupg` to the `apt-get install` list in `Dockerfile.base`. The env var setup landed and the test assertions landed, but the install line was missed. 5. CI never noticed because `service-env.test.js` only asserts that the env var is *set* in the source — it never spawns a subprocess that actually runs `gpg`. So a working test suite + a missing binary coexist silently. The QA report (this issue, NVIDIA#1640) catches it as a runtime failure on DGX Spark aarch64 because their test step does invoke `gpg --list-keys`. The clear intent of NVIDIA#1121 was to **enable** gpg under a redirected `GNUPGHOME` — you wouldn't redirect the homedir if you wanted gpg blocked. This PR is the matching install line that NVIDIA#1121 should have included, closing a one-line oversight rather than adding new capability or rolling anything back. ### Why not just remove the GNUPGHOME redirect The env var redirect from NVIDIA#1121 is doing real work — without it, any future `apt-get install gnupg` would still leave gpg unable to write to its homedir under Landlock-read-only `/sandbox`. The redirect is the "right" half of the pair; the install is the missing left half. ### Why this isn't a security regression The sandbox runs LLM-driven agents and gpg is a credential-handling tool, so it's worth justifying explicitly: - The redirected `GNUPGHOME=/tmp/.gnupg` is **fresh and empty** per session — no preloaded keys. - Without keys, gpg can hash/check signatures of public material but cannot decrypt or sign anything. - An agent would have to first import a key (which requires the user to provide it — keys are not pulled from anywhere automatically) before gpg becomes capable of any sensitive operation. - This is the same threat model as `git` and `curl`, which are already in the image and could equally be used to fetch arbitrary content. gpg adds no new capability that the existing toolchain doesn't already have. If the project explicitly *did* want gpg unavailable to agents, the right fix would be to remove the GNUPGHOME redirect from NVIDIA#1121 *and* the matching test assertions, not to keep the env wiring while leaving the binary missing — that's just confusing. ## Type of Change - [x] Code change for a new feature, bug fix, or refactor. - [ ] Code change with doc updates. - [ ] Doc only. Prose changes without code sample modifications. - [ ] Doc only. Includes code sample changes. ## Testing Smoke-tested locally by building `Dockerfile.base` with the fix and running the exact failing command from the bug report: ```sh $ docker build -f Dockerfile.base -t nemoclaw-base-test:gnupg . [...] => exporting to image 46.7s done $ docker run --rm nemoclaw-base-test:gnupg gpg --version gpg (GnuPG) 2.2.40 libgcrypt 1.10.1 $ docker run --rm nemoclaw-base-test:gnupg gpg --list-keys gpg: directory '/root/.gnupg' created gpg: keybox '/root/.gnupg/pubring.kbx' created gpg: /root/.gnupg/trustdb.gpg: trustdb created (exit 0) # And with the runtime-redirected GNUPGHOME from nemoclaw-start.sh: $ docker run --rm -e GNUPGHOME=/tmp/.gnupg nemoclaw-base-test:gnupg \ sh -c 'mkdir -p /tmp/.gnupg && chmod 700 /tmp/.gnupg && gpg --list-keys' gpg: keybox '/tmp/.gnupg/pubring.kbx' created (exit 0) ``` Both the default `~/.gnupg` and the runtime-redirected `/tmp/.gnupg` (matching what `nemoclaw-start.sh` exports) work as expected. The exact `gpg --list-keys` failure from the bug report no longer reproduces. - [x] `hadolint Dockerfile.base` — clean (no warnings) - [x] `docker build -f Dockerfile.base` — succeeds, exports to image cleanly - [x] `gpg --version` in built image — works (`gpg (GnuPG) 2.2.40`) - [x] `gpg --list-keys` in built image — works (was `bash: gpg: command not found` before this PR) - [x] `gpg --list-keys` with `GNUPGHOME=/tmp/.gnupg` — works (matches the runtime env from `nemoclaw-start.sh`) - [ ] `npx prek run --all-files` — partial: ran the affected hooks (commitlint, gitleaks, hadolint) which all pass; did NOT run `test-cli` against the full local suite because two pre-existing baseline failures on stock `main` get in the way on a WSL2 dev host (the `shouldPatchCoredns` issue addressed by PR NVIDIA#1626 (merged) and the install-preflight PATH leakage addressed by PR NVIDIA#1628 (open)). Upstream CI runs on Linux GHA runners and doesn't hit either of those, so it'll exercise the full suite normally. - [ ] `npm test` — same caveat as above, ran the relevant projects in isolation - [ ] `make docs` builds without warnings. (for doc-only changes — N/A) ## Checklist ### General - [x] I have read and followed the [contributing guide](https://github.com/NVIDIA/NemoClaw/blob/main/CONTRIBUTING.md). - [ ] I have read and followed the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md). (for doc-only changes — N/A) ### Code Changes - [x] Formatters applied — `hadolint Dockerfile.base` clean. No JS/TS/Python files touched. - [x] Tests added or updated for new or changed behavior — N/A. The existing `service-env.test.js` already asserts the `GNUPGHOME` redirect introduced in NVIDIA#1121; this PR makes the corresponding binary available so those assertions reflect a runtime that actually works. A new test that spawns `gpg` directly inside a container would arguably be worth a follow-up (it would have caught this gap originally), but it's a separate concern from this one-line install fix. - [x] No secrets, API keys, or credentials committed. - [ ] Doc pages updated for any user-facing behavior changes — N/A. The bug report describes the expected behavior; this PR just makes runtime match it. No docs claim gpg is unavailable. ### Doc Changes - N/A (no doc changes) --- Signed-off-by: T Savo <evilgenius@nefariousplan.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Base system image now includes GnuPG as a pinned OS package. * **Bug Fixes / Security** * GnuPG runtime directory is now created in a separate step with stricter permissions and sandbox ownership when applicable, reducing exposure. * **Tests** * Test suite updated to verify the new directory creation and permission/ownership behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: T Savo <evilgenius@nefariousplan.com> Co-authored-by: Carlos Villela <cvillela@nvidia.com> Co-authored-by: Prekshi Vyas <34834085+prekshivyas@users.noreply.github.com>
…tic WSL2 tests (NVIDIA#1626) <!-- markdownlint-disable MD041 --> ## Summary Adds an `opts.isWsl` short-circuit to `isWsl()` in `bin/lib/platform.js` so the `shouldPatchCoredns` test can pin its assertions on every host. Without the override the existing test fails on WSL2 dev machines because `os.release()` always reports a "microsoft"-tagged kernel string there. ## Related Issue Closes NVIDIA#1621 (sub-item 3 of three: `shouldPatchCoredns` non-deterministic on WSL2 hosts). ## Changes - `bin/lib/platform.js`: `isWsl()` now returns `opts.isWsl` directly when it's a boolean, before falling through to the existing detection logic. Production callers (which don't pass `opts.isWsl`) keep the existing behavior unchanged. - `test/platform.test.js`: existing `shouldPatchCoredns` test now passes `{ isWsl: false }` so the runtime-matching assertions exercise the *function* rather than the *kernel*. A second test case is added with `{ isWsl: true }` to lock the "skip CoreDNS patching on WSL2" branch so a future change to that branch is caught. Diff: +23 / −5 across 2 files. ## Type of Change - [x] Code change for a new feature, bug fix, or refactor. - [ ] Code change with doc updates. - [ ] Doc only. Prose changes without code sample modifications. - [ ] Doc only. Includes code sample changes. ## Testing - [x] `npx vitest run --project cli test/platform.test.js -t shouldPatchCoredns` — both new test cases pass on a WSL2 Ubuntu host (the existing test was the failure that prompted this PR) - [x] `npx prettier --check bin/lib/platform.js test/platform.test.js` clean - [x] `npx tsc --noEmit -p tsconfig.cli.json` clean - [ ] `npx prek run --all-files` / `npm test` — *not run on the local machine because the full CLI test suite hits two separate baseline failures on a WSL2 host: this very `shouldPatchCoredns` issue (which this PR fixes) and the `install-preflight` sysbin leakage covered by NVIDIA#1628. Upstream CI runs on Linux runners so it'll exercise the full suite normally.* - [ ] `make docs` builds without warnings. (for doc-only changes — N/A) ## Checklist ### General - [x] I have read and followed the [contributing guide](https://github.com/NVIDIA/NemoClaw/blob/main/CONTRIBUTING.md). - [ ] I have read and followed the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md). (for doc-only changes — N/A) ### Code Changes - [x] Formatters applied — `npx prettier --check` clean for both touched files. - [x] Tests added or updated for new or changed behavior — the existing `shouldPatchCoredns` test now uses the `opts.isWsl` override; a new test case pins the WSL2 branch. - [x] No secrets, API keys, or credentials committed. - [ ] Doc pages updated for any user-facing behavior changes — N/A, the new opts argument is test-only API surface; production callers don't pass it. ### Doc Changes - N/A (no doc changes) --- Signed-off-by: T Savo <evilgenius@nefariousplan.com> Signed-off-by: T Savo <evilgenius@nefariousplan.com> Co-authored-by: TSavo <TSavo@users.noreply.github.com>
…VIDIA#1649) <!-- markdownlint-disable MD041 --> ## Summary The sandbox base image (`ghcr.io/nvidia/nemoclaw/sandbox-base`) is missing the `gnupg` package — `gpg --list-keys` (and any other gpg invocation) fails with `bash: gpg: command not found` inside the sandbox. This adds a single pinned `gnupg=2.2.40-1.1+deb12u2` line to the existing `apt-get install` block in `Dockerfile.base`, restoring the binary that the rest of the codebase already assumes is present. ## Related Issue Closes NVIDIA#1640. ## Changes `Dockerfile.base`: add `gnupg=2.2.40-1.1+deb12u2` to the existing `apt-get install` block, slotted right after `git`. Same `--no-install-recommends`, same cleanup tail, same `=<version>` pinning style as every other package in the block. ```diff curl=7.88.1-10+deb12u14 \ git=1:2.39.5-0+deb12u3 \ + gnupg=2.2.40-1.1+deb12u2 \ ca-certificates=20230311+deb12u1 \ ``` The pinned version is the bookworm-stable `2.2.40-1.1+deb12u2`, verified by `apt-cache madison gnupg` against the exact base image SHA `node:22-slim@sha256:4f77a690...`. The package brings in `dirmngr`, `gpg-wks-server`, and `gpg-wks-client` as dependencies. Total layer cost ~3 MB compressed. Diff: **+1 / 0** in 1 file. ### Why this is the right fix (and not "lower the env var" or "remove the test") The fix isn't obvious unless you trace where `GNUPGHOME` came from. Walking that chain: 1. **PR NVIDIA#1121** (`fix(sandbox): restrict /sandbox to read-only via Landlock (NVIDIA#804)`, authored by @prekshivyas, merged 2026-04-08) made the `/sandbox` home directory Landlock-read-only to prevent agents from modifying their own runtime environment. 2. To keep tools that normally write under `~/...` working (gpg, git config, python history, npm prefix, etc.), that PR redirected each tool's homedir to a writable `/tmp/...` path via env vars in `scripts/nemoclaw-start.sh`. The relevant line is at `scripts/nemoclaw-start.sh:53`: ```sh 'GNUPGHOME=/tmp/.gnupg' ``` alongside `HISTFILE=/tmp/.bash_history`, `GIT_CONFIG_GLOBAL=/tmp/.gitconfig`, `PYTHONUSERBASE=/tmp/.local`, etc. 3. PR NVIDIA#1121 also added three matching assertions in `test/service-env.test.js` (lines 177, 191, 347) verifying that the redirect is set: ```js expect(src).toContain("GNUPGHOME=/tmp/.gnupg"); ``` 4. **What PR NVIDIA#1121 didn't do**: add `gnupg` to the `apt-get install` list in `Dockerfile.base`. The env var setup landed and the test assertions landed, but the install line was missed. 5. CI never noticed because `service-env.test.js` only asserts that the env var is *set* in the source — it never spawns a subprocess that actually runs `gpg`. So a working test suite + a missing binary coexist silently. The QA report (this issue, NVIDIA#1640) catches it as a runtime failure on DGX Spark aarch64 because their test step does invoke `gpg --list-keys`. The clear intent of NVIDIA#1121 was to **enable** gpg under a redirected `GNUPGHOME` — you wouldn't redirect the homedir if you wanted gpg blocked. This PR is the matching install line that NVIDIA#1121 should have included, closing a one-line oversight rather than adding new capability or rolling anything back. ### Why not just remove the GNUPGHOME redirect The env var redirect from NVIDIA#1121 is doing real work — without it, any future `apt-get install gnupg` would still leave gpg unable to write to its homedir under Landlock-read-only `/sandbox`. The redirect is the "right" half of the pair; the install is the missing left half. ### Why this isn't a security regression The sandbox runs LLM-driven agents and gpg is a credential-handling tool, so it's worth justifying explicitly: - The redirected `GNUPGHOME=/tmp/.gnupg` is **fresh and empty** per session — no preloaded keys. - Without keys, gpg can hash/check signatures of public material but cannot decrypt or sign anything. - An agent would have to first import a key (which requires the user to provide it — keys are not pulled from anywhere automatically) before gpg becomes capable of any sensitive operation. - This is the same threat model as `git` and `curl`, which are already in the image and could equally be used to fetch arbitrary content. gpg adds no new capability that the existing toolchain doesn't already have. If the project explicitly *did* want gpg unavailable to agents, the right fix would be to remove the GNUPGHOME redirect from NVIDIA#1121 *and* the matching test assertions, not to keep the env wiring while leaving the binary missing — that's just confusing. ## Type of Change - [x] Code change for a new feature, bug fix, or refactor. - [ ] Code change with doc updates. - [ ] Doc only. Prose changes without code sample modifications. - [ ] Doc only. Includes code sample changes. ## Testing Smoke-tested locally by building `Dockerfile.base` with the fix and running the exact failing command from the bug report: ```sh $ docker build -f Dockerfile.base -t nemoclaw-base-test:gnupg . [...] => exporting to image 46.7s done $ docker run --rm nemoclaw-base-test:gnupg gpg --version gpg (GnuPG) 2.2.40 libgcrypt 1.10.1 $ docker run --rm nemoclaw-base-test:gnupg gpg --list-keys gpg: directory '/root/.gnupg' created gpg: keybox '/root/.gnupg/pubring.kbx' created gpg: /root/.gnupg/trustdb.gpg: trustdb created (exit 0) # And with the runtime-redirected GNUPGHOME from nemoclaw-start.sh: $ docker run --rm -e GNUPGHOME=/tmp/.gnupg nemoclaw-base-test:gnupg \ sh -c 'mkdir -p /tmp/.gnupg && chmod 700 /tmp/.gnupg && gpg --list-keys' gpg: keybox '/tmp/.gnupg/pubring.kbx' created (exit 0) ``` Both the default `~/.gnupg` and the runtime-redirected `/tmp/.gnupg` (matching what `nemoclaw-start.sh` exports) work as expected. The exact `gpg --list-keys` failure from the bug report no longer reproduces. - [x] `hadolint Dockerfile.base` — clean (no warnings) - [x] `docker build -f Dockerfile.base` — succeeds, exports to image cleanly - [x] `gpg --version` in built image — works (`gpg (GnuPG) 2.2.40`) - [x] `gpg --list-keys` in built image — works (was `bash: gpg: command not found` before this PR) - [x] `gpg --list-keys` with `GNUPGHOME=/tmp/.gnupg` — works (matches the runtime env from `nemoclaw-start.sh`) - [ ] `npx prek run --all-files` — partial: ran the affected hooks (commitlint, gitleaks, hadolint) which all pass; did NOT run `test-cli` against the full local suite because two pre-existing baseline failures on stock `main` get in the way on a WSL2 dev host (the `shouldPatchCoredns` issue addressed by PR NVIDIA#1626 (merged) and the install-preflight PATH leakage addressed by PR NVIDIA#1628 (open)). Upstream CI runs on Linux GHA runners and doesn't hit either of those, so it'll exercise the full suite normally. - [ ] `npm test` — same caveat as above, ran the relevant projects in isolation - [ ] `make docs` builds without warnings. (for doc-only changes — N/A) ## Checklist ### General - [x] I have read and followed the [contributing guide](https://github.com/NVIDIA/NemoClaw/blob/main/CONTRIBUTING.md). - [ ] I have read and followed the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md). (for doc-only changes — N/A) ### Code Changes - [x] Formatters applied — `hadolint Dockerfile.base` clean. No JS/TS/Python files touched. - [x] Tests added or updated for new or changed behavior — N/A. The existing `service-env.test.js` already asserts the `GNUPGHOME` redirect introduced in NVIDIA#1121; this PR makes the corresponding binary available so those assertions reflect a runtime that actually works. A new test that spawns `gpg` directly inside a container would arguably be worth a follow-up (it would have caught this gap originally), but it's a separate concern from this one-line install fix. - [x] No secrets, API keys, or credentials committed. - [ ] Doc pages updated for any user-facing behavior changes — N/A. The bug report describes the expected behavior; this PR just makes runtime match it. No docs claim gpg is unavailable. ### Doc Changes - N/A (no doc changes) --- Signed-off-by: T Savo <evilgenius@nefariousplan.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Base system image now includes GnuPG as a pinned OS package. * **Bug Fixes / Security** * GnuPG runtime directory is now created in a separate step with stricter permissions and sandbox ownership when applicable, reducing exposure. * **Tests** * Test suite updated to verify the new directory creation and permission/ownership behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: T Savo <evilgenius@nefariousplan.com> Co-authored-by: Carlos Villela <cvillela@nvidia.com> Co-authored-by: Prekshi Vyas <34834085+prekshivyas@users.noreply.github.com>
Summary
Replaces the
TEST_SYSTEM_PATH = "/usr/bin:/bin"literal intest/install-preflight.test.jswithbuildIsolatedSystemPath(), a small helper that creates a tmpdir at module load and symlinks every entry from/usr/binand/bininto it — exceptnode,npm, andnpx, which are deliberately excluded so the runtime preflight's "node missing" / "npm missing" branches are actually exercised.Related Issue
Closes #1621 (sub-item 2 of three: install-preflight tests leak host
/usr/bin/nodethroughTEST_SYSTEM_PATH).Changes
test/install-preflight.test.js:buildIsolatedSystemPath()helper at module top.const TEST_SYSTEM_PATH = "/usr/bin:/bin";toconst TEST_SYSTEM_PATH = buildIsolatedSystemPath();.${fakeBin}:${TEST_SYSTEM_PATH}continue to work unchanged because the fake bin still wins (it comes first in the PATH).PATH: TEST_SYSTEM_PATHdirectly (the "node missing" / "npm missing" cases) now correctly see neither node nor npm.Diff: +42 / −1 in 1 file.
Type of Change
Testing
npx vitest run --project cli test/install-preflight.test.js— all 56 tests pass locally on a Linux host with/usr/bin/nodepresent (the previously-failing "node missing" / "npm missing" cases now correctly report missing)npx prettier --check test/install-preflight.test.jscleannode --check test/install-preflight.test.jscleannpx prek run --all-files/npm test— not run on the local machine because the full CLI test suite hits a separate baseline failure on a WSL2 host: theshouldPatchCorednsissue covered by fix(platform): allow shouldPatchCoredns isWsl override for deterministic WSL2 tests #1626. Upstream CI runs on Linux runners so it'll exercise the full suite normally.make docsbuilds without warnings. (for doc-only changes — N/A)Checklist
General
Code Changes
npx prettier --checkclean.Doc Changes
Signed-off-by: T Savo evilgenius@nefariousplan.com
Summary by CodeRabbit