Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,31 @@ reviews:
gh workflow run nightly-e2e.yaml --ref <branch> -f jobs=shields-config-e2e
```

- path: "src/lib/actions/sandbox/brew.ts"
instructions: &e2e-brew-command |
This file owns the `nemoclaw <name> brew {init,install,uninstall,deinit}`
lifecycle: Linuxbrew bootstrap, formula install/uninstall via the
linuxbrew user, profile.d PATH write, and prefix teardown. Changes
here affect every brew subcommand.

**E2E test recommendation:**
- `brew-command-e2e` — TC-BREW-01..07: init/install/uninstall/deinit
+ idempotent init + shields-up refusal + install-without-init refusal

To run selectively:
```
gh workflow run nightly-e2e.yaml --ref <branch> -f jobs=brew-command-e2e
```

- path: "src/commands/sandbox/brew/**"
instructions: *e2e-brew-command

- path: "src/commands/sandbox/brew.ts"
instructions: *e2e-brew-command

- path: "test/e2e/test-brew-command.sh"
instructions: *e2e-brew-command

- path: "agents/hermes/**"
instructions: |
This directory contains the Hermes agent. Changes affect
Expand Down
40 changes: 39 additions & 1 deletion .github/workflows/nightly-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ on:
openclaw-inference-switch-e2e,
network-policy-e2e, state-backup-restore-e2e, tunnel-lifecycle-e2e, diagnostics-e2e,
credential-migration-e2e,
snapshot-commands-e2e, shields-config-e2e, rebuild-openclaw-e2e,
snapshot-commands-e2e, brew-command-e2e, shields-config-e2e, rebuild-openclaw-e2e,
upgrade-stale-sandbox-e2e, rebuild-hermes-e2e,
rebuild-hermes-stale-base-e2e, double-onboard-e2e,
onboard-repair-e2e, onboard-resume-e2e, onboard-negative-paths-e2e,
Expand Down Expand Up @@ -1420,6 +1420,41 @@ jobs:
path: /tmp/nemoclaw-e2e-install.log
if-no-files-found: ignore

# ── Brew subcommand lifecycle E2E (#3757) ────────────────────
# TC-BREW-01..07: brew init/install/uninstall/deinit, idempotent init,
# shields-up refusal, install-without-init refusal.
brew-command-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' &&
(github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',brew-command-e2e,'))
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.target_ref || github.ref }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
persist-credentials: false

- name: Run brew command E2E test
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-brew-cmd"
GITHUB_TOKEN: ${{ github.token }}
run: bash test/e2e/test-brew-command.sh

- name: Upload test log on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: brew-command-test-log
path: test-brew-command-*.log
if-no-files-found: ignore

# ── Shields & config lifecycle E2E ───────────────────────────
# Validates shields down/up controls config mutability, config get/set/
# rotate-token, audit trail, and auto-restore timer.
Expand Down Expand Up @@ -2216,6 +2251,7 @@ jobs:
diagnostics-e2e,
credential-migration-e2e,
snapshot-commands-e2e,
brew-command-e2e,
shields-config-e2e,
rebuild-openclaw-e2e,
upgrade-stale-sandbox-e2e,
Expand Down Expand Up @@ -2312,6 +2348,7 @@ jobs:
diagnostics-e2e,
credential-migration-e2e,
snapshot-commands-e2e,
brew-command-e2e,
shields-config-e2e,
rebuild-openclaw-e2e,
upgrade-stale-sandbox-e2e,
Expand Down Expand Up @@ -2465,6 +2502,7 @@ jobs:
diagnostics-e2e,
credential-migration-e2e,
snapshot-commands-e2e,
brew-command-e2e,
shields-config-e2e,
rebuild-openclaw-e2e,
upgrade-stale-sandbox-e2e,
Expand Down
2 changes: 1 addition & 1 deletion docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ For example, Slack bot tokens must start with `xoxb-`.
### Choose Network Policy Presets

After the sandbox image builds and OpenClaw starts inside the sandbox, NemoClaw asks which network policy tier to apply.
The default **Balanced** tier includes common development presets such as npm, PyPI, Hugging Face, Homebrew, and Brave Search when the selected agent supports web search.
The default **Balanced** tier includes common development presets such as npm, PyPI, Hugging Face, and Brave Search when the selected agent supports web search.
Use the arrow keys or `j` and `k` to move, Space to select, and Enter to confirm.

The preset selector lets you include more destinations, such as GitHub, Jira, Slack, Telegram, or local inference.
Expand Down
1 change: 0 additions & 1 deletion docs/network-policy/customize-network-policy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ Available presets:
| Preset | Endpoints |
|--------|-----------|
| `brave` | Brave Search API |
| `brew` | Homebrew (Linuxbrew) package manager |
| `discord` | Discord API, gateway, and CDN access |
| `github` | GitHub and GitHub REST API |
| `huggingface` | Hugging Face Hub (download-only) and inference router |
Expand Down
13 changes: 9 additions & 4 deletions docs/network-policy/integration-policy-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ NemoClaw ships maintained policy presets for common services in `nemoclaw-bluepr
| Workflow | Preset |
|----------|--------|
| Brave Search | `brave` |
| Homebrew packages | `brew` |
| Discord messaging | `discord` |
| GitHub and GitHub API | `github` |
| Hugging Face Hub and Inference API | `huggingface` |
Expand Down Expand Up @@ -205,15 +204,13 @@ Use these presets when an agent workflow installs packages or downloads model as
|----------|--------|
| npm or Yarn packages | `npm` |
| Python packages from PyPI | `pypi` |
| Homebrew packages | `brew` |
| Hugging Face model or dataset access | `huggingface` |

Add only the preset required for the task:

```console
$ nemoclaw my-assistant policy-add npm --yes
$ nemoclaw my-assistant policy-add pypi --yes
$ nemoclaw my-assistant policy-add brew --yes
$ nemoclaw my-assistant policy-add huggingface --yes
```

Expand All @@ -222,10 +219,18 @@ Remove package access after a one-time setup task if the sandbox no longer needs
```console
$ nemoclaw my-assistant policy-remove npm --yes
$ nemoclaw my-assistant policy-remove pypi --yes
$ nemoclaw my-assistant policy-remove brew --yes
$ nemoclaw my-assistant policy-remove huggingface --yes
```

Homebrew is installed via a first-class subcommand rather than a network preset:

```console
$ nemoclaw my-assistant brew init
$ nemoclaw my-assistant brew install hello
```

See [`nemoclaw <name> brew init`](../reference/commands.mdx#nemoclaw-name-brew-init) for the full lifecycle (init, install, uninstall, deinit).

## Local Inference

Use `local-inference` when the sandbox needs access to host-side local inference services such as Ollama or vLLM through the OpenShell host gateway.
Expand Down
53 changes: 53 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,59 @@ Files with unsafe path characters are rejected to prevent shell injection.
If the skill already exists on the sandbox, the command updates it in place and preserves chat history.
For new installs, the agent session index is refreshed so the agent discovers the skill on the next session.

### `nemoclaw <name> brew init`

Bootstrap Homebrew (Linuxbrew) inside the sandbox.
Creates the `linuxbrew` user inside the sandbox and runs the canonical Linuxbrew installer.
The bottled `brew` binary lands at `/home/linuxbrew/.linuxbrew/bin/brew`.
Refuses when shields are up — run `nemoclaw <name> shields down` first.

```console
$ nemoclaw my-assistant brew init
```

Homebrew refuses to run as anyone other than the prefix owner, so the agent inside the sandbox cannot invoke `brew install` directly. Bottled binaries dropped by `brew install` (for example `/home/linuxbrew/.linuxbrew/bin/jq`) survive `shields up` and remain runnable by their absolute path. `brew init` also writes `/etc/profile.d/nemoclaw-linuxbrew.sh`, so new login shells inside the sandbox (e.g. `nemoclaw <name> connect` and `bash -lc ...`) pick up the linuxbrew prefix on `PATH`. Long-running processes started before `brew init` (including the OpenClaw agent) keep their original PATH; restart them or invoke the binaries by absolute path if they need access.

### `nemoclaw <name> brew install`

Install one or more Homebrew formulae via the `linuxbrew` user.
Refuses when shields are up or when `brew init` has not been run.

```console
$ nemoclaw my-assistant brew install hello
$ nemoclaw my-assistant brew install jq curl
```

| Flag | Description |
|------|-------------|
| `--yes`, `-y` | When set together with `NEMOCLAW_NON_INTERACTIVE=1`, auto-run `brew init` first if Homebrew is not yet installed in the sandbox. |

For unattended automation:

```console
$ NEMOCLAW_NON_INTERACTIVE=1 nemoclaw my-assistant brew install hello --yes
```

`--yes` alone (without `NEMOCLAW_NON_INTERACTIVE=1`) keeps the explicit-init requirement, so the auto-init only triggers in scripted contexts that already opt into non-interactive behaviour repo-wide.

### `nemoclaw <name> brew uninstall`

Uninstall one or more Homebrew formulae via the `linuxbrew` user.
Refuses when shields are up or when `brew init` has not been run.

```console
$ nemoclaw my-assistant brew uninstall hello
```

### `nemoclaw <name> brew deinit`

Remove the `linuxbrew` user and the entire Homebrew prefix (`/home/linuxbrew`).
Refuses when shields are up.

```console
$ nemoclaw my-assistant brew deinit
```

### `nemoclaw <name> rebuild`

Upgrade a sandbox to the current agent version while preserving workspace state.
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/network-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ The baseline policy is always applied regardless of the selected tier.
| Tier | Presets included | Description |
|------|------------------|-------------|
| Restricted | None | Base sandbox only. No third-party network access beyond inference and core agent tooling. |
| Balanced (default) | `npm`, `pypi`, `huggingface`, `brew`, `brave when supported` | Full dev tooling and web search for agents that support web search. No messaging platform access. |
| Open | `npm`, `pypi`, `huggingface`, `brew`, `brave when supported`, `slack`, `discord`, `telegram`, `whatsapp`, `jira`, `outlook` | Broad access across third-party services including messaging and productivity. |
| Balanced (default) | `npm`, `pypi`, `huggingface`, `brave when supported` | Full dev tooling and web search for agents that support web search. No messaging platform access. |
| Open | `npm`, `pypi`, `huggingface`, `brave when supported`, `slack`, `discord`, `telegram`, `wechat`, `whatsapp`, `jira`, `outlook` | Broad access across third-party services including messaging and productivity. |

After selecting a tier, a combined preset and access-mode screen lets you include or exclude individual presets and toggle each between read (GET only) and read-write (GET + POST/PUT/PATCH) access.
Tier-default presets are pre-selected; additional presets can be added from the full list.
Expand Down
1 change: 0 additions & 1 deletion docs/security/best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ NemoClaw ships preset policy files in `nemoclaw-blueprint/policies/presets/` for
| Preset | What it enables | Key risk |
|---|---|---|
| `brave` | Brave Search API. | Agent can issue search queries. |
| `brew` | Homebrew (Linuxbrew) package manager. | Allows installing arbitrary Homebrew packages, which may contain malicious code. |
| `discord` | Discord REST API, WebSocket gateway, CDN. | CDN endpoint (`cdn.discordapp.com`) allows GET to any path. WebSocket uses `access: full` (no inspection). |
| `github` | GitHub and GitHub REST API. | Gives agent read/write access to repositories and issues via `git`. |
| `huggingface` | Hugging Face Hub (download-only) and inference router. | Allows downloading arbitrary models and datasets. POST is restricted to the inference router only. |
Expand Down
61 changes: 61 additions & 0 deletions nemoclaw-blueprint/policies/integrations/brew.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Internal policy applied by `nemoclaw <name> brew init` and removed by
# `nemoclaw <name> brew deinit`. Not listed as a user-facing preset
# (this file lives outside `policies/presets/`, so `listPresets()` and
# `policy-add` cannot see it). The binaries section is the load-bearing
# half — without it, Landlock denies execve on /home/linuxbrew/.linuxbrew/*
# from inside the sandbox.

preset:
name: __brew_runtime__
description: "Homebrew (Linuxbrew) runtime — applied by `brew init`."

network_policies:
brew:
name: brew
endpoints:
- host: formulae.brew.sh
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: github.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: ghcr.io
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/token" }
- host: pkg-containers.githubusercontent.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: objects.githubusercontent.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- host: raw.githubusercontent.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
binaries:
- { path: /usr/bin/curl }
- { path: /usr/bin/git }
- { path: /home/linuxbrew/.linuxbrew/bin/brew }
- { path: /home/linuxbrew/.linuxbrew/bin/* }
- { path: /home/linuxbrew/.linuxbrew/Homebrew/bin/* }
41 changes: 0 additions & 41 deletions nemoclaw-blueprint/policies/presets/brew.yaml

This file was deleted.

2 changes: 0 additions & 2 deletions nemoclaw-blueprint/policies/tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ tiers:
- { name: npm, access: read-write }
- { name: pypi, access: read-write }
- { name: huggingface, access: read-write }
- { name: brew, access: read-write }
- { name: brave, access: read-write }

- name: open
Expand All @@ -34,7 +33,6 @@ tiers:
- { name: npm, access: read-write }
- { name: pypi, access: read-write }
- { name: huggingface, access: read-write }
- { name: brew, access: read-write }
- { name: brave, access: read-write }
- { name: slack, access: read-write }
- { name: discord, access: read-write }
Expand Down
Loading
Loading