Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .agents/skills/create-spike/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ User says: "Allow sandbox egress to private IP space via networking policy"
1. Problem is clear — no clarification needed
2. Fire `principal-engineer-reviewer` to investigate:
- Finds `is_internal_ip()` SSRF check in `proxy.rs` that blocks RFC 1918 addresses
- Reads OPA policy evaluation pipeline in `opa.rs` and `dev-sandbox-policy.rego`
- Reads OPA policy evaluation pipeline in `opa.rs` and `crates/navigator-sandbox/data/sandbox-policy.rego`
- Reads proto definitions in `sandbox.proto` for `NetworkEndpoint`
- Maps the 4-layer defense model: netns, seccomp, OPA, SSRF check
- Reads `architecture/security-policy.md` and `architecture/sandbox.md`
Expand Down
14 changes: 7 additions & 7 deletions .agents/skills/generate-sandbox-policy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Key sections to reference:
Also read the example policy for real-world patterns:

```
Read dev-sandbox-policy.yaml
Read deploy/docker/sandbox/dev-sandbox-policy.yaml
```

## Step 4: Choose Policy Shape
Expand Down Expand Up @@ -355,8 +355,8 @@ The policy needs to go somewhere. Determine which mode applies:

| Signal | Mode |
|--------|------|
| User names an existing policy file (e.g., "add to dev-sandbox-policy.yaml") | **Update existing file** |
| User says "update my policy", "add this to my policy file" | **Update existing file** — look for `dev-sandbox-policy.yaml` or ask which file |
| User names an existing policy file (e.g., "add to deploy/docker/sandbox/dev-sandbox-policy.yaml") | **Update existing file** |
| User says "update my policy", "add this to my policy file" | **Update existing file** — look for `deploy/docker/sandbox/dev-sandbox-policy.yaml` or ask which file |
| User asks to modify an existing policy rule by name | **Update existing file** — edit the named policy in place |
| User says "create a new policy file" or names a file that doesn't exist | **Create new file** |
| No file context given | **Present only** — show the YAML and ask if the user wants it written to a file |
Expand Down Expand Up @@ -418,7 +418,7 @@ inference:

The `filesystem_policy`, `landlock`, `process`, and `inference` sections above are sensible defaults. Tell the user these are defaults and may need adjustment for their environment. The generated `network_policies` block is the primary output.

If the user provides a file path, write to it. Otherwise, suggest `dev-sandbox-policy.yaml` for local development or ask where to place it.
If the user provides a file path, write to it. Otherwise, suggest `deploy/docker/sandbox/dev-sandbox-policy.yaml` for local development or ask where to place it.

### Mode C: Present Only (no file write)

Expand All @@ -427,7 +427,7 @@ Show the generated policy YAML with:
1. **Summary** — what the policy allows and denies, in plain language
2. **The YAML** — the complete `network_policies` block, ready to paste
3. **Integration guidance**:
- For local dev: add to `dev-sandbox-policy.yaml` under `network_policies`
- For local dev: add to `deploy/docker/sandbox/dev-sandbox-policy.yaml` under `network_policies`
- For production: configure via the gateway
4. **Caveats** — any assumptions made, anything the user should verify

Expand Down Expand Up @@ -545,6 +545,6 @@ private_services:
## Additional Resources

- Full policy schema: [architecture/security-policy.md](../../../architecture/security-policy.md)
- Example policy file: [dev-sandbox-policy.yaml](../../../dev-sandbox-policy.yaml)
- Rego evaluation rules: [dev-sandbox-policy.rego](../../../dev-sandbox-policy.rego)
- Example policy file: [dev-sandbox-policy.yaml](../../../deploy/docker/sandbox/dev-sandbox-policy.yaml)
- Rego evaluation rules: [sandbox-policy.rego](../../../crates/navigator-sandbox/data/sandbox-policy.rego)
- For translation examples from real API docs, see [examples.md](examples.md)
12 changes: 6 additions & 6 deletions .agents/skills/generate-sandbox-policy/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,11 +729,11 @@ An exact IP is treated as `/32` — only that specific address is permitted.

### Example F1: Add a New Policy to an Existing File

**User**: "Add read-only access to api.github.com for curl to my dev-sandbox-policy.yaml"
**User**: "Add read-only access to api.github.com for curl to my deploy/docker/sandbox/dev-sandbox-policy.yaml"

**Agent workflow**:

1. Read `dev-sandbox-policy.yaml`
1. Read `deploy/docker/sandbox/dev-sandbox-policy.yaml`
2. Check that no existing policy already covers `api.github.com:443` — if one does, warn about overlap
3. Check that the key `github_readonly` doesn't already exist
4. Insert the new policy under `network_policies`:
Expand All @@ -760,11 +760,11 @@ The agent uses `StrReplace` to insert after the last existing policy in the `net

### Example F2: Modify an Existing Policy (Add an Endpoint)

**User**: "Add sentry.io to the claude_code policy in dev-sandbox-policy.yaml"
**User**: "Add sentry.io to the claude_code policy in deploy/docker/sandbox/dev-sandbox-policy.yaml"

**Agent workflow**:

1. Read `dev-sandbox-policy.yaml`
1. Read `deploy/docker/sandbox/dev-sandbox-policy.yaml`
2. Find the `claude_code` policy
3. Check that `sentry.io:443` isn't already listed in its endpoints
4. Add the new endpoint to the existing `endpoints` list:
Expand Down Expand Up @@ -878,11 +878,11 @@ The agent notes that `filesystem_policy`, `landlock`, `process`, and `inference`

### Example F5: Handle a Key Conflict

**User**: "Add an nvidia policy to dev-sandbox-policy.yaml"
**User**: "Add an nvidia policy to deploy/docker/sandbox/dev-sandbox-policy.yaml"

**Agent workflow**:

1. Read `dev-sandbox-policy.yaml`
1. Read `deploy/docker/sandbox/dev-sandbox-policy.yaml`
2. Find that a policy key `nvidia` already exists
3. **Ask the user**: "A policy named `nvidia` already exists. Do you want to replace it, add endpoints to it, or use a different name (e.g., `nvidia_inference_v2`)?"
4. Proceed based on the user's answer
2 changes: 1 addition & 1 deletion .claude/agent-memory/arch-doc-writer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
- Naming convention: "gateway" in prose for the control plane component; code identifiers like `navigator-server` stay unchanged

## Key Patterns
- OPA baked-in rules: `include_str!("../../../dev-sandbox-policy.rego")` in opa.rs
- OPA baked-in rules: `include_str!("../data/sandbox-policy.rego")` in opa.rs
- Policy loading: gRPC mode (NAVIGATOR_SANDBOX_ID + NAVIGATOR_ENDPOINT) or file mode (--policy-rules + --policy-data)
- Provider env injection: both entrypoint process (tokio Command) and SSH shell (std Command)
- Cluster bootstrap: `sandbox_create_with_bootstrap()` auto-deploys when no cluster exists (main.rs ~line 632)
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ The inference routing system transparently intercepts AI inference API calls fro

| Component | Location | Role |
|---|---|---|
| OPA `network_action` rule | `dev-sandbox-policy.rego` | Returns `inspect_for_inference` when no explicit policy match and inference routes exist |
| OPA `network_action` rule | `crates/navigator-sandbox/data/sandbox-policy.rego` | Returns `inspect_for_inference` when no explicit policy match and inference routes exist |
| Proxy interception | `crates/navigator-sandbox/src/proxy.rs` | TLS-terminates intercepted connections, parses HTTP, calls gateway |
| Inference pattern detection | `crates/navigator-sandbox/src/l7/inference.rs` | Matches HTTP method + path against known inference API patterns |
| gRPC forwarding | `crates/navigator-sandbox/src/grpc_client.rs` | Sends `ProxyInferenceRequest` to the gateway |
Expand Down
2 changes: 1 addition & 1 deletion architecture/build-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ All builds use mise tasks defined in `tasks/*.toml` (included from `mise.toml`).
| `crates/navigator-core/*`, `crates/navigator-providers/*` | Gateway + sandbox rebuild |
| `crates/navigator-router/*` | Gateway rebuild |
| `crates/navigator-server/*`, `deploy/docker/Dockerfile.server` | Gateway rebuild |
| `crates/navigator-sandbox/*`, `deploy/docker/sandbox/*`, `deploy/docker/openclaw-start.sh`, `python/*`, `pyproject.toml`, `uv.lock`, `dev-sandbox-policy.rego` | Sandbox rebuild |
| `crates/navigator-sandbox/*`, `deploy/docker/sandbox/*`, `deploy/docker/openclaw-start.sh`, `python/*`, `pyproject.toml`, `uv.lock`, `crates/navigator-sandbox/data/sandbox-policy.rego` | Sandbox rebuild |
| `deploy/helm/navigator/*` | Helm upgrade |

**Explicit target mode** (arguments: `server`, `sandbox`, `chart`, `all`): Rebuilds only the specified components.
Expand Down
8 changes: 4 additions & 4 deletions architecture/inference-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The inference routing system transparently intercepts AI inference API calls fro
| `crates/navigator-sandbox/src/main.rs` | Sandbox binary CLI: `--inference-routes` / `NEMOCLAW_INFERENCE_ROUTES` flag definition |
| `tasks/ci.toml` | `[sandbox]` task: mounts `inference-routes.yaml`, sets env vars for dev sandbox |
| `inference-routes.yaml` | Default standalone routes for dev sandbox (NVIDIA API endpoint) |
| `dev-sandbox-policy.rego` | `network_action` Rego rule -- tri-state decision logic |
| `crates/navigator-sandbox/data/sandbox-policy.rego` | `network_action` Rego rule -- tri-state decision logic |

## Architecture Overview

Expand Down Expand Up @@ -174,7 +174,7 @@ The `evaluate_network_action()` method evaluates `data.navigator.sandbox.network

### Rego rules

**File:** `dev-sandbox-policy.rego`
**File:** `crates/navigator-sandbox/data/sandbox-policy.rego`

```rego
default network_action := "deny"
Expand Down Expand Up @@ -567,8 +567,8 @@ The `create` and `update` commands perform protocol auto-detection when `--proto

Running `mise run cluster:sandbox` starts a standalone sandbox container with inference routing pre-configured. The task mounts three files into the container:

- `dev-sandbox-policy.rego` as `/var/navigator/policy.rego`
- `dev-sandbox-policy.yaml` as `/var/navigator/data.yaml`
- `crates/navigator-sandbox/data/sandbox-policy.rego` as `/var/navigator/policy.rego`
- `deploy/docker/sandbox/dev-sandbox-policy.yaml` as `/var/navigator/data.yaml`
- `inference-routes.yaml` as `/var/navigator/inference-routes.yaml`

The container receives `NEMOCLAW_INFERENCE_ROUTES=/var/navigator/inference-routes.yaml` to enable standalone inference routing. `NVIDIA_API_KEY` is always forwarded from the host environment (empty string if unset).
Expand Down
2 changes: 1 addition & 1 deletion architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ The OPA engine lives in `crates/navigator-sandbox/src/opa.rs` and uses the `rego

### Baked-in rules

The Rego rules are compiled into the binary via `include_str!("../../../dev-sandbox-policy.rego")`. The package is `navigator.sandbox`. Key rules:
The Rego rules are compiled into the binary via `include_str!("../data/sandbox-policy.rego")`. The package is `navigator.sandbox`. Key rules:

| Rule | Type | Purpose |
|------|------|---------|
Expand Down
12 changes: 6 additions & 6 deletions architecture/security-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Provide a Rego rules file and a YAML data file via CLI flags or environment vari

```bash
navigator-sandbox \
--policy-rules dev-sandbox-policy.rego \
--policy-rules sandbox-policy.rego \
--policy-data dev-sandbox-policy.yaml \
-- /bin/bash
```
Expand Down Expand Up @@ -45,7 +45,7 @@ navigator-sandbox \
| `--sandbox-id` | `NEMOCLAW_SANDBOX_ID` | Sandbox ID for policy lookup |
| `--nemoclaw-endpoint` | `NEMOCLAW_ENDPOINT` | Gateway gRPC endpoint |

The gateway returns a `SandboxPolicy` protobuf message (defined in `proto/sandbox.proto`). The sandbox supervisor converts this proto into JSON, validates L7 config, expands presets, and loads it into the OPA engine using baked-in Rego rules (`dev-sandbox-policy.rego` compiled via `include_str!`). See `crates/navigator-sandbox/src/opa.rs` -- `OpaEngine::from_proto()`.
The gateway returns a `SandboxPolicy` protobuf message (defined in `proto/sandbox.proto`). The sandbox supervisor converts this proto into JSON, validates L7 config, expands presets, and loads it into the OPA engine using baked-in Rego rules (`sandbox-policy.rego` compiled via `include_str!`). See `crates/navigator-sandbox/src/opa.rs` -- `OpaEngine::from_proto()`.

### Policy Loading Sequence

Expand Down Expand Up @@ -421,7 +421,7 @@ Each endpoint defines a network destination and, optionally, L7 inspection behav
| ------ | -------- | -------- | ------------------------------------------------------------------ |
| `path` | `string` | Yes | Filesystem path of the binary. Supports glob patterns (`*`, `**`). |

**Binary identity matching** is evaluated in the Rego rules (`dev-sandbox-policy.rego`) using four strategies, tried in order:
**Binary identity matching** is evaluated in the Rego rules (`sandbox-policy.rego`) using four strategies, tried in order:

1. **Direct path match** -- `exec.path == binary.path`
2. **Ancestor match** -- any entry in `exec.ancestors` matches `binary.path`
Expand Down Expand Up @@ -450,7 +450,7 @@ rules:
| `path` | `string` | URL path glob pattern: `**` matches everything, otherwise `glob.match` with `/` delimiter. |
| `command` | `string` | SQL command: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, or `*` (any). Case-insensitive matching. For `protocol: sql` endpoints. |

Method and command fields use `*` as wildcard for "any". Path patterns use `**` for "match everything" and standard glob patterns with `/` as a delimiter otherwise. See `dev-sandbox-policy.rego` -- `method_matches()`, `path_matches()`, `command_matches()`.
Method and command fields use `*` as wildcard for "any". Path patterns use `**` for "match everything" and standard glob patterns with `/` as a delimiter otherwise. See `sandbox-policy.rego` -- `method_matches()`, `path_matches()`, `command_matches()`.

#### Access Presets

Expand Down Expand Up @@ -524,7 +524,7 @@ flowchart LR

This is the single most important behavioral trigger in the policy language. An endpoint with no `protocol` field passes traffic opaquely after the L4 (CONNECT) check. Adding `protocol: rest` activates per-request HTTP parsing and policy evaluation inside the proxy.

**Implementation path**: After L4 CONNECT is allowed, the proxy calls `query_l7_config()` which evaluates the Rego rule `data.navigator.sandbox.matched_endpoint_config`. This rule only matches endpoints that have a `protocol` field set (see `dev-sandbox-policy.rego` line `ep.protocol`). If a config is returned, the proxy enters `relay_with_inspection()` instead of `copy_bidirectional()`. See `crates/navigator-sandbox/src/proxy.rs` -- `handle_tcp_connection()`.
**Implementation path**: After L4 CONNECT is allowed, the proxy calls `query_l7_config()` which evaluates the Rego rule `data.navigator.sandbox.matched_endpoint_config`. This rule only matches endpoints that have a `protocol` field set (see `sandbox-policy.rego` line `ep.protocol`). If a config is returned, the proxy enters `relay_with_inspection()` instead of `copy_bidirectional()`. See `crates/navigator-sandbox/src/proxy.rs` -- `handle_tcp_connection()`.

**Validation requirement**: When `protocol` is set, either `rules` or `access` must also be present. An endpoint with `protocol` but no rules/access is rejected at validation time because it would deny all traffic (no allow rules means nothing matches). See `crates/navigator-sandbox/src/l7/mod.rs` -- `validate_l7_policies()`.

Expand Down Expand Up @@ -1055,7 +1055,7 @@ The OPA engine evaluates two categories of rules:
| `allow_request` | `input.network.*`, `input.exec.*`, `input.request.method`, `input.request.path` | `true` if the request matches any rule in the matched endpoint |
| `request_deny_reason` | Same input | Human-readable deny message |

See `dev-sandbox-policy.rego` for the full Rego implementation.
See `sandbox-policy.rego` for the full Rego implementation.

---

Expand Down
14 changes: 7 additions & 7 deletions crates/navigator-bootstrap/src/kubeconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,13 @@ pub fn rewrite_kubeconfig(contents: &str, cluster_name: &str, kube_port: Option<
let mut replaced = Vec::new();
for line in contents.lines() {
let trimmed = line.trim_start();
if let Some(kp) = kube_port {
if trimmed.starts_with("server:") {
let indent_len = line.len() - trimmed.len();
let indent = &line[..indent_len];
replaced.push(format!("{indent}server: https://127.0.0.1:{kp}"));
continue;
}
if let Some(kp) = kube_port
&& trimmed.starts_with("server:")
{
let indent_len = line.len() - trimmed.len();
let indent = &line[..indent_len];
replaced.push(format!("{indent}server: https://127.0.0.1:{kp}"));
continue;
}
// Rename default cluster/context/user to the cluster name
// Handle both "name: default" and "- name: default" (YAML list item)
Expand Down
2 changes: 1 addition & 1 deletion crates/navigator-bootstrap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ fn default_cluster_image_ref() -> String {
image::pull_registry_image()
}

/// Create the three TLS K8s secrets required by the NemoClaw server and sandbox pods.
/// Create the three TLS K8s secrets required by the `NemoClaw` server and sandbox pods.
///
/// Secrets are created via `kubectl` exec'd inside the cluster container:
/// - `navigator-server-tls` (kubernetes.io/tls): server cert + key
Expand Down
24 changes: 24 additions & 0 deletions crates/navigator-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,17 @@ enum SandboxCommands {
#[arg(long)]
forward: Option<u16>,

/// Allocate a pseudo-terminal for the remote command.
/// Defaults to auto-detection (on when stdin and stdout are terminals).
/// Use --tty to force a PTY even when auto-detection fails, or
/// --no-tty to disable.
#[arg(long, overrides_with = "no_tty")]
tty: bool,

/// Disable pseudo-terminal allocation.
#[arg(long, overrides_with = "tty")]
no_tty: bool,

/// Command to run after "--" (defaults to an interactive shell).
#[arg(trailing_var_arg = true)]
command: Vec<String>,
Expand Down Expand Up @@ -918,8 +929,19 @@ async fn main() -> Result<()> {
providers,
policy,
forward,
tty,
no_tty,
command,
} => {
// Resolve --tty / --no-tty into an Option<bool> override.
let tty_override = if no_tty {
Some(false)
} else if tty {
Some(true)
} else {
None // auto-detect
};

// For `sandbox create`, a missing cluster is not fatal — the
// bootstrap flow inside `sandbox_create` can deploy one.
match resolve_cluster(&cli.cluster) {
Expand Down Expand Up @@ -947,6 +969,7 @@ async fn main() -> Result<()> {
policy.as_deref(),
forward,
&command,
tty_override,
&tls,
)
.await?;
Expand All @@ -964,6 +987,7 @@ async fn main() -> Result<()> {
policy.as_deref(),
forward,
&command,
tty_override,
)
.await?;
}
Expand Down
Loading
Loading