Skip to content
Open
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
80 changes: 80 additions & 0 deletions crates/nono-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,86 @@ Profiles can form chains (up to 10 levels deep). Circular dependencies are detec
my-dev.json → team-base.json → claude-code (pack)
```

## Network Modes

nono has three network modes. You pick one per run; they cannot be combined.

| Mode | CLI flag | Profile field | What it does |
|------|----------|---------------|--------------|
| **Unrestricted** | *(default)* | — | Child has full network access |
| **Blocked** | `--block-net` | `"network": { "block": true }` | All outbound connections denied |
| **Proxy-only** | `--allow-domain <host>` | `"network": { "allow_domain": [...] }` | Child may only reach the nono proxy; proxy enforces an allowlist |

### Localhost IPC ports

Use `open_port` and `open_port_range` to let the sandboxed process talk to other processes running on the same machine — dev servers, databases, test containers, sidecar agents, etc. These only take effect in blocked or proxy mode; in unrestricted mode all ports are already open.

**Single port:**
```bash
nono run --allow-cwd --block-net --open-port 3000 -- my-agent
```
```json
{ "network": { "block": true, "open_port": [3000, 3001] } }
```

**Port range** (profile only, works on both platforms):
```json
{ "network": { "block": true, "open_port_range": [[3000, 3100]] } }
```

**Wildcard — allow any localhost port** (proxy mode only):
```json
{
"network": {
"allow_domain": ["api.example.com"],
"open_port": [0]
}
}
```

Use `open_port: [0]` when the port isn't known ahead of time — for example, Testcontainers and Maven Surefire bind to a random ephemeral port assigned by the OS. The `allow_domain` list still restricts what external hosts the agent can reach; only localhost traffic is unrestricted.

> **Note:** `open_port: [0]` (wildcard) requires proxy mode on Linux. It will error at startup if used with `--block-net` on Linux, because block-net has no mechanism to express "any port". Use proxy mode with `allow_domain` instead.

#### Platform differences

On macOS, when any port or range is set, inbound connections to those ports are also allowed (macOS cannot filter inbound by port). On Linux, inbound is not automatically allowed — only the ports you list are open.

On macOS, ranges over 256 ports collapse to a full localhost wildcard with a warning in the logs. On Linux, all ports in the range are allowed individually regardless of range width.

### Proxy-only mode (`--allow-domain`)

Proxy mode restricts the agent to only the domains you list. All other outbound traffic is blocked.

```bash
nono run --allow-cwd --allow-domain api.openai.com -- my-agent
```

```json
{
"network": {
"allow_domain": ["api.openai.com", "registry.npmjs.org"],
"open_port_range": [[3000, 3002]]
}
}
```

`open_port` and `open_port_range` work alongside `allow_domain` — the domain list controls external traffic, and the port exceptions control localhost IPC. They are independent.

### Blocking external network while allowing localhost IPC

```json
{
"network": {
"block": true,
"open_port": [6379],
"open_port_range": [[49152, 49200]]
}
}
```

Works on both macOS and Linux.

## Deprecated Command Blocking

Command blocking is deprecated in `v0.33.0`. It is only checked against the
Expand Down
3 changes: 3 additions & 0 deletions crates/nono-cli/src/capability_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,9 @@ impl CapabilitySetExt for CapabilitySet {
for port in &profile.network.open_port {
caps.add_localhost_port(*port);
}
for &[start, end] in &profile.network.open_port_range {
caps.add_localhost_port_range(start, end);
}

// Outbound TCP connect port allowlist from profile (Linux Landlock V4+ only)
#[cfg(target_os = "macos")]
Expand Down
8 changes: 6 additions & 2 deletions crates/nono-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,9 @@ pub struct SandboxArgs {
)]
pub allow_bind: Vec<u16>,

/// Allow bidirectional localhost TCP on a port: connect + listen (repeatable)
/// Allow bidirectional localhost TCP on a port: connect + listen (repeatable).
/// Port 0 = localhost wildcard (any port); macOS and Linux proxy mode only.
/// For ranges use open_port_range in a profile (no CLI flag equivalent).
/// ALIAS(canonical="--open-port", introduced="v0.0.0", remove_by="indefinite", issue="#415")
#[arg(
long = "open-port",
Expand Down Expand Up @@ -1530,7 +1532,9 @@ pub struct WrapSandboxArgs {
)]
pub allow_bind: Vec<u16>,

/// Allow bidirectional localhost TCP on a port: connect + listen (repeatable)
/// Allow bidirectional localhost TCP on a port: connect + listen (repeatable).
/// Port 0 = localhost wildcard (any port); macOS and Linux proxy mode only.
/// For ranges use open_port_range in a profile (no CLI flag equivalent).
/// ALIAS(canonical="--open-port", introduced="v0.0.0", remove_by="indefinite", issue="#415")
#[arg(
long = "open-port",
Expand Down
51 changes: 51 additions & 0 deletions crates/nono-cli/src/exec_strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ pub struct SupervisorConfig<'a> {
/// Bind ports allowed for seccomp proxy-only fallback.
#[cfg(target_os = "linux")]
pub proxy_bind_ports: Vec<u16>,
/// Localhost IPC ports for seccomp proxy-only fallback. Port 0 = wildcard
/// (any loopback port); otherwise exact ports for connect and bind.
#[cfg(target_os = "linux")]
pub localhost_ports: Vec<u16>,
/// Localhost IPC port ranges `[start, end]` (inclusive) for seccomp proxy-only.
#[cfg(target_os = "linux")]
pub localhost_port_ranges: Vec<(u16, u16)>,
/// Pathname AF_UNIX socket grants allowed for seccomp proxy-only fallback.
#[cfg(target_os = "linux")]
pub unix_socket_allowlist: &'a [nono::UnixSocketCapability],
Expand Down Expand Up @@ -4585,6 +4592,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -4705,6 +4716,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -4791,6 +4806,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -4834,6 +4853,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -4875,6 +4898,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand All @@ -4898,6 +4925,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -4944,6 +4975,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -5095,6 +5130,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -5146,6 +5185,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -5186,6 +5229,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down Expand Up @@ -5245,6 +5292,10 @@ mod tests {
#[cfg(target_os = "linux")]
proxy_bind_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_ports: Vec::new(),
#[cfg(target_os = "linux")]
localhost_port_ranges: Vec::new(),
#[cfg(target_os = "linux")]
unix_socket_allowlist: &[],
#[cfg(target_os = "linux")]
linux_network_notify_mode: LinuxNetworkNotifyMode::ProxyOnly,
Expand Down
Loading
Loading