Skip to content

fix(image): install gnupg in sandbox base image so gpg is available#1649

Merged
cv merged 5 commits intoNVIDIA:mainfrom
TSavo:fix/sandbox-base-add-gnupg
Apr 9, 2026
Merged

fix(image): install gnupg in sandbox base image so gpg is available#1649
cv merged 5 commits intoNVIDIA:mainfrom
TSavo:fix/sandbox-base-add-gnupg

Conversation

@TSavo
Copy link
Copy Markdown
Contributor

@TSavo TSavo commented Apr 8, 2026

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.

        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 fix(sandbox): restrict /sandbox to read-only via Landlock (#804) #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:
    'GNUPGHOME=/tmp/.gnupg'
    alongside HISTFILE=/tmp/.bash_history, GIT_CONFIG_GLOBAL=/tmp/.gitconfig, PYTHONUSERBASE=/tmp/.local, etc.
  3. PR fix(sandbox): restrict /sandbox to read-only via Landlock (#804) #1121 also added three matching assertions in test/service-env.test.js (lines 177, 191, 347) verifying that the redirect is set:
    expect(src).toContain("GNUPGHOME=/tmp/.gnupg");
  4. What PR fix(sandbox): restrict /sandbox to read-only via Landlock (#804) #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, [NemoClaw][All platforms] Sandbox image missing gnupg package — gpg command not found #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

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

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

  • hadolint Dockerfile.base — clean (no warnings)
  • docker build -f Dockerfile.base — succeeds, exports to image cleanly
  • gpg --version in built image — works (gpg (GnuPG) 2.2.40)
  • gpg --list-keys in built image — works (was bash: gpg: command not found before this PR)
  • 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 fix(platform): allow shouldPatchCoredns isWsl override for deterministic WSL2 tests #1626 (merged) and the install-preflight PATH leakage addressed by PR fix(test): isolate sysbin in install-preflight tests to prevent host PATH leakage #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

Code Changes

  • Formatters applied — hadolint Dockerfile.base clean. No JS/TS/Python files touched.
  • Tests added or updated for new or changed behavior — N/A. The existing service-env.test.js already asserts the GNUPGHOME redirect introduced in fix(sandbox): restrict /sandbox to read-only via Landlock (#804) #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.
  • 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

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.

The sandbox base image (`ghcr.io/nvidia/nemoclaw/sandbox-base`) does not
include the gnupg package, even though `nemoclaw-start.sh` already
exports `GNUPGHOME=/tmp/.gnupg` and `test/service-env.test.js` asserts
the redirect is in place. As a result `gpg --list-keys` (or any other
gpg invocation) inside the sandbox fails with `bash: gpg: command not
found`, breaking workflows that expect signing/verification to be
available — including the smoke check QA reported on DGX Spark
(aarch64).

The GNUPGHOME redirect was introduced in NVIDIA#1121 ("restrict /sandbox to
read-only via Landlock") to keep gpg writable when `~/.gnupg` became
unwritable, but the matching `apt-get install gnupg` line was never
added to `Dockerfile.base`. The service-env tests assert the env var
setup but don't actually invoke gpg, so CI never noticed the binary
was missing.

This adds `gnupg=2.2.40-1.1+deb12u2` (the bookworm-pinned version,
matching the existing `=<version>` pinning style for every other
package in the same `apt-get install` block) right after `git`. No
other changes — same `--no-install-recommends`, same cleanup tail.

The package brings in dirmngr, gpg-wks-server, and gpg-wks-client as
dependencies (per a clean install probe in the exact base image SHA).
Total layer cost ~3 MB compressed.

Smoke tested locally by building Dockerfile.base with the fix and
running the exact failing command from the bug report:

  $ docker build -f Dockerfile.base -t nemoclaw-base-test:gnupg .
  $ docker run --rm nemoclaw-base-test:gnupg gpg --version
  gpg (GnuPG) 2.2.40
  $ 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)
  $ 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.

Closes NVIDIA#1640.

Signed-off-by: T Savo <evilgenius@nefariousplan.com>
Copilot AI review requested due to automatic review settings April 8, 2026 23:23
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

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: 02e507c6-0ea8-4052-8adf-cfb10dd64a8b

📥 Commits

Reviewing files that changed from the base of the PR and between 575341f and f56ffa5.

📒 Files selected for processing (2)
  • Dockerfile.base
  • scripts/nemoclaw-start.sh
✅ Files skipped from review due to trivial changes (1)
  • Dockerfile.base

📝 Walkthrough

Walkthrough

Installs a pinned gnupg package in the base Docker image and changes the entrypoint to create /tmp/.gnupg separately with restrictive permissions; tests updated to assert the new creation and permission behavior.

Changes

Cohort / File(s) Summary
Docker Base Configuration
Dockerfile.base
Added pinned gnupg (2.2.40-1.1+deb12u2) to the apt-get install package list.
Entrypoint / Init Script
scripts/nemoclaw-start.sh
Removed /tmp/.gnupg from the bulk directory creation; now creates /tmp/.gnupg separately with -m 700, and when root uses install -d -o sandbox -g sandbox -m 700.
Tests
test/service-env.test.ts
Removed assertions expecting /tmp/.gnupg pre-created by bulk install; added test asserting the script creates /tmp/.gnupg with restrictive permissions (install -d -o sandbox -g sandbox -m 700 and install -d -m 700).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I hopped in quiet, soft and quick,

A gnupg patch — a careful trick,
A folder made with locks so tight,
Sandboxed snug through day and night,
Hooray — secure and cozy, tick-tick!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: installing gnupg in the sandbox base image to make gpg available, which directly addresses the root cause identified in the objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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.

Copy link
Copy Markdown

Copilot AI left a comment

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 restores gpg availability in the NemoClaw sandbox base image (ghcr.io/nvidia/nemoclaw/sandbox-base) by adding the missing Debian gnupg package to the pinned apt-get install list in Dockerfile.base, addressing runtime failures like gpg: command not found (Issue #1640).

Changes:

  • Add gnupg=2.2.40-1.1+deb12u2 to the existing pinned apt-get install block in Dockerfile.base.

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

@cv cv enabled auto-merge (squash) April 9, 2026 01:15
@wscurran wscurran added Docker Support for Docker containerization fix labels Apr 9, 2026
@cv cv added the v0.0.11 Release target label Apr 9, 2026
@prekshivyas prekshivyas self-assigned this Apr 9, 2026
@prekshivyas
Copy link
Copy Markdown
Contributor

Thanks for the thorough investigation and clean fix, @TSavo — the root cause trace through #1121 was especially helpful.

Two things we'd like to see before merging:

  1. /tmp/.gnupg permissions: The startup script (scripts/nemoclaw-start.sh:72-74) creates /tmp/.gnupg with mode 755 via install -d -m 755, but gpg expects 700 on its homedir and emits a warning otherwise. Since this PR is completing the work fix(sandbox): restrict /sandbox to read-only via Landlock (#804) #1121 started, it would be great to fix the permissions here too — either a separate chmod 700 /tmp/.gnupg after the install call, or splitting .gnupg out into its own install -d -m 700 line.

  2. Runtime gpg test (nice-to-have): The existing service-env.test.ts assertions only check that the GNUPGHOME env var is set in the script source — they never actually invoke gpg. That's how this gap slipped through originally. A lightweight test that verifies gpg --version exits 0 inside the container would prevent this from regressing. Not blocking, but would be a great addition if you have time.

Thanks again!

@cv
Copy link
Copy Markdown
Contributor

cv commented Apr 9, 2026

I pushed a narrow follow-up onto this branch to address the remaining runtime hardening concern:

  • create GNUPGHOME=/tmp/.gnupg with mode 700 in both root and non-root startup paths
  • add a regression test in test/service-env.test.ts that locks in the restrictive mode setup

Local validation run: npx vitest run test/service-env.test.ts and npx shellcheck scripts/nemoclaw-start.sh both passed.

CI is rerunning now.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/service-env.test.ts (1)

195-200: Prefer behavior-based assertion over exact source-string matching.

This test is useful, but it can still pass/fail on formatting-only edits and doesn’t validate effective directory mode behavior. Consider asserting the resulting mode/ownership from a small executed wrapper around the extracted setup block instead of toContain(...) literals.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/service-env.test.ts` around lines 195 - 200, The test `entrypoint
creates GNUPGHOME with restrictive permissions` relies on string matching and
should instead execute the setup logic and assert actual filesystem effects:
extract the setup block from the script (using the existing
scriptPath/readFileSync in test/service-env.test.ts), run that snippet in a
controlled temp directory as a small child process or by invoking the same shell
commands, then use fs.stat (or fs.promises.stat) to assert the created directory
mode is 0o700 and ownership matches the expected uid/gid (or at least not
world-writable); replace the toContain(...) assertions with these behavior-based
checks to avoid brittle formatting-dependent tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/service-env.test.ts`:
- Around line 195-200: The test `entrypoint creates GNUPGHOME with restrictive
permissions` relies on string matching and should instead execute the setup
logic and assert actual filesystem effects: extract the setup block from the
script (using the existing scriptPath/readFileSync in test/service-env.test.ts),
run that snippet in a controlled temp directory as a small child process or by
invoking the same shell commands, then use fs.stat (or fs.promises.stat) to
assert the created directory mode is 0o700 and ownership matches the expected
uid/gid (or at least not world-writable); replace the toContain(...) assertions
with these behavior-based checks to avoid brittle formatting-dependent tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3e3ed07c-5072-433a-9cbc-8785f2cb1dba

📥 Commits

Reviewing files that changed from the base of the PR and between e8e6968 and 575341f.

📒 Files selected for processing (2)
  • scripts/nemoclaw-start.sh
  • test/service-env.test.ts

@cv
Copy link
Copy Markdown
Contributor

cv commented Apr 9, 2026

Follow-up check after the salvage fix:

  • CI is fully green
  • no unresolved major/critical CodeRabbit findings
  • risky startup-script change has regression coverage
  • local mergeability check against current main is clean

Ready for merge once a maintainer picks it up.

@cv cv merged commit 9d3f9cc into NVIDIA:main Apr 9, 2026
10 checks passed
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
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Docker Support for Docker containerization fix v0.0.11 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NemoClaw][All platforms] Sandbox image missing gnupg package — gpg command not found

5 participants