Skip to content

fix(test): isolate sysbin in install-preflight tests to prevent host PATH leakage - #1628

Closed
TSavo wants to merge 1 commit into
NVIDIA:mainfrom
TSavo:fix/test-install-preflight-isolated-sysbin
Closed

fix(test): isolate sysbin in install-preflight tests to prevent host PATH leakage#1628
TSavo wants to merge 1 commit into
NVIDIA:mainfrom
TSavo:fix/test-install-preflight-isolated-sysbin

Conversation

@TSavo

@TSavo TSavo commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the TEST_SYSTEM_PATH = "/usr/bin:/bin" literal in test/install-preflight.test.js with buildIsolatedSystemPath(), a small helper that creates a tmpdir at module load and symlinks every entry from /usr/bin and /bin into it — except node, npm, and npx, 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/node through TEST_SYSTEM_PATH).

Changes

  • test/install-preflight.test.js:
    • Adds buildIsolatedSystemPath() helper at module top.
    • Changes const TEST_SYSTEM_PATH = "/usr/bin:/bin"; to const TEST_SYSTEM_PATH = buildIsolatedSystemPath();.
    • All ~19 existing call sites that use ${fakeBin}:${TEST_SYSTEM_PATH} continue to work unchanged because the fake bin still wins (it comes first in the PATH).
    • The 3 sites that use PATH: TEST_SYSTEM_PATH directly (the "node missing" / "npm missing" cases) now correctly see neither node nor npm.

Diff: +42 / −1 in 1 file.

Type of Change

  • 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

  • npx vitest run --project cli test/install-preflight.test.js — all 56 tests pass locally on a Linux host with /usr/bin/node present (the previously-failing "node missing" / "npm missing" cases now correctly report missing)
  • npx prettier --check test/install-preflight.test.js clean
  • node --check test/install-preflight.test.js clean
  • npx prek run --all-files / npm testnot run on the local machine because the full CLI test suite hits a separate baseline failure on a WSL2 host: the shouldPatchCoredns issue 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 docs builds without warnings. (for doc-only changes — N/A)

Checklist

General

Code Changes

  • Formatters applied — npx prettier --check clean.
  • Tests added or updated for new or changed behavior — the 3 previously-broken "missing binary" test cases now correctly assert the missing branch.
  • No secrets, API keys, or credentials committed.
  • Doc pages updated for any user-facing behavior changes — N/A, test infrastructure only.

Doc Changes

  • N/A (no doc changes)

Signed-off-by: T Savo evilgenius@nefariousplan.com

Summary by CodeRabbit

  • Tests
    • Improved test environment isolation to prevent unintended system dependencies during testing, ensuring more reliable and predictable test execution.

Copilot AI review requested due to automatic review settings April 8, 2026 20:53
@coderabbitai

coderabbitai Bot commented Apr 8, 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: Pro

Run ID: cdddb4ab-e0df-4124-99aa-f1a82f7f91a8

📥 Commits

Reviewing files that changed from the base of the PR and between 8b717d5 and 9c02f6f.

📒 Files selected for processing (1)
  • test/install-preflight.test.js

📝 Walkthrough

Walkthrough

Replaced the fixed TEST_SYSTEM_PATH = "/usr/bin:/bin" in test/install-preflight.test.js with a runtime buildIsolatedSystemPath() helper that creates a temporary system-bin directory, symlinks host executables while explicitly excluding node, npm, and npx, and sets TEST_SYSTEM_PATH to that temp directory to prevent host PATH leakage.

Changes

Cohort / File(s) Summary
Test environment isolation
test/install-preflight.test.js
Added buildIsolatedSystemPath() which creates a temp dir and symlinks executables from /usr/bin and /bin while excluding node, npm, and npx. Replaced the static TEST_SYSTEM_PATH with the helper's returned directory so tests don't inadvertently see the host's node/npm unless explicitly provided.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 In a tempdir burrow I softly tread,
I stitch small symlinks where tests are led,
Node and npm are left outside,
Clean paths hop in, no host to hide,
Hooray for tidy runs—carrots for bread! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 title accurately describes the main change: isolating the system PATH in install-preflight tests to prevent host binaries from leaking into test execution.
Linked Issues check ✅ Passed The PR implements objective #2 from issue #1621 by creating an isolated temporary sysbin directory with symlinks, excluding node/npm/npx to fix the 'node missing' and 'npm missing' tests.
Out of Scope Changes check ✅ Passed All changes are scoped to test/install-preflight.test.js and directly address the system PATH leakage issue identified in linked issue #1621 objective #2.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copilot AI 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.

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/bin and /bin into the isolated directory via symlinks while excluding node, npm, and npx.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/install-preflight.test.js Outdated
Comment on lines +46 to +48
} catch {
// Ignore EEXIST when /bin is a symlink to /usr/bin (modern Linux)
// and we already linked the same name on the first pass.

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

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

Suggested change
} 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;
}

Copilot uses AI. Check for mistakes.

@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

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between b8a5245 and 5703f8a.

📒 Files selected for processing (1)
  • test/install-preflight.test.js

Comment thread test/install-preflight.test.js
@TSavo
TSavo force-pushed the fix/test-install-preflight-isolated-sysbin branch from 5703f8a to 8b717d5 Compare April 8, 2026 21:06
@TSavo

TSavo commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed to add a Signed-off-by trailer (DCO compliance) — the diff itself is unchanged. CodeRabbit's prior review still applies to the same content.

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>
@TSavo
TSavo force-pushed the fix/test-install-preflight-isolated-sysbin branch from 8b717d5 to 9c02f6f Compare April 8, 2026 22:03
@TSavo

TSavo commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

@Copilot @coderabbitai both right — the bare catch {} was overbroad. Force-pushed 9c02f6f narrowing it to only swallow EEXIST:

} 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 err && err.code guard handles the (unlikely) case where Node throws a non-Error object — err.code access would otherwise crash on a bare null/undefined. Same shape as the proposed suggestion otherwise.

Diff is now +47 / −1 in one file. The 56-test suite still passes locally on a Linux host with /usr/bin/node present. Re-review welcome.

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@TSavo — the updated catch block looks good. The narrowing from a bare catch {} to an EEXIST-only swallow is exactly the right fix, and the logic is sound:

  • EEXIST swallow + continue — handles the expected /bin → /usr/bin symlink alias collision on modern Linux without masking anything unexpected.
  • throw err fallthrough — surfaces EPERM, EACCES, EINVAL, ENOENT, etc. as hard failures, which prevents a silently half-populated TEST_SYSTEM_PATH from producing confusing downstream test failures. This is exactly the fail-closed posture that's appropriate here.
  • err && err.code guard — defensively handles the (theoretical) non-Error throw. Functionally identical to err?.code === "EEXIST", just slightly more explicit — either form is fine.

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 os.tmpdir() is typically ephemeral, but if you want to be tidy you could register an afterAll(() => fs.rmSync(TEST_SYSTEM_PATH, { recursive: true, force: true })). Not a blocker — just mentioning it for completeness.

LGTM on the buildIsolatedSystemPath change. 🟢


🧠 Learnings used
Learnt from: cluster2600
Repo: NVIDIA/NemoClaw PR: 782
File: test/config-io.test.js:54-75
Timestamp: 2026-03-24T11:21:32.603Z
Learning: In the NVIDIA/NemoClaw repo, the test suite (including `test/config-io.test.js`) runs on Ubuntu CI runners as a non-root user. Chmod-driven EACCES assertions (e.g., `fs.chmodSync(dir, 0o500)` to simulate permission errors) are intentional and reliable in this environment. Do not flag these as fragile or platform-unsafe unless there is evidence of root/Windows CI usage.

Learnt from: brianwtaylor
Repo: NVIDIA/NemoClaw PR: 251
File: bin/lib/preflight.js:114-129
Timestamp: 2026-03-17T23:56:54.445Z
Learning: In bin/lib/preflight.js (NemoClaw repo), checkPortAvailable() uses `typeof lsofOut === "string"` as a sentinel: a string value means lsof ran (possibly as non-root); undefined means lsof was not available on the host. Empty lsof string output is NOT treated as authoritative — non-root lsof silently drops root-owned sockets because it cannot read /proc/[pid]/fd (returns EACCES), which is the exact deployment scenario for NemoClaw (Docker proxy / openclaw-gateway runs as root, nemoclaw onboard runs as non-root). Empty lsof output must fall through to the net probe (bind() has no ownership restriction and checks the kernel socket hash table directly). The net probe is the fallback for both "lsof unavailable" and "lsof returned empty rows". Tests should cover: empty lsof + occupied port → net probe catches EADDRINUSE.

Learnt from: CR
Repo: NVIDIA/NemoClaw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T17:01:37.795Z
Learning: Applies to {nemoclaw/src/blueprint,bin/lib}/**/*.{js,ts} : Security-sensitive code paths in isolation/sandbox features must have extra test coverage to prevent credential leaks and sandbox escapes

Learnt from: gn00295120
Repo: NVIDIA/NemoClaw PR: 156
File: nemoclaw/src/commands/migration-state.ts:660-679
Timestamp: 2026-03-22T01:05:45.467Z
Learning: In `nemoclaw/src/commands/migration-state.ts`, the `walkAndRemoveFile` and `walkAndStripCredentials` functions intentionally warn-and-continue on all filesystem errors (including non-ENOENT) rather than throwing/aborting. This is a deliberate UX trade-off to avoid blocking the entire migration on transient permission errors in large workspace directories. The author is aware of the fail-open security risk and plans to tighten to fail-closed for non-ENOENT errors only if credential leaks are observed in practice.

Learnt from: CR
Repo: NVIDIA/NemoClaw PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T17:01:37.795Z
Learning: Applies to test/**/*.{js,ts} : Use ESM syntax (`import`/`export`) for root-level integration tests in `test/` directory

Learnt from: jieunl24
Repo: NVIDIA/NemoClaw PR: 827
File: bin/lib/onboard.js:1118-1123
Timestamp: 2026-03-24T19:26:43.742Z
Learning: In the NemoClaw project (`bin/lib/onboard.js`), `openshell sandbox create` is synchronous — it does not return until the sandbox container is running. The race condition being fixed is specifically between step 3 (sandbox creation) completing and step 7 (policy application), not within step 3 itself. Readiness gates before `registry.registerSandbox()` or the sandbox connect call would be redundant.

@wscurran

wscurran commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

✨ 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:

@cv cv added the v0.0.11 label Apr 9, 2026
@cv

cv commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

I attempted to port this branch across the JS→TS migration and merge the latest main, but it still needs manual follow-up.

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

cv added a commit that referenced this pull request Apr 9, 2026
…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>
@ericksoa ericksoa added v0.0.12 and removed v0.0.11 labels Apr 10, 2026
@cv cv added v0.0.13 and removed v0.0.12 labels Apr 10, 2026
@prekshivyas

Copy link
Copy Markdown
Collaborator

@TSavo can you also resolve conflicts pls !

@prekshivyas prekshivyas self-assigned this Apr 10, 2026
@ericksoa

Copy link
Copy Markdown
Contributor

Closing — this fix landed on main via #1706. Thank you @TSavo for the original contribution and the buildIsolatedSystemPath() approach.

@ericksoa ericksoa closed this Apr 11, 2026
ericksoa pushed a commit to cheese-head/NemoClaw that referenced this pull request Apr 14, 2026
…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>
gemini2026 pushed a commit to gemini2026/NemoClaw that referenced this pull request Apr 14, 2026
…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>
gemini2026 pushed a commit to gemini2026/NemoClaw that referenced this pull request Apr 14, 2026
…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>
@wscurran wscurran added area: ci CI workflows, checks, release automation, or GitHub Actions bug-fix PR fixes a bug or regression chore Build, CI, dependency, or tooling maintenance and removed CI/CD chore Build, CI, dependency, or tooling maintenance labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ci CI workflows, checks, release automation, or GitHub Actions bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Tests] Three test-environment fragility issues (slow hosts, system PATH leakage, WSL2)

6 participants