* fix(security): harden sandbox SSH with mandatory HMAC secret, NetworkPolicy, and nonce replay detection
Closes#25
- Make NEMOCLAW_SSH_HANDSHAKE_SECRET mandatory: server and sandbox both
refuse to start if the secret is empty/unset. Cluster deployments
auto-generate it via openssl rand in the entrypoint script.
- Add Kubernetes NetworkPolicy restricting sandbox port 2222 ingress to
the gateway pod only, preventing lateral movement from other cluster
workloads.
- Add NSSH1 nonce replay detection with a TTL-bounded cache, rejecting
replayed handshakes within the timestamp validity window.
- Add unit tests for verify_preface (valid, replay, expired, bad HMAC,
malformed) and env injection.
* fix(deploy): pass sshHandshakeSecret in fast deploy helm upgrade
---------
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>
The gsactions/dco-check action repository was deleted, causing every DCO
workflow run to fail with startup_failure. Replace it with
contributor-assistant/github-action pinned to a commit SHA, matching the
pattern used by NVIDIA-NeMo/DataDesigner.
Signed-off-by: John Myers <johntmyers@users.noreply.github.com>
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>
* feat(skills): create nemoclaw-cli agent skill and fix generate-sandbox-policy schema drift
Closes#83
Create a new nemoclaw-cli agent skill with tiered workflow guidance (basic
through advanced) and a supplementary CLI reference. The skill covers provider
management, sandbox lifecycle, policy iteration loops, BYOC pipelines, and
agent-assisted sandbox sessions. It cross-references generate-sandbox-policy
for policy content authoring and includes --help self-teaching as a fallback.
Fix critical schema drift in generate-sandbox-policy: rename
allowed_routing_hints to allowed_routes (matching the actual proto/serde
field name). Update default read_only paths to match dev-sandbox-policy.yaml.
Fix the same field name error in architecture/security-policy.md.
* policy updates
* fix(test): update OPA tests for renamed github_ssh_over_https policy key
---------
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
The sandbox SSH server used tokio::spawn fire-and-forget for each stdout/stderr
chunk, which provided no ordering guarantee under tokio's work-stealing scheduler.
Under load, chunks arrived out-of-order, corrupting tar streams during download.
Replace tokio::spawn with runtime.block_on() to ensure each chunk is fully sent
before reading the next. Remove AsyncWriteExt::shutdown() from the SSH proxy's
copy helper to prevent gateways from treating TCP half-close as full-close and
truncating in-flight downloads.
Implement channel_eof handler on the sandbox SSH server to close the child
process stdin when the client signals EOF. Without this, commands like
'cat | tar xf -' used by sync --up would hang waiting for stdin EOF
indefinitely. Add ValueHint::AnyPath to the --up argument for shell completion.
* feat(tui): add port forwarding support to Gator (#80)
Extract forward PID management, resolve_ssh_gateway, and shell_escape from
navigator-cli into navigator-core::forward as a shared module. Add a Ports
field to the TUI create sandbox modal, a NOTES column to the sandbox table,
and a Forwards row to the sandbox detail view. On creation with ports, Gator
polls for Ready state then spawns background SSH tunnels. Forward cleanup
runs automatically on sandbox delete.
Closes#80
* fix(tui): fix Ports field invisible due to modal height overflow
The modal height calculation used content_height + 3 but block chrome
(borders + padding) is 4 rows, not 3. Combined with the 3-row Ports
field layout, the input line was clipped to zero height on standard
24-row terminals. Switch Ports to a compact single-line inline layout
and fix the chrome arithmetic.
* wip: BYOC example with port forwarding and TUI command field fix
- Move examples/bring-your-own-container.md into its own directory
- Add example Dockerfile and app.py (Python REST API with /hello endpoint)
- Rewrite README with full CLI and TUI port-forward workflows
- Change TUI Command field default from /bin/bash to empty so custom
image entrypoints run without manual clearing
* feat(tui): wire up Command field to SSH exec after sandbox creation
When a command is specified in the create sandbox modal, the TUI now
waits for the sandbox to reach Ready (with the pacman animation),
starts any port forwards, then suspends the TUI and executes the
command via SSH — matching the CLI's post-creation flow.
- Refactor spawn_create_sandbox to poll for Ready inline when ports
or command are set, keeping the animation visible throughout
- Add start_port_forwards() helper called within the create task
- Add handle_exec_command() to suspend TUI and run SSH exec
- Remove unused ForwardResult event variant (forwards are now started
within the create task, not as a separate event)
* fix(tui): fix command exec, forward timeouts, and create modal UX
- Fix handle_exec_command to suspend TUI and run SSH attached (matching
CLI behavior) so the process stays alive for the session duration
- Fix shell_escape double-quoting: escape each word individually
- Add ConnectTimeout and 20s spawn timeout to forward SSH so a stalled
auth doesn't freeze the create flow forever
- Remove dead ForwardResult event variant
- Add cluster_name to start_port_forwards for ProxyCommand
- Add spacer between Providers and Ports in create modal
- Fix Command placeholder: 'runs /bin/bash if empty' (not image entrypoint)
- Update BYOC README: document that CMD is replaced by supervisor,
command must be passed explicitly, remove TUI-specific sections
* fix(sandbox): demote Landlock fallback log from warn to debug
The Landlock filesystem sandbox emits a noisy warning when a policy
path does not exist (e.g. /app in custom images). Since BestEffort
mode intentionally continues without Landlock, demote to debug.
* docs: fix BYOC Dockerfile comments for supervisor CMD override
---------
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>
The multi-arch publish job fails with 'extern location does not exist'
errors for version_check when BuildKit cache mounts contain stale
artifacts. The existing retry logic (cargo clean + retry) was
ineffective because:
1. cargo clean relies on its own metadata to decide what to remove,
which can itself be stale on BuildKit cache mounts shared across
different build profiles (debug vs release).
2. sccache remained enabled during the retry, so a corrupt memcached
cache entry would reproduce the same failure even after cleaning.
Replace cargo clean with rm -rf /build/target/* for a thorough wipe,
and unset RUSTC_WRAPPER on retry so cargo falls back to raw rustc.
Add a top-level Mermaid architecture diagram (architecture/system-architecture.md)
covering all deployable components, external systems, communication protocols,
and security boundaries. Update the arch-doc-writer agent to maintain this
diagram as part of its documentation workflow.
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>
TLS certificates are always resolved automatically from cluster metadata,
making the explicit CLI flags unnecessary. The TlsOptions struct and
auto-resolution logic remain intact for programmatic and test use.
* 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.
Add tab-completion for positional name arguments across sandbox, cluster,
and provider commands. Completers fetch live data on tab-press: cluster
names from local metadata files (no network), sandbox and provider names
via gRPC to the active cluster's gateway with a 2s timeout.
Key implementation details:
- Move rustls crypto provider init before CompleteEnv::complete() so TLS
works inside completers
- Spawn a dedicated OS thread for async gRPC calls to avoid nested tokio
runtime panic (completers run synchronously inside #[tokio::main]'s
block_on)
- All completers fail silently (return empty vec) on any error
## Summary
- Downgrade `GetSandboxPolicy served from policy history` from INFO to DEBUG — this was firing every 30s per sandbox and filling the 2000-line in-memory log buffer with noise, drowning out real events
- Add `"navigator: log push reconnected (attempt N)"` eprintln on successful reconnect in the sandbox log push loop, so operators can confirm recovery after a gateway restart
## Test Plan
- Deployed to local cluster, created sandbox, verified no policy polling spam in `nav sandbox logs`
- Restarted gateway, confirmed sandbox stderr shows reconnect success message: `navigator: log push reconnected (attempt 5)`
- Verified sandbox log push re-establishes after gateway restart
## Summary
- Add HTTP/2 keep-alive settings (`http2_keep_alive_interval`, `keep_alive_while_idle`, `keep_alive_timeout`, `connect_timeout`) to the sandbox gRPC client channel, matching what the CLI already uses
- Wrap the `PushSandboxLogs` client-streaming RPC in an outer reconnect loop with exponential backoff (1s → 30s) so that server restarts, network blips, or idle connection reaping no longer permanently kill sandbox log delivery
- Buffer up to 200 log lines during reconnect attempts so they are delivered after re-establishing the connection
- Detect stream death proactively via an `rpc_done` channel instead of waiting for the next batch send to fail
## Problem
The `PushSandboxLogs` RPC is a long-lived client-streaming call that stays open for the sandbox's entire lifetime. Without HTTP/2 keep-alive, Kubernetes service proxies reap idle TCP connections, causing a silent `"transport error"`. The push loop would then exit permanently with no retry — all subsequent sandbox-source logs were lost.
Unary RPCs like `GetSandboxPolicy` were unaffected because they complete in milliseconds and never sit idle.
## Test Plan
- Deployed to local cluster, verified `[sandbox]` logs appear in `nav sandbox logs` output
- Killed `navigator-0` while sandboxes were running; sandbox stderr showed `log push stream lost, reconnecting...` followed by successful reconnection
- Both `[gateway]` and `[sandbox]` log sources confirmed present after server recovery