* 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>
16 KiB
Contributing to Navigator
Prerequisites
Install mise. This is used to setup the development environment.
# Install mise (macOS/Linux)
curl https://mise.run | sh
After installing mise be sure to activate the environment by running mise activate or add it to your shell.
Shell installation examples:
Fish:
echo '~/.local/bin/mise activate fish | source' >> ~/.config/fish/config.fish
Zsh (Mac OS Default):
echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
Project uses Rust 1.88+ and Python 3.12+. Docker must be running for cluster and sandbox workflows.
Getting started
# Trust the project config (one-time)
mise trust
# Fast local cluster recreate (reuses prebuilt images)
mise run cluster
# Build images and deploy (recommended for CI/first setup)
mise run cluster:build
# Create a sandbox with Claude (or opencode / codex)
nav sandbox create -- claude
Note: nav builds the CLI from source on first run, which takes several minutes while Rust compiles. Subsequent runs are fast.
Other useful commands
nav --help # CLI help
mise build # Debug build (without running)
mise test # Run all project tests
mise run sandbox # Run sandbox container interactively
Shell Completions
The CLI supports dynamic shell completions. Run navigator completions --help for full per-shell setup instructions.
For the nav wrapper, generate completions from the real binary and rewrite the registration to target nav:
Fish:
navigator completions fish | sed 's/--command navigator/--command nav/' > ~/.config/fish/completions/nav.fish
Bash:
navigator completions bash | sed 's/_clap_complete_navigator/_clap_complete_nav/g; s/ navigator$/ nav/' > ~/.local/share/bash-completion/completions/nav
Zsh:
navigator completions zsh | sed 's/_clap_dynamic_completer_navigator/_clap_dynamic_completer_nav/g; s/ navigator$/ nav/' > ~/.zfunc/_nav
Sandbox SSH access
To connect to a running sandbox with SSH, use:
navigator sandbox connect <sandbox-id>
To forward a local port into a sandbox (e.g., port 18789):
navigator sandbox forward start 18789 <sandbox-name>
This opens a local SSH tunnel so connections to 127.0.0.1:18789 on the host
are forwarded to 127.0.0.1:18789 inside the sandbox. The command stays
attached until interrupted (Ctrl+C). Add -d to run in the background.
Relevant environment variables:
NAVIGATOR_SSH_GATEWAY_HOST,NAVIGATOR_SSH_GATEWAY_PORT,NAVIGATOR_SSH_CONNECT_PATHNAVIGATOR_SANDBOX_SSH_PORT,NAVIGATOR_SSH_HANDSHAKE_SECRET,NAVIGATOR_SSH_HANDSHAKE_SKEW_SECSNAVIGATOR_SSH_LISTEN_ADDR(set inside sandbox pods)
Project Structure
crates/
├── navigator-core/ # Core library
├── navigator-server/ # Main gateway server, ingress for all operations
├── navigator-sandbox/ # Sandbox execution environment
├── navigator-bootstrap/ # Local cluster bootstrap (Docker)
└── navigator-cli/ # Command-line interface
python/ # Python bindings
proto/ # Protocol buffer definitions
architecture/ # Architecture documentation and design plans
build/ # mise task definitions and build scripts
├── *.toml # Task includes (loaded by mise.toml task_config)
└── scripts/ # Shared build scripts used by tasks
deploy/
├── docker/ # Dockerfiles and build artifacts
├── helm/navigator/ # Navigator Helm chart
└── kube/manifests/ # Kubernetes manifests for k3s auto-deploy
Development Workflow
Building
mise run build # Debug build
mise run build:release # Release build
mise run check # Quick compile check
Testing
mise run test # All tests (Rust + Python)
mise run test:rust # Rust tests only
mise run test:python # Python tests only
mise run test:e2e:sandbox # Sandbox Python e2e tests
Python E2E Test Patterns
- Put sandbox SDK e2e tests in
e2e/python/. - Prefer
Sandbox.exec_python(...)with Python callables over inlinepython -cstrings. - Define callable helpers inside the test function when possible so they serialize cleanly in sandbox.
- Keep scenarios focused: one test for happy path and separate tests for negative/policy enforcement behavior.
- Use
mise run test:e2e:sandboxto run this suite locally.
Linting & Formatting
# Rust
mise run rust:format # Format code
mise run rust:format:check # Check formatting
mise run rust:lint # Lint with Clippy
# Python
mise run python:format # Format with ruff
mise run python:lint # Lint with ruff
mise run python:typecheck # Type check with ty
# Helm
mise run helm:lint # Lint the navigator helm chart
Running Components
mise run sandbox # Run sandbox container with interactive shell
Custom Container Images
Use --image to run a sandbox with any Linux container image:
# Run an interactive shell in an Ubuntu sandbox
nav sandbox create --image ubuntu:24.04
# Run a command in a custom image
nav sandbox create --image python:3.12-slim -- python3 -c "print('hello')"
# Sync local files and run in a custom image
nav sandbox create --image node:22 --sync -- npm test
The supervisor binary is side-loaded from the standard sandbox image via a Kubernetes init
container. The default run_as_user/run_as_group policy is cleared for custom images to
avoid failures on images that lack the sandbox user. See architecture/sandbox.md for
details on the bootstrap flow and constraints.
Building and Pushing Custom Images
Use nav sandbox image push to build a Dockerfile and push the resulting image into the
cluster's containerd runtime so it can be used with --image:
# Build and push from a Dockerfile
nav sandbox image push --dockerfile ./Dockerfile
# Specify a custom tag
nav sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest
# Specify a build context directory
nav sandbox image push --dockerfile ./build/Dockerfile --context ./build
# Pass build arguments
nav sandbox image push --dockerfile ./Dockerfile --build-arg PYTHON_VERSION=3.12
# Use the pushed image
nav sandbox create --image my-sandbox:latest
The command builds the image using the local Docker daemon and pushes it into the cluster
via the same docker save / ctr images import pipeline used for component images. A
.dockerignore file in the build context directory is respected.
Git Hooks (Pre-commit)
We use mise generate git-pre-commit for local pre-commit checks.
Generate a Git pre-commit hook that runs the pre-commit task:
mise generate git-pre-commit --write --task=pre-commit
Kubernetes Development
The project uses the Navigator CLI to provision a local k3s-in-container cluster. Docker is the only external dependency for cluster bootstrap.
mise run cluster # Recreate local cluster quickly using prebuilt images
mise run cluster:build # Build component images, then deploy cluster (CI-friendly)
mise run cluster:deploy # Fast deploy: rebuild changed components and skip unnecessary helm work
mise run cluster:deploy:sandbox # Fast deploy sandbox-only changes
mise run cluster:push:server # Push local server image to configured pull registry
mise run cluster:push:sandbox # Push local sandbox image to configured pull registry
mise run cluster:deploy:pull # Force full pull-mode deploy flow
mise run cluster:push # Legacy image-import fallback workflow
mise run cluster uses local .env values when present and appends missing keys:
CLUSTER_NAME, GATEWAY_PORT, and NAVIGATOR_CLUSTER.
If GATEWAY_PORT is missing, it picks a free local port and persists it to .env.
Existing .env values are not overwritten.
Fast mise run cluster flow:
- Recreate cluster.
- Ensure local registry (
127.0.0.1:5000) is running in pull-through-cache mode. - Deploy with local image refs (
127.0.0.1:5000/navigator/*, taglatestunlessIMAGE_TAGis set) while k3s pulls throughhost.docker.internal:5000. - Use
mise run cluster:deploy(orcluster:deploy:sandbox) to push local changes to that registry and redeploy only relevant components.
This keeps iterative local push workflows working while still caching remote pulls.
mise run cluster:build keeps the local build-and-push flow for development/CI.
Cluster bootstrap pulls the cluster image from the published remote registry by default.
Set NAVIGATOR_CLUSTER_IMAGE to override the image reference explicitly.
Default local cluster workflow uses pull mode with a local Docker registry at 127.0.0.1:5000.
Local clusters also bind host port 6443 for the Kubernetes API, so only one
local Navigator cluster can run at a time on a given Docker host.
You can override repository settings with:
IMAGE_REPO_BASE(for example127.0.0.1:5000/navigator)NAVIGATOR_REGISTRY_HOST,NAVIGATOR_REGISTRY_NAMESPACENAVIGATOR_REGISTRY_ENDPOINT(optional mirror endpoint override, e.g.host.docker.internal:5000)NAVIGATOR_REGISTRY_USERNAME,NAVIGATOR_REGISTRY_PASSWORDNAVIGATOR_REGISTRY_INSECURE=true|false
Useful env flags for fast deploy:
FORCE_HELM_UPGRADE=1- run Helm upgrade even when chart files are unchangedDEPLOY_FAST_HELM_WAIT=1- wait for Helm upgrade completion (helm --wait)DEPLOY_FAST_MODE=full- force full component rebuild behavior through fast deployDOCKER_BUILD_CACHE_DIR=.cache/buildkit- local BuildKit cache directory used by component image builds
GitHub Container Registry mapping (CI or shared dev):
export NAVIGATOR_REGISTRY_HOST=ghcr.io
export NAVIGATOR_REGISTRY_NAMESPACE=${GITHUB_REPOSITORY}
export NAVIGATOR_REGISTRY_USERNAME=${GITHUB_ACTOR}
export NAVIGATOR_REGISTRY_PASSWORD=${GITHUB_TOKEN}
export IMAGE_REPO_BASE=ghcr.io/${GITHUB_REPOSITORY}
The cluster exposes ports 80/443 for gateway traffic and 6443 for the Kubernetes API.
Once the cluster is deployed. You can interact with the cluster using standard nav CLI commands.
Gateway mTLS for CLI
When the cluster is configured to terminate TLS at the Gateway with client authentication, the
CLI needs the generated client certificate bundle. The chart creates a navigator-cli-client
Secret containing ca.crt, tls.crt, and tls.key. During nav cluster admin deploy, the
CLI bundle is automatically copied into ~/.config/navigator/clusters/<name>/mtls, where
<name> comes from NAVIGATOR_CLUSTER_NAME or the host in NAVIGATOR_CLUSTER (localhost
defaults to navigator).
Debugging Cluster Issues
If a cluster fails to start or is unhealthy after nav cluster admin deploy, use the debug-navigator-cluster skill (located at .agent/skills/debug-navigator-cluster/SKILL.md) to diagnose the issue. This skill provides step-by-step instructions for troubleshooting cluster bootstrap failures, health check errors, and other infrastructure problems.
Docker Build Tasks
mise run docker:build # Build all Docker images
mise run docker:build:sandbox # Build the sandbox Docker image
mise run docker:build:server # Build the server Docker image
mise run docker:build:cluster # Build the airgapped k3s cluster image
Python Development
mise run python:dev # Install Python package in development mode (builds CLI binary)
mise run python:build # Build Python wheel with CLI binary
Python protobuf stubs in python/navigator/_proto/ are generated artifacts and are gitignored
(except __init__.py). mise Python build/test/lint/typecheck tasks run python:proto
automatically, so you generally do not need to generate stubs manually.
Publishing
Versions are derived from git tags using setuptools_scm. No version bumps need to be committed.
Python wheel builds inject version at build time via
NAVIGATOR_CARGO_VERSION (Cargo/SemVer), applied inside wheel-builder Docker
layers, so publish flows do not edit Cargo.toml/Cargo.lock in the working
tree.
Version commands:
mise run version:print # Show computed versions (python, cargo, docker)
mise run version:print -- --cargo # Show cargo version only
Publishing credentials (one-time setup):
echo "
NAV_PYPI_USERNAME=$USER
NAV_PYPI_PASSWORD=$ARTIFACTORY_PASSWORD" >> .env
Docker publishing in CI uses AWS credentials for ECR. Python publishing uses a
two-stage flow: wheels are uploaded to S3, then an internal-network runner
publishes them to Artifactory with NAV_PYPI_* credentials.
Main branch publish (CI):
- Publishes Docker multiarch images to ECR as
:dev,:latest, and a versioned dev tag. - Builds Linux + macOS (arm64) Python wheels and uploads them to
s3://navigator-pypi-artifacts/navigator/<wheel-version>/. - Runs a publish job on the
nvrunner to list that version prefix, download the wheels, and publish them to Artifactory.
Tag release publish (CI):
- Push a semver tag (
vX.Y.Z) to trigger release jobs. - CI publishes Docker multiarch images to ECR as
:X.Y.Z(no:latest). - CI stages Linux + macOS (arm64) Python wheels in S3 and publishes to
Artifactory from the
nvrunner.
Tagging a release:
git tag v0.1.1
git push --tags
# CI will build and publish Docker + Linux/macOS Python wheels.
Local macOS wheel publish (arm64):
# Native on macOS host:
mise run python:publish:macos
# Cross-compile from Linux via Docker:
mise run python:build:macos:docker
Cleaning
mise run clean # Clean build artifacts
Code Style
• Rust: Formatted with rustfmt, linted with Clippy (pedantic + nursery)
• Python: Formatted and linted with ruff, type-checked with ty
Run mise run all before committing to check everything (runs fmt:check, clippy, test, python:lint).
CLI Output Style
When printing structured output from CLI commands, follow these conventions:
• Blank line after headings: Always print an empty line between a heading and its key-value fields. This improves readability in the terminal.
• Indented fields: Key-value fields should be indented with 2 spaces.
• Dimmed keys: Use .dimmed() for field labels (e.g., "Id:".dimmed()).
• Colored headings: Use .cyan().bold() for primary headings.
Good:
Created sandbox:
Id: cddeeb6d-a4d3-4158-a4d1-bd931f743700
Name: sandbox-cddeeb6d
Namespace: navigator
Bad (no blank line after heading):
Created sandbox:
Id: cddeeb6d-a4d3-4158-a4d1-bd931f743700
Name: sandbox-cddeeb6d
Namespace: navigator
Commit Messages
This project uses Conventional Commits. All commit messages must follow the format:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
Types:
feat- New featurefix- Bug fixdocs- Documentation onlychore- Maintenance tasks (dependencies, build config)refactor- Code change that neither fixes a bug nor adds a featuretest- Adding or updating testsci- CI/CD changesperf- Performance improvements
Examples:
feat(cli): add --verbose flag to nav run
fix(sandbox): handle timeout errors gracefully
docs: update installation instructions
chore(deps): bump tokio to 1.40
Pull Requests
- Create a feature branch from
main - Make your changes with tests
- Run
mise run allto verify - Open a PR with a clear description
Use the create-github-pr skill to help with opening your pull request.