* feat(policy): support host wildcards and multi-port endpoints
Add glob-style host wildcards to endpoints[].host using OPA's
glob.match with "." as delimiter — *.example.com matches a single
DNS label, **.example.com matches across labels. Validation rejects
bare * and requires *. prefix; warns on broad patterns like *.com.
Add repeated uint32 ports field to NetworkEndpoint for multi-port
support. Backwards compatible: existing port scalar is normalized to
ports array. Both the proto-to-JSON and YAML-to-JSON conversion paths
emit a ports array; Rego always references endpoint.ports[_].
Fix OpaEngine::reload() to route through the full preprocessing
pipeline instead of bypassing L7 validation and port normalization.
Closes#359
* fix(policy): reject configs with both port and ports set
---------
Co-authored-by: johntmyers <johntmyers@users.noreply.github.com>
* fix(sandbox): prevent overread request smuggling in L7 REST parser
The HTTP/1.1 parser used a 1024-byte read buffer that could capture
bytes from a pipelined second request. Those overflow bytes were
forwarded upstream as body overflow without L7 policy evaluation,
enabling request smuggling that bypasses per-request method/path
enforcement.
Replace multi-byte read with byte-at-a-time read_u8 that stops exactly
at the CRLFCRLF header terminator. Add regression test proving two
pipelined requests are parsed independently.
Refs: #350
* fix(server): harden sandbox TLS secret volume permissions to 0400
Kubernetes secret volumes default to 0644, allowing the unprivileged
sandbox user to read the mTLS client private key via the Landlock
baseline /etc read set. A compromised sandbox could use the key to
impersonate the control-plane client.
Set defaultMode to 256 (octal 0400, owner-read only) on both the
default and custom pod template paths. The supervisor reads TLS
materials as root before forking, so this does not affect normal
operation.
Refs: #350
* fix(sandbox): stop using cmdline paths for binary policy matching
/proc/pid/cmdline is fully attacker-controlled (argv[0] can be set to
any string via execve) and had no integrity verification, unlike
exec.path and ancestors which are kernel-managed and get TOFU/SHA256
checks. The Rego policy used cmdline_paths as a grant-access signal,
allowing any sandboxed process to claim the identity of an allowed
binary and bypass network restrictions.
Remove the cmdline exact-match rule and exclude cmdline_paths from the
glob-match rule. Only exec.path and exec.ancestors (from /proc/pid/exe)
are now used for binary identity. cmdline_paths remain in the OPA input
for deny-reason diagnostics only.
Refs: #350
* fix(sandbox): reject symlink and non-dir read_write paths before chown
The supervisor runs as root and calls chown on each read_write path.
Since chown follows symlinks, a malicious container image could place a
symlink (e.g. /sandbox -> /etc/shadow) to trick the supervisor into
transferring ownership of arbitrary files to the sandbox user.
Add symlink_metadata (lstat) check before chown to reject symlinks and
non-directory entries. The TOCTOU window is not exploitable because no
untrusted child process has been forked yet at this point.
Refs: #350
* fix(server): redact provider credentials in gRPC CRUD responses
Provider CRUD RPCs (create, get, list, update) returned full Provider
objects including plaintext credentials (API keys, secrets). Any
authenticated client -- including sandbox workloads running untrusted
code -- could read credentials for all providers.
Add redact_provider_credentials helper that clears the credentials map
before returning. Internal server paths (inference routing, sandbox env
injection) read from the store directly and are unaffected. Update
tests to verify redaction and assert persistence via direct store reads.
Refs: #350
* fix(sandbox): enforce non-root fallback when process user unset
drop_privileges silently returned Ok(()) when both run_as_user and
run_as_group were None, even when running as root. In local/dev mode
policies are loaded from disk without passing through the server-side
ensure_sandbox_process_identity normalization, so child processes could
retain root and all capabilities (SYS_ADMIN, NET_ADMIN).
When running as root with no process identity configured, fall back to
sandbox:sandbox instead of no-oping. Non-root runtimes are unaffected.
Refs: #350
* fix(sandbox): deny forward proxy for L7-configured endpoints
The forward proxy path only performed L4 (endpoint) and allowed_ips
checks. If an endpoint had L7 rules (method/path restrictions), a
sandboxed process could bypass them by using HTTP_PROXY with plain
http:// requests instead of CONNECT tunneling, since L7 inspection
only runs in the CONNECT path.
Add a guard in handle_forward_proxy that queries the endpoint's L7
config and returns 403 if any L7 rules are present, forcing traffic
through the CONNECT path where per-request inspection happens.
Refs: #350
* test(e2e): add regression test for forward proxy L7 bypass
Verifies that the forward proxy path (plain http:// via HTTP_PROXY)
returns 403 for endpoints with L7 rules configured, preventing
sandboxed processes from bypassing per-request method/path enforcement
by avoiding the CONNECT tunnel.
Refs: #350
* fix: update tests for cmdline path and TLS volume changes
Update OPA tests to verify cmdline_paths no longer grant access
(regression tests for the security fix). Fix formatting in TLS
volume test.
Refs: #350
* test(e2e): update provider e2e tests for credential redaction
Provider CRUD gRPC responses no longer include credential values.
Update three e2e tests to assert credentials are empty in responses.
Provider functionality is verified by existing e2e tests that check
env var injection into sandboxes (which read from the store directly).
Refs: #350
* chore: reformat for Rust 1.94 assert! macro style
* fix(sandbox): make drop_privileges tests root-aware for CI
CI runs as root but has no 'sandbox' user. The security fix correctly
errors when running as root with no process identity and no fallback
user available -- this is the intended behavior (refuse to run as root).
Update tests to expect that error in root-without-sandbox-user
environments instead of unconditionally asserting Ok.
Refs: #350
* fix(sandbox): allow non-directory read_write entries like /dev/null
The symlink guard incorrectly rejected all non-directory entries.
Character devices like /dev/null are legitimate read_write paths
used in sandbox policies. Only reject symlinks, which are the
actual attack vector for the chown privilege escalation.
Refs: #350
* feat(sandbox): add gpu sandbox scheduling support
Allow sandbox creation to request GPU resources explicitly or infer them from GPU image names. This wires GPU intent through bootstrap, validates gateway support, and adds dedicated GPU E2E coverage for follow-up cluster testing.
UpdateProvider RPC was doing full replacement of credentials and config
maps, silently destroying data on partial updates. This changes the
semantics so empty maps preserve existing values, non-empty maps merge
(upsert), and empty-string values delete individual keys. Also makes
provider type immutable after creation and adds field validation on
update.
Closes#199
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
* feat(proxy): support plain HTTP forward proxy for private IP endpoints
Add forward proxy mode to the sandbox proxy so that standard HTTP
libraries (httpx, requests, etc.) work with HTTP_PROXY for plain HTTP
calls to private IP endpoints. Previously, non-CONNECT methods were
unconditionally rejected with 403.
The forward proxy path requires all three conditions to be met:
- OPA policy explicitly allows the destination
- The matched endpoint has allowed_ips configured
- All resolved IPs are RFC 1918 private
This ensures plain HTTP never reaches the public internet while enabling
seamless access to internal services without custom CONNECT tunnel code.
Implementation:
- parse_proxy_uri(): parses absolute-form URIs into components
- rewrite_forward_request(): rewrites to origin-form, strips hop-by-hop
headers, adds Via and Connection: close
- handle_forward_proxy(): full handler with OPA eval, SSRF checks,
private-IP gate, upstream connect, and bidirectional relay
- Updated dispatch in handle_tcp_connection to route non-CONNECT methods
Includes 14 unit tests and 6 E2E tests (FWD-1 through FWD-6).
CONNECT path remains completely untouched.
Closes#155
* fix(proxy): remove InspectForInference match arm removed by #146
The inference routing simplification in #146 reduced NetworkAction to
Allow/Deny, removing InspectForInference. Drop the dead match arm from
handle_forward_proxy.
* fix(sandbox): restore BestEffort as default Landlock compatibility
The Landlock V2 upgrade in #151 changed the default from BestEffort to
HardRequirement. This causes all proxy-mode sandboxes to crash with
Permission denied when the policy omits the landlock field, because the
child process gets locked to only /etc/navigator-tls and /sandbox.
Restore BestEffort as the default so policies without an explicit
landlock field degrade gracefully.
Fixes#161
* fix(sandbox): inject baseline filesystem paths for proxy-mode sandboxes
Proxy-mode sandboxes need baseline filesystem paths (/usr, /lib, /etc,
/app, /var/log read-only; /sandbox, /tmp read-write) for the child
process to function under Landlock. Without these, the child can't exec
binaries, resolve DNS, or load shared libraries.
The supervisor now enriches the policy with these baseline paths at
startup, covering both standalone (file) and gateway (gRPC) modes. For
gateway mode, the enriched policy is synced back so users see the
effective policy via 'nemoclaw sandbox get'.
The gateway validation is relaxed to allow additive filesystem changes
(new paths can be added, existing paths cannot be removed) to support
the supervisor's enrichment sync-back.
Includes 2 E2E tests: BFS-1 (missing filesystem_policy) and BFS-2
(incomplete filesystem_policy).
Fixes#161
* fix(e2e): update assertion for relaxed filesystem validation message
---------
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
* feat(policy): add validation layer to reject unsafe sandbox policies
Add policy validation that checks for root process identity, path
traversal sequences, overly broad filesystem paths, and exceeding
filesystem rule limits. Validation runs at three entry points:
disk-loaded YAML policies (fallback to restrictive default on violation),
gRPC CreateSandbox, and gRPC UpdateSandboxPolicy (returns
INVALID_ARGUMENT). Filesystem paths are normalized before storage to
collapse traversal components.
Closes#33
* fix(e2e): correct policy update test to match immutable field behavior
The update policy test was asserting on validation errors for fields
(process, filesystem) that are immutable on live sandboxes. The server
rejects changes to these fields before validation runs. Updated the test
to verify the immutability guard instead.
---------
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
Closes#101
Add pytest-xdist for parallel e2e test execution with configurable
concurrency. Default to 5 workers; override via E2E_PARALLEL env var
(accepts a number or 'auto' for CPU-count matching). Make session-scoped
mock inference route fixtures worker-safe by incorporating the xdist
worker_id into route names and routing hints.
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
* refactor(policy): consolidate duplicated YAML struct hierarchies into navigator-policy
Closes#96
Merge the Deserialize-only input structs and Serialize-only output structs
into a single set of types in navigator-policy that derive both Serialize
and Deserialize. This eliminates the duplicate PolicyYaml hierarchy in
navigator-cli and fixes three round-trip issues:
- filesystem_policy vs filesystem field name mismatch
- allowed_ips silently dropped on proto-to-YAML conversion
- network policy name field silently dropped on proto-to-YAML conversion
Also adds api_patterns support to the inference YAML schema and switches
network_policies from HashMap to BTreeMap for deterministic output ordering.
* fix(e2e): update non-CONNECT test assertion from 405 to 403
Align test_l4_non_connect_method_rejected with the proxy behavior
change in c06117e which intentionally returns 403 for non-CONNECT
requests.
---------
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
Closes#42
- Change non-CONNECT proxy response from 405 to 403 to align with
how CONNECT denials are surfaced
- Add structured deny logging for non-CONNECT requests with hostname
extraction from absolute-form URIs
- Revise 7 user-facing error messages across proxy.rs and
dev-sandbox-policy.rego to follow consistent principle: generic
policy-deny messages for non-inference requests, descriptive messages
for recognized inference endpoints
- Update E2E test assertion to match new error message
- Update architecture docs to reflect new behavior
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
* feat(sandbox): allow egress to private IP space via allowed_ips policy field
Add an allowed_ips field to NetworkEndpoint that accepts CIDR notation and
exact IPs, enabling sandboxes to reach private IP space under policy control
while maintaining SSRF protections for loopback and link-local addresses.
Three modes are supported:
- Default (no allowed_ips): all private IPs blocked (existing behavior)
- Host + allowlist: domain must resolve to an IP in allowed_ips
- Hostless allowlist (no host): any domain allowed if resolved IP matches
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
* fix(router): use protocol-aware auth headers for inference proxying
The router always sent `Authorization: Bearer` when proxying to backends,
which breaks Anthropic endpoints that require `x-api-key`. Thread the
source_protocol through to proxy_to_backend and set the appropriate auth
header based on protocol family.
* fix(server): remove noisy inference bundle log
The "serving inference bundle" info log fired every 30s per sandbox on
each route refresh poll. Remove it since there is no equivalent log for
policy serving.
* fix(sandbox): keep inference routing active when cluster bundle is initially empty
In cluster mode, if GetSandboxInferenceBundle returned zero routes at
sandbox startup, build_inference_context() returned None, disabling
inference routing for the sandbox lifetime and preventing
spawn_route_refresh() from starting. Routes created after sandbox
startup were never picked up.
Keep inference interception active with an empty cache in cluster mode
so background refresh can activate routes created later. File-mode
semantics are unchanged.
Closes#78
## What You Can Do
### Live policy updates
Update a running sandbox's network policy without recreating it:
```bash
nav sandbox policy set <sandbox> --policy new-policy.yaml --wait --timeout 60
```
The sandbox hot-reloads the policy within 30s (configurable). On failure, the previous policy stays active (last-known-good).
Idempotent — submitting the same policy twice returns the existing version:
```
✓ Policy version 3 submitted (hash: a1b2c3d4e5f6)
$ nav sandbox policy set test --policy same.yaml
· Policy unchanged (version 3, hash: a1b2c3d4e5f6)
```
### Policy history & inspection
```bash
nav sandbox policy list <sandbox> # version history with status
nav sandbox policy get <sandbox> # current policy metadata
nav sandbox policy get <sandbox> --full # print full policy as YAML
nav sandbox policy get <sandbox> --rev 2 --full # specific revision as YAML
```
### Sandbox logs
Stream logs from both the gateway and sandbox supervisor in one view:
```bash
nav sandbox logs <sandbox> # one-shot, last 2000 lines
nav sandbox logs <sandbox> --tail # live streaming
nav sandbox logs <sandbox> --source sandbox # supervisor logs only
nav sandbox logs <sandbox> --source gateway # gateway logs only
nav sandbox logs <sandbox> --level warn # warnings and errors only
nav sandbox logs <sandbox> --since 5m # last 5 minutes
nav sandbox logs <sandbox> --tail --source sandbox --level info
```
Each log line is tagged with its source and includes structured fields:
```
[1772055394.673] [sandbox] [INFO ] [navigator_sandbox::proxy] CONNECT action=allow dst_host=api.anthropic.com dst_port=443 policy=claude_code
[1772055061.005] [gateway] [INFO ] [navigator_server::grpc] GetSandboxPolicy served from policy history
```
---
## Implementation
### Proto changes
- 7 new RPCs: `UpdateSandboxPolicy`, `GetSandboxPolicyStatus`, `ListSandboxPolicies`, `ReportPolicyStatus`, `GetSandboxLogs`, `PushSandboxLogs` (client-streaming)
- `SandboxLogLine`: added `source` (gateway/sandbox), `fields` (structured key-value map)
- `WatchSandboxRequest`: added `log_since_ms`, `log_sources`, `log_min_level`
- `GetSandboxLogsRequest`: added `sources`, `min_level`
- `PolicyStatus` enum, `SandboxPolicyRevision` message
- `Sandbox.current_policy_version` field
### Server
- **Policy persistence**: New `sandbox_policies` table (SQLite + Postgres) with per-sandbox monotonic versions, status tracking, and policy hash
- **UpdateSandboxPolicy**: Validates static field immutability (filesystem/landlock/process), network mode consistency (Block↔Proxy), deterministic hash comparison for idempotent updates
- **Lazy backfill**: First `GetSandboxPolicy` call creates version 1 from `spec.policy` for existing sandboxes
- **Log broker**: `TracingLogBus::publish_external()` injects sandbox-pushed logs into the same broadcast channel + tail buffer (2000 lines). Server forces `source="sandbox"` and `sandbox_id` on all pushed logs
- **Source/level filtering**: Applied server-side in both `GetSandboxLogs` and `WatchSandbox` streams
- **Version supersession**: When a new version is loaded, all older pending+loaded versions are marked superseded
### Sandbox
- **`OpaEngine::reload_from_proto()`**: Full `from_proto()` pipeline (L7 validation, access preset expansion) with atomic engine swap. On failure, previous engine untouched (LKG)
- **Policy poll loop**: Background task polls every 30s (configurable via `NAVIGATOR_POLICY_POLL_INTERVAL_SECS`), reports status via `ReportPolicyStatus` RPC
- **`LogPushLayer`**: Tracing layer captures events at INFO+ (configurable via `NAVIGATOR_LOG_PUSH_LEVEL`), sends structured fields via `PushSandboxLogs` client-streaming RPC. Background task batches 50 lines / flushes every 500ms. Best-effort (drops on full channel, never blocks)
- **`CachedNavigatorClient`**: Persistent mTLS channel for both policy polling and log push
### Database migration
- `002_create_sandbox_policies.sql` (SQLite + Postgres)
## Tests
- **Unit**: 8 policy persistence tests (put/get/list/status/supersede/isolation)
- **Integration**: 4 test files updated with new RPC stubs
- **E2E**: `test_live_policy_update_and_logs` — full lifecycle: create → set same (unchanged) → push new → wait for load → verify connectivity → push same (unchanged) → verify history → fetch logs
## Documentation
- `architecture/sandbox.md`: Log streaming architecture, LogPushLayer, push task, server broker, source tagging, structured fields, CLI filtering, failure modes
- `architecture/security-policy.md`: Live update semantics, deterministic hashing, CLI filter flags, policy inspection
- `architecture/plans/issue-78-sandbox-log-streaming.md`: Design plan for log streaming
## Security
- Trust boundary documented: shared mTLS cert model (per-sandbox auth tracked in #80)
- Server forces `source="sandbox"` and `sandbox_id` on pushed logs (can't impersonate gateway or other sandboxes)
- Per-batch line cap (100) prevents flooding
> **🏗️ build-from-issue-agent**
Closes#79
## Summary
Replaces the gateway-proxied inference model (`ProxyInference` RPC) with sandbox-local execution. The sandbox now resolves routes from either a standalone YAML route file or a cluster bundle fetched via the new `GetSandboxInferenceBundle` RPC, then forwards requests directly to inference backends using `navigator-router`.
### Benefits of removing the gRPC hop
Moving inference execution from the gateway to the sandbox eliminates the gRPC round-trip for every inference request:
- **No payload size limits**: Direct HTTP forwarding replaces protobuf-over-gRPC serialization.
- **Streaming-ready**: Preserves connection semantics for future SSE support (e.g., `stream: true`).
- **Lower latency**: Direct sandbox → backend path instead of sandbox → gateway → backend round-trip.
- **Reduced gateway load**: Gateway only serves the control-plane bundle delivery RPC.
- **Simpler error model**: HTTP status codes flow through directly without gRPC wrapping.
## Changes Made
- **Proto**: Removed `ProxyInference` RPC. Added `GetSandboxInferenceBundle` RPC for route bundle delivery.
- **Gateway**: Replaced `proxy_inference` with `get_sandbox_inference_bundle`. Removed `Router` and `navigator-router` dependency — gateway is now control-plane only for inference.
- **Router**: Switched config file format from TOML to YAML. Added `Debug` impl that redacts API keys.
- **Sandbox**: New `InferenceContext` with local `Router` + route cache. Routes loaded from `--inference-routes` file (standalone) or cluster bundle via gRPC, with 30s background refresh. New `--inference-routes` / `NAVIGATOR_INFERENCE_ROUTES` CLI arg.
- **Dev sandbox**: Added `inference-routes.yaml` at repo root with default NVIDIA NIM route. `mise run sandbox` now mounts it automatically and supports `-e` flag to forward env vars (e.g., `mise run sandbox`).
- **Example**: Added standalone example (`examples/inference/routes.yaml`) and rewrote README to cover both standalone and cluster workflows.
- **E2E tests**: Updated existing test names/docs. Added tests for Anthropic messages protocol and multi-route policy filtering.
## Tests Added
- **Unit (21 new):** `router_error_to_http` (6 variants), `load_from_file` YAML round-trip (5), `resolve_sandbox_inference_bundle` gRPC handler (6), `build_inference_context` route loading (4)
- **E2E (2 new):** Anthropic messages protocol routing, route filtering by `allowed_routes`
- **Total:** 188 unit tests passing across 3 crates
## Documentation Updated
- `architecture/gateway.md`: Removed router, updated Inference Service
- `architecture/sandbox.md`: Updated orchestration flow, InferenceContext, route loading
- `architecture/inference-routing.md`: Complete rewrite for sandbox-local architecture
## Verification
- [x] All unit tests passing (188 across 3 crates)
- [x] Pre-commit Rust checks passing (fmt, clippy, tests)
- [x] Architecture documentation updated
- [x] E2E tests updated with new test cases
- [x] Standalone example and documentation added
- [x] Default inference-routes.yaml with env passthrough for mise run sandbox
Closes#48, #52
## Summary
- Replace the envoy-gateway-based TLS setup with inline PKI generation during cluster bootstrap, generating CA, server, and client certificates directly in the `navigator-bootstrap` crate
- Remove all envoy gateway Helm templates (`gateway.yaml`, `gatewayclass.yaml`, `grpcroute.yaml`, PKI job, traffic policies) and the `Dockerfile.pki-job`
- Add native mTLS support to the navigator server with `tokio-rustls`, mounting client TLS certs as volumes into sandbox pods
- Update cluster entrypoint, healthcheck, and deploy scripts to work with the new direct-TLS architecture
- Add TLS security e2e test and fix formatting/clippy warnings
## Test Plan
- All unit tests pass (`cargo test --workspace`)
- Clippy clean (`cargo clippy --workspace --all-targets`)
- Format clean (`cargo fmt --all -- --check`)
- Python tests pass (`uv run pytest python/`)
- Full `mise run pre-commit` passes
Closes#67
## Summary
Implements transparent inference interception and routing for sandboxed AI agents. The sandbox proxy intercepts outbound AI SDK calls (OpenAI, Anthropic) and reroutes them through the gateway to policy-controlled backends — enabling organizations to redirect inference traffic to local or self-hosted models without modifying agent code.
**Decision model** — a tri-state OPA evaluation for every CONNECT request:
1. Binary + endpoint explicitly allowed in `network_policies` → **allow** (pass through)
2. Not explicitly allowed + `inference.allowed_routes` configured → **inspect for inference** (TLS intercept, detect API patterns, route through gateway)
3. Otherwise → **deny**
No endpoint declarations or binary lists needed for inference routing. Just configure `inference.allowed_routes`.
## Key Changes
### Sandbox (interception)
- **OPA policy**: New `network_action` Rego rule with three outcomes (`allow`, `inspect_for_inference`, `deny`). New `NetworkAction` enum replaces `PolicyDecision.allowed` bool for the proxy's main decision path.
- **Proxy**: New `InspectForInference` path — TLS-terminates client, parses HTTP, detects inference API patterns (`POST /v1/chat/completions`, `/v1/completions`, `/v1/messages`), strips auth headers, forwards via gRPC.
- **New module**: `l7/inference.rs` — `InferenceApiPattern`, `detect_inference_pattern()`, HTTP request/response parsing.
- **gRPC client**: New `proxy_inference()` for sandbox→gateway forwarding.
- **Sandbox init**: Creates OPA engine when inference is configured, even without `network_policies`.
### Gateway (dispatch)
- **InferenceService**: `ProxyInference` RPC loads sandbox policy, resolves allowed routes, dispatches to router. Full CRUD for inference routes.
- **Proto**: `InferenceRoute`, `InferenceRouteSpec`, `ProxyInferenceRequest/Response`, Inference gRPC service.
### Router (backend proxying)
- **New crate**: `navigator-router` with `Router`, `proxy_with_candidates()`, protocol-based route selection, backend HTTP proxying with auth header rewriting.
- **Mock support** for testing (`mock://` scheme).
### CLI
- `nav inference create/update/delete/list` commands for route management.
### Python SDK
- Updated protobuf bindings. Removed old `inference.py` client (replaced by transparent interception).
### Documentation
- New `architecture/inference-routing.md` — end-to-end system documentation.
- Updated `architecture/sandbox.md` — proxy, OPA, and source index sections.
- Updated `architecture/README.md` — new subsystem overview and diagram.
## Addendum: Chunked Transfer Compatibility
This branch now also fixes intercepted SDK requests that send chunked request bodies:
- `inspect_for_inference` now accepts `Transfer-Encoding: chunked` and decodes chunked request bodies before forwarding to the gateway
- Removed the prior `411 Length Required` response for chunked intercepted requests
- Added request/response header sanitization for framing and hop-by-hop headers (`content-length`, `transfer-encoding`, `connection`, etc.) to keep forwarded requests and returned responses valid
- Added unit tests for chunked parsing and header sanitization
Note: this improves compatibility for streaming-style SDK request patterns; true token-by-token passthrough response streaming is still a separate follow-up.
## Minimal Policy for Inference Routing
```yaml
inference:
allowed_routes:
- local
```
Any outgoing connection from a binary not explicitly allowed in `network_policies` will be intercepted and checked for inference API patterns.
## Test Plan
- [x] `cargo test --workspace` — all tests pass
- [x] `mise run pre-commit` — all checks pass
- [x] E2E: OpenAI chat completions routed through gateway
- [x] E2E: Anthropic messages routed through gateway
- [x] E2E: Python OpenAI SDK from sandbox (`examples/inference/inference.py`)
- [x] E2E test: `e2e/python/test_inference_routing.py`
Closes#44
## Summary
- Add `nav sandbox image push --dockerfile <path>` command to build and push custom container images into the cluster
- Fix sandbox Docker build failure caused by OpenClaw installer TTY access
- Fix sandbox proxy crash when network namespace is unavailable (fall back to loopback)
- Add e2e test for the full custom image build and sandbox creation flow
- Rename and rewrite BYOC architecture doc to reflect current state
## Changes
### `navigator-bootstrap`
- New `build` module (`src/build.rs`) with `build_and_push_image()` public API
- Builds via bollard `Docker::build_image()`, streams output to caller
- `.dockerignore` support with glob matching, 8 unit tests
### `navigator-cli`
- Added `SandboxImageCommands::Push` subcommand with `--dockerfile`, `--tag`, `--context`, `--build-arg` flags
- Added `test:e2e:custom-image` mise task
### `navigator-sandbox`
- Proxy now falls back to `127.0.0.1:3128` when no network namespace or explicit bind address is available, instead of erroring out
### Docker / Infra
- Sandbox Dockerfile: replaced OpenClaw native installer (`curl | bash`) with `npm install -g openclaw` to avoid `/dev/tty` failures in Docker build
### Documentation
- Renamed `architecture/sandbox-byoc.md` to `sandbox-custom-containers.md`
- Rewrote doc to describe current behavior (CLI usage, push pipeline, supervisor behavior, design decisions) instead of listing implementation changes
### Tests
- Added `e2e/bash/test_sandbox_custom_image.sh` covering image push + sandbox create with custom image
> **🔧 security-fix-agent**
Closes#62
## Security Fix
### Summary
The CONNECT proxy accepted hostnames from clients and connected to whatever IP they resolved to, with no validation against internal address ranges. While the OPA policy is default-deny, a misconfigured or overly permissive policy could allow SSRF to cloud metadata (169.254.169.254), localhost, or RFC1918 services. This fix adds DNS resolution before connecting and rejects any host that resolves to an internal IP.
### Severity Assessment
- **Impact:** Medium — if exploited, could reach cloud metadata (IAM creds), cluster-internal services, or host-local services
- **Exploitability:** Very low — requires OPA policy misconfiguration or DNS rebinding attack
- **Affected components:** `crates/navigator-sandbox/src/proxy.rs` — `handle_tcp_connection`
### Changes Made
- `crates/navigator-sandbox/src/proxy.rs`: Added `is_internal_ip()` helper that checks IPv4 loopback/private/link-local, IPv6 loopback/link-local, and IPv4-mapped IPv6. Added `resolve_and_reject_internal()` that resolves DNS and rejects internal IPs. Inserted check between OPA allow and `TcpStream::connect`, with control plane endpoints exempt.
- `architecture/security-policy.md`: Added SSRF Protection section with blocked ranges table and flow diagram
- `architecture/sandbox.md`: Updated proxy connection flow diagram and added SSRF protection subsection
- `architecture/README.md`: Added internal IP rejection step to proxy description
### Tests Added
- **Unit:** 17 tests in `proxy::tests` — covers IPv4 loopback/private/link-local, IPv6 loopback/link-local, IPv4-mapped IPv6, public IPs, DNS resolution of localhost/127.0.0.1/169.254.169.254, and DNS failure handling
- **Integration/E2E:** N/A — the proxy runs inside a Linux network namespace; unit tests for IP checking and DNS resolution cover the security boundary
### Documentation Updated
- `architecture/security-policy.md`: New SSRF Protection section with blocked IP ranges and Mermaid flowchart
- `architecture/sandbox.md`: Updated proxy flow diagram and added SSRF protection subsection
- `architecture/README.md`: Added step 4 to proxy description
### Verification
All 85 sandbox tests pass including 17 new proxy SSRF tests. Pre-commit (fmt, clippy, full test suite) passes clean with zero warnings.
## Summary
- Add `GetSandboxProviderEnvironment` gRPC endpoint so the sandbox supervisor can fetch provider credentials at runtime instead of embedding them in the pod spec
- Sandbox supervisor (`navigator-sandbox`) fetches credentials on startup and injects them as environment variables into both entrypoint processes and SSH shell sessions via `Command::env()`
- Remove credential injection from sandbox creation-time pod spec in `navigator-server`
- Update `architecture/providers.md` to document the full runtime credential injection flow, discovery engine details, trait definitions, and end-to-end diagram
## Changes
- **proto**: Add `GetSandboxProviderEnvironment` RPC and request/response messages, add `providers` field to `SandboxSpec`
- **navigator-server**: Implement `GetSandboxProviderEnvironment` handler and `resolve_provider_environment()` with env var key validation and first-wins dedup; remove old creation-time credential injection from `sandbox/mod.rs`
- **navigator-sandbox**: Fetch provider env via gRPC on startup (`grpc_client.rs`), thread `provider_env` HashMap through to `process.rs` (entrypoint) and `ssh.rs` (shell sessions), inject via `cmd.env()`
- **navigator-cli**: Adjust sandbox create flow to set provider names in `SandboxSpec.providers`
- **e2e**: Add `test_sandbox_providers.py` end-to-end tests
- **docs**: Rewrite `architecture/providers.md` with implementation details
## Test Plan
- Unit tests for `resolve_provider_environment` in `grpc.rs`
- Integration tests updated in `provider_commands_integration.rs`, `mtls_integration.rs`, `multiplex_integration.rs`, `multiplex_tls_integration.rs`
- New e2e test suite `test_sandbox_providers.py`
Closes#13
## Summary
- add Python sandbox execution APIs for command and callable workflows
- consolidate sandbox policy fixtures and expand e2e test coverage for policy and Python exec paths
- update CI/build config and images for sandbox e2e execution dependencies
## Test Plan
- mise run pre-commit