From 4ec2c8b22366d02c9829183f3ba26c0b9e86eeae Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 3 Mar 2026 22:31:41 -0800 Subject: [PATCH 1/3] feat(cli): add --from flag to sandbox create for unified image sources (#88) --- CONTRIBUTING.md | 354 +++++++++++++++++++- architecture/sandbox-custom-containers.md | 63 +++- crates/navigator-cli/src/main.rs | 21 +- crates/navigator-cli/src/run.rs | 166 ++++++++- e2e/bash/test_sandbox_custom_image.sh | 6 +- examples/bring-your-own-container/README.md | 4 +- 6 files changed, 574 insertions(+), 40 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aaa7b71c1f..a30df8942a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,23 +66,343 @@ These are the primary `mise` tasks for day-to-day development: ## Project Structure -| Path | Purpose | -|---|---| -| `crates/` | Rust crates | -| `python/` | Python SDK and bindings | -| `proto/` | Protocol buffer definitions | -| `tasks/` | `mise` task definitions and build scripts | -| `deploy/` | Dockerfiles, Helm chart, Kubernetes manifests | -| `architecture/` | Architecture docs and plans | - -## Pull Requests - -1. Create a feature branch from `main` -2. Make your changes with tests -3. Run `mise run ci` to verify -4. Open a PR with a clear description - -### Commit Messages +``` +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/ # NemoClaw Helm chart +└── kube/manifests/ # Kubernetes manifests for k3s auto-deploy +``` + +## Development Workflow + +### Building + +```bash +mise run build # Debug build +mise run build:release # Release build +mise run check # Quick compile check +``` + +### Testing + +```bash +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 inline `python -c` strings. +- 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:sandbox` to run this suite locally. + +### Linting & Formatting + +```bash +# 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 nemoclaw helm chart +``` + +### Running Components + +```bash +mise run sandbox # Run sandbox container with interactive shell +``` + +### Custom Container Images + +Use `--from` to run a sandbox with any Linux container image, a community sandbox, or a +local Dockerfile: + +```bash +# Use a community sandbox image +ncl sandbox create --from openclaw + +# Run an interactive shell in an Ubuntu sandbox +ncl sandbox create --from ubuntu:24.04 + +# Run a command in a custom image +ncl sandbox create --from python:3.12-slim -- python3 -c "print('hello')" + +# Sync local files and run in a custom image +ncl sandbox create --from node:22 --sync -- npm test + +# Build from a local Dockerfile in one step +ncl sandbox create --from ./Dockerfile + +# Build from a directory containing a Dockerfile +ncl sandbox create --from ./my-sandbox/ +``` + +The `--from` flag accepts community sandbox names (e.g., `openclaw`), paths to Dockerfiles +or directories, and full container image references. See `architecture/sandbox-custom-containers.md` +for the full resolution heuristic. + +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 (Manual Two-Step) + +Use `ncl sandbox image push` to build a Dockerfile and push the resulting image into the +cluster's containerd runtime separately (the `--from` flag does this automatically for +Dockerfile paths): + +```bash +# Build and push from a Dockerfile +ncl sandbox image push --dockerfile ./Dockerfile + +# Specify a custom tag +ncl sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest + +# Specify a build context directory +ncl sandbox image push --dockerfile ./build/Dockerfile --context ./build + +# Pass build arguments +ncl sandbox image push --dockerfile ./Dockerfile --build-arg PYTHON_VERSION=3.12 + +# Use the pushed image +ncl sandbox create --from 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: + +```bash +mise generate git-pre-commit --write --task=pre-commit +``` + +### Kubernetes Development + +The project uses the NemoClaw CLI to provision a local k3s-in-container cluster. Docker is the only external dependency for cluster bootstrap. + +```bash +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 `NEMOCLAW_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: +1. Recreate cluster. +2. Ensure local registry (`127.0.0.1:5000`) is running in pull-through-cache mode. +3. Deploy with local image refs (`127.0.0.1:5000/navigator/*`, tag `latest` unless `IMAGE_TAG` is set) while k3s pulls through `host.docker.internal:5000`. +4. Use `mise run cluster:deploy` (or `cluster: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 `NEMOCLAW_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 NemoClaw cluster can run at a time on a given Docker host. +You can override repository settings with: + +- `IMAGE_REPO_BASE` (for example `127.0.0.1:5000/navigator`) +- `NEMOCLAW_REGISTRY_HOST`, `NEMOCLAW_REGISTRY_NAMESPACE` +- `NEMOCLAW_REGISTRY_ENDPOINT` (optional mirror endpoint override, e.g. `host.docker.internal:5000`) +- `NEMOCLAW_REGISTRY_USERNAME`, `NEMOCLAW_REGISTRY_PASSWORD` +- `NEMOCLAW_REGISTRY_INSECURE=true|false` + +Useful env flags for fast deploy: + +- `FORCE_HELM_UPGRADE=1` - run Helm upgrade even when chart files are unchanged +- `DEPLOY_FAST_HELM_WAIT=1` - wait for Helm upgrade completion (`helm --wait`) +- `DEPLOY_FAST_MODE=full` - force full component rebuild behavior through fast deploy +- `DOCKER_BUILD_CACHE_DIR=.cache/buildkit` - local BuildKit cache directory used by component image builds + +GitHub Container Registry mapping (CI or shared dev): + +```bash +export NEMOCLAW_REGISTRY_HOST=ghcr.io +export NEMOCLAW_REGISTRY_NAMESPACE=${GITHUB_REPOSITORY} +export NEMOCLAW_REGISTRY_USERNAME=${GITHUB_ACTOR} +export NEMOCLAW_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 `ncl` 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 `ncl cluster admin deploy`, the +CLI bundle is automatically copied into `~/.config/nemoclaw/clusters//mtls`, where +`` comes from `NEMOCLAW_CLUSTER_NAME` or the host in `NEMOCLAW_CLUSTER` (localhost +defaults to `nemoclaw`). + +### Debugging Cluster Issues + +If a cluster fails to start or is unhealthy after `ncl 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 + +```bash +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 + +```bash +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 +`NEMOCLAW_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:** + +```bash +mise run version:print # Show computed versions (python, cargo, docker) +mise run version:print -- --cargo # Show cargo version only +``` + +**Publishing credentials (one-time setup):** + +```bash +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//`. +- Runs a publish job on the `nv` runner 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 `nv` runner. + +**Tagging a release:** + +```bash +git tag v0.1.1 +git push --tags +# CI will build and publish Docker + Linux/macOS Python wheels. +``` + +**Local macOS wheel publish (arm64):** + +```bash +# Native on macOS host: +mise run python:publish:macos + +# Cross-compile from Linux via Docker: +mise run python:build:macos:docker +``` + +### Cleaning + +```bash +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](https://www.conventionalcommits.org/). All commit messages must follow the format: diff --git a/architecture/sandbox-custom-containers.md b/architecture/sandbox-custom-containers.md index 4eced89bd5..5690adcd9a 100644 --- a/architecture/sandbox-custom-containers.md +++ b/architecture/sandbox-custom-containers.md @@ -1,10 +1,42 @@ # Sandbox Custom Containers -Users can run `nemoclaw sandbox create --image ` to launch a sandbox with an arbitrary container image while keeping the `navigator-sandbox` process supervisor in control. +Users can run `ncl sandbox create --from ` to launch a sandbox with a custom container image while keeping the `navigator-sandbox` process supervisor in control. + +## The `--from` Flag + +The `--from` flag accepts four kinds of input: + +| Input | Example | Behavior | +|-------|---------|----------| +| **Community sandbox name** | `--from openclaw` | Resolves to `ghcr.io/nvidia/nemoclaw-community/sandboxes/openclaw:latest` | +| **Dockerfile path** | `--from ./Dockerfile` | Builds the image, pushes it into the cluster, then creates the sandbox | +| **Directory with Dockerfile** | `--from ./my-sandbox/` | Uses the directory as the build context | +| **Full image reference** | `--from myregistry.com/img:tag` | Uses the image directly | + +### Resolution heuristic + +The CLI classifies the value in this order: + +1. **Existing file** whose name contains "Dockerfile" (case-insensitive) — treated as a Dockerfile to build. +2. **Existing directory** containing a `Dockerfile` — treated as a build context directory. +3. **Contains `/`, `:`, or `.`** — treated as a full container image reference. +4. **Otherwise** — treated as a community sandbox name, expanded to `{NEMOCLAW_COMMUNITY_REGISTRY}/{name}:latest`. + +The community registry prefix defaults to `ghcr.io/nvidia/nemoclaw-community/sandboxes` and can be overridden with the `NEMOCLAW_COMMUNITY_REGISTRY` environment variable. + +### Dockerfile build flow + +When `--from` points to a Dockerfile or directory, the CLI: + +1. Builds the image locally via the Docker daemon (respecting `.dockerignore`). +2. Pushes it into the cluster's containerd runtime using `docker save` / `ctr import`. +3. Creates the sandbox with the resulting image tag. + +This is equivalent to running `ncl sandbox image push` followed by `ncl sandbox create --from ` in a single step. ## How It Works -When `--image` is provided and differs from the server's default sandbox image, the server activates **supervisor bootstrap mode**. The supervisor binary is side-loaded from the default sandbox image via a Kubernetes init container: +When the resolved image differs from the server's default sandbox image, the server activates **supervisor bootstrap mode**. The supervisor binary is side-loaded from the default sandbox image via a Kubernetes init container: ```mermaid flowchart TB @@ -37,19 +69,32 @@ These transforms apply to both generated templates and user-provided `pod_templa ## CLI Usage +### Creating a sandbox from a community image + +```bash +ncl sandbox create --from openclaw +``` + ### Creating a sandbox with a custom image ```bash -nemoclaw sandbox create --image myimage:latest -- echo "hello from custom container" +ncl sandbox create --from myimage:latest -- echo "hello from custom container" ``` -When `--image` is set the CLI clears the default `run_as_user`/`run_as_group` policy (which expects a `sandbox` user) so that arbitrary images that lack that user can start without error. +When `--from` is set the CLI clears the default `run_as_user`/`run_as_group` policy (which expects a `sandbox` user) so that arbitrary images that lack that user can start without error. + +### Building from a Dockerfile in one step + +```bash +ncl sandbox create --from ./Dockerfile -- echo "built and running" +ncl sandbox create --from ./my-sandbox/ # directory with Dockerfile +``` -### Pushing custom images into the cluster +### Pushing custom images into the cluster (manual two-step) ```bash -nemoclaw sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest -nemoclaw sandbox create --image my-sandbox:latest +ncl sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest +ncl sandbox create --from my-sandbox:latest ``` `nemoclaw sandbox image push` accepts: @@ -75,6 +120,10 @@ The `navigator-sandbox` supervisor adapts to arbitrary environments: | Decision | Rationale | |----------|-----------| +| Unified `--from` flag | Single entry point for community names, Dockerfiles, directories, and image refs — removes the need to know registry paths | +| Community name resolution | Bare names like `openclaw` expand to the GHCR community registry, making the common case simple | +| Auto build+push for Dockerfiles | Eliminates the two-step `image push` + `create` workflow for local development | +| `NEMOCLAW_COMMUNITY_REGISTRY` env var | Allows organizations to host their own community sandbox registry | | Init container side-load | Avoids rebuilding every workload image with the supervisor binary baked in | | `emptyDir` shared volume | Zero-config, no PVC needed, ephemeral by design | | Read-only mount in agent | Supervisor binary cannot be tampered with by the workload | diff --git a/crates/navigator-cli/src/main.rs b/crates/navigator-cli/src/main.rs index bb9d1ae9f0..1e57c77314 100644 --- a/crates/navigator-cli/src/main.rs +++ b/crates/navigator-cli/src/main.rs @@ -479,10 +479,18 @@ enum SandboxCommands { #[arg(long)] name: Option, - /// Container image for the sandbox workload. - /// The sandbox supervisor is side-loaded via an init container. + /// Sandbox source: a community sandbox name (e.g., `openclaw`), a path + /// to a Dockerfile or directory containing one, or a full container + /// image reference (e.g., `myregistry.com/img:tag`). + /// + /// Community names are resolved to + /// `ghcr.io/nvidia/nemoclaw-community/sandboxes/:latest` + /// (override the prefix with `NEMOCLAW_COMMUNITY_REGISTRY`). + /// + /// When given a Dockerfile or directory, the image is built and pushed + /// into the cluster automatically before creating the sandbox. #[arg(long)] - image: Option, + from: Option, /// Sync local files into the sandbox before running. #[arg(long)] @@ -921,7 +929,7 @@ async fn main() -> Result<()> { match command { SandboxCommands::Create { name, - image, + from, sync, keep, remote, @@ -960,7 +968,8 @@ async fn main() -> Result<()> { run::sandbox_create( endpoint, name.as_deref(), - image.as_deref(), + from.as_deref(), + &ctx.name, sync, keep, remote.as_deref(), @@ -978,7 +987,7 @@ async fn main() -> Result<()> { // No cluster configured — go straight to bootstrap. run::sandbox_create_with_bootstrap( name.as_deref(), - image.as_deref(), + from.as_deref(), sync, keep, remote.as_deref(), diff --git a/crates/navigator-cli/src/run.rs b/crates/navigator-cli/src/run.rs index 301d0c9c24..f5ba4a1f0a 100644 --- a/crates/navigator-cli/src/run.rs +++ b/crates/navigator-cli/src/run.rs @@ -969,7 +969,7 @@ pub fn cluster_admin_tunnel( #[allow(clippy::too_many_arguments)] pub async fn sandbox_create_with_bootstrap( name: Option<&str>, - image: Option<&str>, + from: Option<&str>, sync: bool, keep: bool, remote: Option<&str>, @@ -988,10 +988,13 @@ pub async fn sandbox_create_with_bootstrap( )); } let (tls, server) = crate::bootstrap::run_bootstrap(remote, ssh_key).await?; + // The bootstrap flow always creates a cluster named "nemoclaw". + let cluster_name = "nemoclaw"; sandbox_create( &server, name, - image, + from, + cluster_name, sync, keep, remote, @@ -1011,7 +1014,8 @@ pub async fn sandbox_create_with_bootstrap( pub async fn sandbox_create( server: &str, name: Option<&str>, - image: Option<&str>, + from: Option<&str>, + cluster_name: &str, sync: bool, keep: bool, remote: Option<&str>, @@ -1042,6 +1046,25 @@ pub async fn sandbox_create( } }; + // Resolve the --from flag into a container image reference, building from + // a Dockerfile first if necessary. + let image: Option = match from { + Some(val) => { + let resolved = resolve_from(val)?; + match resolved { + ResolvedSource::Image(img) => Some(img), + ResolvedSource::Dockerfile { + dockerfile, + context, + } => { + let tag = build_from_dockerfile(&dockerfile, &context, cluster_name).await?; + Some(tag) + } + } + } + None => None, + }; + let inferred_types: Vec = inferred_provider_type(command).into_iter().collect(); let configured_providers = ensure_required_providers(&mut client, providers, &inferred_types).await?; @@ -1057,7 +1080,7 @@ pub async fn sandbox_create( } let template = image.map(|img| SandboxTemplate { - image: img.to_string(), + image: img, ..SandboxTemplate::default() }); @@ -1316,6 +1339,139 @@ pub async fn sandbox_create( } } +/// The default community sandbox registry prefix. +/// +/// Bare sandbox names (e.g., `openclaw`) are expanded to +/// `{prefix}/{name}:latest` using this value. Override with the +/// `NEMOCLAW_COMMUNITY_REGISTRY` environment variable. +const DEFAULT_COMMUNITY_REGISTRY: &str = "ghcr.io/nvidia/nemoclaw-community/sandboxes"; + +/// Resolved source for the `--from` flag on `sandbox create`. +enum ResolvedSource { + /// A ready-to-use container image reference. + Image(String), + /// A Dockerfile that must be built and pushed before creating the sandbox. + Dockerfile { + dockerfile: PathBuf, + context: PathBuf, + }, +} + +/// Classify the `--from` value into an image reference or a Dockerfile that +/// needs building. +/// +/// Resolution order: +/// 1. Existing file whose name contains "Dockerfile" → build from file. +/// 2. Existing directory that contains a `Dockerfile` → build from directory. +/// 3. Value contains `/`, `:`, or `.` → treat as a full image reference. +/// 4. Otherwise → community sandbox name, expanded via the registry prefix. +fn resolve_from(value: &str) -> Result { + let path = Path::new(value); + + // 1. Existing file that looks like a Dockerfile. + if path.is_file() { + let name = path + .file_name() + .map(|n| n.to_string_lossy()) + .unwrap_or_default(); + let lower = name.to_lowercase(); + if lower.contains("dockerfile") || lower.ends_with(".dockerfile") { + let dockerfile = path + .canonicalize() + .into_diagnostic() + .wrap_err_with(|| format!("failed to resolve path: {}", path.display()))?; + let context = dockerfile + .parent() + .ok_or_else(|| miette::miette!("Dockerfile has no parent directory"))? + .to_path_buf(); + return Ok(ResolvedSource::Dockerfile { + dockerfile, + context, + }); + } + } + + // 2. Existing directory containing a Dockerfile. + if path.is_dir() { + let candidate = path.join("Dockerfile"); + if candidate.is_file() { + let context = path + .canonicalize() + .into_diagnostic() + .wrap_err_with(|| format!("failed to resolve path: {}", path.display()))?; + let dockerfile = context.join("Dockerfile"); + return Ok(ResolvedSource::Dockerfile { + dockerfile, + context, + }); + } + return Err(miette::miette!( + "No Dockerfile found in directory: {}", + path.display() + )); + } + + // 3. Looks like a full image reference (contains / : or .). + if value.contains('/') || value.contains(':') || value.contains('.') { + return Ok(ResolvedSource::Image(value.to_string())); + } + + // 4. Community sandbox name. + let prefix = std::env::var("NEMOCLAW_COMMUNITY_REGISTRY") + .unwrap_or_else(|_| DEFAULT_COMMUNITY_REGISTRY.to_string()); + let prefix = prefix.trim_end_matches('/'); + Ok(ResolvedSource::Image(format!("{prefix}/{value}:latest"))) +} + +/// Build a Dockerfile and push the resulting image into the cluster. +/// +/// Returns the image tag that was built so the caller can use it for sandbox +/// creation. +async fn build_from_dockerfile( + dockerfile: &Path, + context: &Path, + cluster_name: &str, +) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let tag = format!("navigator/sandbox-from:{timestamp}"); + + eprintln!( + "Building image {} from {}", + tag.cyan(), + dockerfile.display() + ); + eprintln!(" {} {}", "Context:".dimmed(), context.display()); + eprintln!(" {} {}", "Cluster:".dimmed(), cluster_name); + eprintln!(); + + let mut on_log = |msg: String| { + eprintln!(" {msg}"); + }; + + navigator_bootstrap::build::build_and_push_image( + dockerfile, + &tag, + context, + cluster_name, + &HashMap::new(), + &mut on_log, + ) + .await?; + + eprintln!(); + eprintln!( + "{} Image {} is available in the cluster.", + "✓".green().bold(), + tag.cyan(), + ); + eprintln!(); + + Ok(tag) +} + /// Load sandbox policy YAML. /// /// Resolution order: `--policy` flag > `NEMOCLAW_SANDBOX_POLICY` env var. @@ -1674,7 +1830,7 @@ pub async fn sandbox_image_push( eprintln!(); eprintln!( "Use it with: {}", - format!("ncl sandbox create --image {tag}").dimmed() + format!("ncl sandbox create --from {tag}").dimmed() ); Ok(()) diff --git a/e2e/bash/test_sandbox_custom_image.sh b/e2e/bash/test_sandbox_custom_image.sh index a92fa9f4cb..6c3a51f801 100755 --- a/e2e/bash/test_sandbox_custom_image.sh +++ b/e2e/bash/test_sandbox_custom_image.sh @@ -7,8 +7,8 @@ # with it. # # Verifies the full flow: -# 1. nemoclaw sandbox image push --dockerfile (build + import into cluster) -# 2. nemoclaw sandbox create --image -- (run sandbox with custom image) +# 1. ncl sandbox image push --dockerfile (build + import into cluster) +# 2. ncl sandbox create --from -- (run sandbox with custom image) # # Prerequisites: # - A running nemoclaw cluster (nemoclaw cluster admin deploy) @@ -120,7 +120,7 @@ info "Creating sandbox with custom image: ${IMAGE_TAG}" CREATE_LOG=$(mktemp) if ! "${NAV}" sandbox create \ - --image "${IMAGE_TAG}" \ + --from "${IMAGE_TAG}" \ -- cat /opt/marker.txt \ > "${CREATE_LOG}" 2>&1; then error "Sandbox create failed" diff --git a/examples/bring-your-own-container/README.md b/examples/bring-your-own-container/README.md index 72c4e3fcdf..d8aeea37fe 100644 --- a/examples/bring-your-own-container/README.md +++ b/examples/bring-your-own-container/README.md @@ -29,7 +29,7 @@ nemoclaw sandbox image push \ ### 2. Create a sandbox with port forwarding ```bash -nemoclaw sandbox create --image byoc-demo:latest --forward 8080 -- python /sandbox/app.py +ncl sandbox create --from byoc-demo:latest --forward 8080 -- python /sandbox/app.py ``` The `--forward 8080` flag opens an SSH tunnel so `localhost:8080` on your @@ -72,7 +72,7 @@ TODO(#70): Remove the sandbox user note once custom images are secure by default NemoClaw handles all the wiring automatically. You build a standard Linux container image — no NemoClaw-specific dependencies or -configuration required. When you create a sandbox with `--image`, +configuration required. When you create a sandbox with `--from`, NemoClaw ensures that sandboxing (network policy, filesystem isolation, SSH access) works the same as with the default image. From 35e040453a4b35f373e2000629e647d33f1baeff Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 4 Mar 2026 20:36:15 -0800 Subject: [PATCH 2/3] wip --- .agents/skills/nemoclaw-cli/SKILL.md | 200 +++++++++---------- .agents/skills/nemoclaw-cli/cli-reference.md | 80 ++++---- CONTRIBUTING.md | 165 +++++++++------ architecture/sandbox-custom-containers.md | 16 +- crates/navigator-cli/src/main.rs | 18 +- crates/navigator-cli/src/run.rs | 18 +- crates/navigator-cli/src/ssh.rs | 2 +- crates/navigator-tui/src/lib.rs | 2 +- e2e/bash/test_sandbox_custom_image.sh | 4 +- examples/bring-your-own-container/README.md | 2 +- examples/private-ip-routing/README.md | 2 +- tasks/scripts/cluster.sh | 6 + 12 files changed, 278 insertions(+), 237 deletions(-) diff --git a/.agents/skills/nemoclaw-cli/SKILL.md b/.agents/skills/nemoclaw-cli/SKILL.md index fbbd1e94f1..0d2422d910 100644 --- a/.agents/skills/nemoclaw-cli/SKILL.md +++ b/.agents/skills/nemoclaw-cli/SKILL.md @@ -1,31 +1,31 @@ --- name: nemoclaw-cli -description: Guide agents through using the NemoClaw CLI (nemoclaw/ncl) for sandbox management, provider configuration, policy iteration, BYOC workflows, and inference routing. Covers basic through advanced multi-step workflows. Trigger keywords - nemoclaw, ncl, sandbox create, sandbox connect, sandbox logs, provider create, policy set, policy get, image push, port forward, BYOC, bring your own container, use nemoclaw, run nemoclaw, CLI usage, manage sandbox, manage provider. +description: Guide agents through using the NemoClaw CLI (nemoclaw) for sandbox management, provider configuration, policy iteration, BYOC workflows, and inference routing. Covers basic through advanced multi-step workflows. Trigger keywords - nemoclaw, sandbox create, sandbox connect, sandbox logs, provider create, policy set, policy get, image push, port forward, BYOC, bring your own container, use nemoclaw, run nemoclaw, CLI usage, manage sandbox, manage provider. --- # NemoClaw CLI -Guide agents through using the `nemoclaw` CLI (`ncl`) for sandbox and platform management -- from basic operations to advanced multi-step workflows. +Guide agents through using the `nemoclaw` CLI for sandbox and platform management -- from basic operations to advanced multi-step workflows. ## Overview -The NemoClaw CLI (`nemoclaw`, commonly aliased as `ncl`) is the primary interface for managing sandboxes, providers, policies, inference routes, and clusters. This skill teaches agents how to orchestrate CLI commands for common and complex workflows. +The NemoClaw CLI (`nemoclaw`) is the primary interface for managing sandboxes, providers, policies, inference routes, and clusters. This skill teaches agents how to orchestrate CLI commands for common and complex workflows. **Companion skill**: For creating or modifying sandbox policy YAML content (network rules, L7 inspection, access presets), use the `generate-sandbox-policy` skill. This skill covers the CLI *commands* for the policy lifecycle; `generate-sandbox-policy` covers policy *content authoring*. **Self-teaching**: The CLI has comprehensive built-in help. When you encounter a command or option not covered in this skill, walk the help tree: ```bash -ncl --help # Top-level commands -ncl --help # Subcommands in a group -ncl --help # Flags for a specific command +nemoclaw --help # Top-level commands +nemoclaw --help # Subcommands in a group +nemoclaw --help # Flags for a specific command ``` This is your primary fallback. Use it freely -- the CLI's help output is authoritative and always up-to-date. ## Prerequisites -- `ncl` or `nemoclaw` is on the PATH (install via `cargo install --path crates/navigator-cli` or use the `ncl` wrapper script) +- `nemoclaw` is on the PATH (install via `cargo install --path crates/navigator-cli`) - Docker is running (required for cluster operations and BYOC) - For remote clusters: SSH access to the target host @@ -42,7 +42,7 @@ Use this workflow when no cluster exists yet and the user wants to get a sandbox ### Step 1: Bootstrap a cluster ```bash -ncl cluster admin deploy +nemoclaw cluster admin deploy ``` This provisions a local k3s cluster in Docker. The CLI will prompt interactively if a cluster already exists. The cluster is automatically set as the active cluster. @@ -50,13 +50,13 @@ This provisions a local k3s cluster in Docker. The CLI will prompt interactively For remote deployment: ```bash -ncl cluster admin deploy --remote user@host --ssh-key ~/.ssh/id_rsa +nemoclaw cluster admin deploy --remote user@host --ssh-key ~/.ssh/id_rsa ``` ### Step 2: Verify the cluster ```bash -ncl cluster status +nemoclaw cluster status ``` Confirm the cluster is reachable and shows a version. @@ -66,7 +66,7 @@ Confirm the cluster is reachable and shows a version. The simplest way to get a sandbox running: ```bash -ncl sandbox create +nemoclaw sandbox create ``` This creates a sandbox with defaults and drops you into an interactive shell. The CLI auto-bootstraps a cluster if none exists. @@ -74,8 +74,8 @@ This creates a sandbox with defaults and drops you into an interactive shell. Th **Shortcut for known tools**: When the trailing command is a recognized tool, the CLI auto-creates the required provider from local credentials: ```bash -ncl sandbox create -- claude # Auto-creates claude provider -ncl sandbox create -- codex # Auto-creates codex provider +nemoclaw sandbox create -- claude # Auto-creates claude provider +nemoclaw sandbox create -- codex # Auto-creates codex provider ``` The agent will be prompted interactively if credentials are missing. @@ -85,7 +85,7 @@ The agent will be prompted interactively if credentials are missing. Exit the sandbox shell (`exit` or Ctrl-D), then: ```bash -ncl sandbox delete +nemoclaw sandbox delete ``` --- @@ -99,7 +99,7 @@ Supported types: `claude`, `opencode`, `codex`, `generic`, `nvidia`, `gitlab`, ` ### Create a provider from local credentials ```bash -ncl provider create --name my-github --type github --from-existing +nemoclaw provider create --name my-github --type github --from-existing ``` The `--from-existing` flag discovers credentials from local state (e.g., `gh auth` tokens, Claude config files). @@ -107,7 +107,7 @@ The `--from-existing` flag discovers credentials from local state (e.g., `gh aut ### Create a provider with explicit credentials ```bash -ncl provider create --name my-api --type generic \ +nemoclaw provider create --name my-api --type generic \ --credential API_KEY=sk-abc123 \ --config base_url=https://api.example.com ``` @@ -115,16 +115,16 @@ ncl provider create --name my-api --type generic \ Bare `KEY` (without `=VALUE`) reads the value from the environment variable of that name: ```bash -ncl provider create --name my-api --type generic --credential API_KEY +nemoclaw provider create --name my-api --type generic --credential API_KEY ``` ### List, inspect, update, delete ```bash -ncl provider list -ncl provider get my-github -ncl provider update my-github --type github --from-existing -ncl provider delete my-github +nemoclaw provider list +nemoclaw provider get my-github +nemoclaw provider update my-github --type github --from-existing +nemoclaw provider delete my-github ``` --- @@ -134,7 +134,7 @@ ncl provider delete my-github ### Create with options ```bash -ncl sandbox create \ +nemoclaw sandbox create \ --name my-sandbox \ --provider my-github \ --provider my-claude \ @@ -153,53 +153,53 @@ Key flags: ### List and inspect sandboxes ```bash -ncl sandbox list -ncl sandbox get my-sandbox +nemoclaw sandbox list +nemoclaw sandbox get my-sandbox ``` ### Connect to a running sandbox ```bash -ncl sandbox connect my-sandbox +nemoclaw sandbox connect my-sandbox ``` Opens an interactive SSH shell. To configure VS Code Remote-SSH: ```bash -ncl sandbox ssh-config my-sandbox >> ~/.ssh/config +nemoclaw sandbox ssh-config my-sandbox >> ~/.ssh/config ``` ### Sync files ```bash # Push local files to sandbox -ncl sandbox sync my-sandbox --up ./src /sandbox/src +nemoclaw sandbox sync my-sandbox --up ./src /sandbox/src # Pull files from sandbox -ncl sandbox sync my-sandbox --down /sandbox/output ./local-output +nemoclaw sandbox sync my-sandbox --down /sandbox/output ./local-output ``` ### View logs ```bash # Recent logs -ncl sandbox logs my-sandbox +nemoclaw sandbox logs my-sandbox # Stream live logs -ncl sandbox logs my-sandbox --tail +nemoclaw sandbox logs my-sandbox --tail # Filter by source and level -ncl sandbox logs my-sandbox --tail --source sandbox --level warn +nemoclaw sandbox logs my-sandbox --tail --source sandbox --level warn # Logs from the last 5 minutes -ncl sandbox logs my-sandbox --since 5m +nemoclaw sandbox logs my-sandbox --since 5m ``` ### Delete sandboxes ```bash -ncl sandbox delete my-sandbox -ncl sandbox delete sandbox-1 sandbox-2 sandbox-3 # Multiple at once +nemoclaw sandbox delete my-sandbox +nemoclaw sandbox delete sandbox-1 sandbox-2 sandbox-3 # Multiple at once ``` --- @@ -236,7 +236,7 @@ Create sandbox with initial policy ### Step 1: Create sandbox with initial policy ```bash -ncl sandbox create --name dev --policy ./initial-policy.yaml --keep -- claude +nemoclaw sandbox create --name dev --policy ./initial-policy.yaml --keep -- claude ``` Use `--keep` so the sandbox stays alive for iteration. The user can work in the sandbox via a separate shell. @@ -246,7 +246,7 @@ Use `--keep` so the sandbox stays alive for iteration. The user can work in the In a separate terminal or as the agent: ```bash -ncl sandbox logs dev --tail --source sandbox +nemoclaw sandbox logs dev --tail --source sandbox ``` Look for log lines with `action: deny` -- these indicate blocked network requests. The logs include: @@ -257,7 +257,7 @@ Look for log lines with `action: deny` -- these indicate blocked network request ### Step 3: Pull the current policy ```bash -ncl sandbox policy get dev --full > current-policy.yaml +nemoclaw sandbox policy get dev --full > current-policy.yaml ``` The `--full` flag outputs valid YAML that can be directly re-submitted. This is the round-trip format. @@ -277,7 +277,7 @@ Only `network_policies` and `inference` sections can be modified at runtime. If ### Step 5: Push the updated policy ```bash -ncl sandbox policy set dev --policy current-policy.yaml --wait +nemoclaw sandbox policy set dev --policy current-policy.yaml --wait ``` The `--wait` flag blocks until the sandbox confirms the policy is loaded (polls every second). Exit codes: @@ -288,7 +288,7 @@ The `--wait` flag blocks until the sandbox confirms the policy is loaded (polls ### Step 6: Verify the update ```bash -ncl sandbox policy list dev +nemoclaw sandbox policy list dev ``` Check that the latest revision shows status `loaded`. If `failed`, check the error column for details. @@ -302,13 +302,13 @@ Return to Step 2. Continue monitoring logs and refining the policy until all req View all revisions to understand how the policy evolved: ```bash -ncl sandbox policy list dev --limit 50 +nemoclaw sandbox policy list dev --limit 50 ``` Fetch a specific historical revision: ```bash -ncl sandbox policy get dev --rev 3 --full +nemoclaw sandbox policy get dev --rev 3 --full ``` --- @@ -320,7 +320,7 @@ Build a custom container image and run it as a sandbox. ### Step 1: Build and push the image ```bash -ncl sandbox image push \ +nemoclaw sandbox image push \ --dockerfile ./Dockerfile \ --tag my-app:latest \ --context . @@ -331,7 +331,7 @@ The image is built locally via Docker and imported directly into the cluster's c Build arguments are supported: ```bash -ncl sandbox image push \ +nemoclaw sandbox image push \ --dockerfile ./Dockerfile \ --tag my-app:v2 \ --build-arg PYTHON_VERSION=3.12 @@ -340,10 +340,10 @@ ncl sandbox image push \ ### Step 2: Create a sandbox with the custom image ```bash -ncl sandbox create --image my-app:latest --keep --name my-app +nemoclaw sandbox create --from my-app:latest --keep --name my-app ``` -When `--image` is specified, the CLI: +When `--from` is specified, the CLI: - Clears default `run_as_user`/`run_as_group` (custom images may not have the `sandbox` user) - Uses a supervisor bootstrap pattern (init container copies the sandbox supervisor into a shared volume) @@ -351,10 +351,10 @@ When `--image` is specified, the CLI: ```bash # Foreground (blocks) -ncl sandbox forward start 8080 my-app +nemoclaw sandbox forward start 8080 my-app # Background (returns immediately) -ncl sandbox forward start 8080 my-app -d +nemoclaw sandbox forward start 8080 my-app -d ``` The service is now reachable at `localhost:8080`. @@ -363,10 +363,10 @@ The service is now reachable at `localhost:8080`. ```bash # List active forwards -ncl sandbox forward list +nemoclaw sandbox forward list # Stop a forward -ncl sandbox forward stop 8080 my-app +nemoclaw sandbox forward stop 8080 my-app ``` ### Step 5: Iterate @@ -374,15 +374,15 @@ ncl sandbox forward stop 8080 my-app To update the container: ```bash -ncl sandbox delete my-app -ncl sandbox image push --dockerfile ./Dockerfile --tag my-app:v2 -ncl sandbox create --image my-app:v2 --keep --name my-app --forward 8080 +nemoclaw sandbox delete my-app +nemoclaw sandbox image push --dockerfile ./Dockerfile --tag my-app:v2 +nemoclaw sandbox create --from my-app:v2 --keep --name my-app --forward 8080 ``` ### Shortcut: Create with port forward in one command ```bash -ncl sandbox create --image my-app:latest --forward 8080 --keep -- ./start-server.sh +nemoclaw sandbox create --from my-app:latest --forward 8080 --keep -- ./start-server.sh ``` The `--forward` flag starts a background port forward before the command runs, so the service is reachable immediately. @@ -401,7 +401,7 @@ This workflow supports a human working in a sandbox while an agent monitors acti ### Step 1: Create sandbox with providers and keep alive ```bash -ncl sandbox create \ +nemoclaw sandbox create \ --name work-session \ --provider github \ --provider claude \ @@ -414,13 +414,13 @@ ncl sandbox create \ Tell the user to run: ```bash -ncl sandbox connect work-session +nemoclaw sandbox connect work-session ``` Or for VS Code: ```bash -ncl sandbox ssh-config work-session >> ~/.ssh/config +nemoclaw sandbox ssh-config work-session >> ~/.ssh/config # Then connect via VS Code Remote-SSH to the host "work-session" ``` @@ -429,7 +429,7 @@ ncl sandbox ssh-config work-session >> ~/.ssh/config While the user works, monitor the sandbox logs: ```bash -ncl sandbox logs work-session --tail --source sandbox --level warn +nemoclaw sandbox logs work-session --tail --source sandbox --level warn ``` Watch for `deny` actions that indicate the user's work is being blocked by policy. @@ -438,17 +438,17 @@ Watch for `deny` actions that indicate the user's work is being blocked by polic When denied actions are observed: -1. Pull current policy: `ncl sandbox policy get work-session --full > policy.yaml` +1. Pull current policy: `nemoclaw sandbox policy get work-session --full > policy.yaml` 2. Modify the policy to allow the blocked actions (use `generate-sandbox-policy` skill for content) -3. Push the update: `ncl sandbox policy set work-session --policy policy.yaml --wait` -4. Verify: `ncl sandbox policy list work-session` +3. Push the update: `nemoclaw sandbox policy set work-session --policy policy.yaml --wait` +4. Verify: `nemoclaw sandbox policy list work-session` The user does not need to disconnect -- policy updates are hot-reloaded within ~30 seconds (or immediately when using `--wait`, which polls for confirmation). ### Step 5: Clean up when done ```bash -ncl sandbox delete work-session +nemoclaw sandbox delete work-session ``` --- @@ -460,7 +460,7 @@ Configure inference routes so sandboxes can access LLM endpoints. ### Create an inference route ```bash -ncl inference create \ +nemoclaw inference create \ --routing-hint local \ --base-url https://my-llm.example.com \ --model-id my-model-v1 \ @@ -472,9 +472,9 @@ If `--protocol` is omitted, the CLI auto-detects by probing the endpoint. ### List and manage routes ```bash -ncl inference list -ncl inference update my-route --routing-hint local --base-url https://new-url.example.com --model-id my-model-v2 -ncl inference delete my-route +nemoclaw inference list +nemoclaw inference update my-route --routing-hint local --base-url https://new-url.example.com --model-id my-model-v2 +nemoclaw inference delete my-route ``` ### Connect sandbox to inference @@ -491,7 +491,7 @@ inference: Then create the sandbox with the policy: ```bash -ncl sandbox create --policy ./policy-with-inference.yaml -- claude +nemoclaw sandbox create --policy ./policy-with-inference.yaml -- claude ``` --- @@ -501,31 +501,31 @@ ncl sandbox create --policy ./policy-with-inference.yaml -- claude ### List and switch clusters ```bash -ncl cluster list # See all clusters -ncl cluster use my-cluster # Switch active cluster -ncl cluster status # Verify connectivity +nemoclaw cluster list # See all clusters +nemoclaw cluster use my-cluster # Switch active cluster +nemoclaw cluster status # Verify connectivity ``` ### Lifecycle ```bash -ncl cluster admin deploy # Start local cluster -ncl cluster admin stop # Stop (preserves state) -ncl cluster admin deploy # Restart (reuses state) -ncl cluster admin destroy # Destroy permanently +nemoclaw cluster admin deploy # Start local cluster +nemoclaw cluster admin stop # Stop (preserves state) +nemoclaw cluster admin deploy # Restart (reuses state) +nemoclaw cluster admin destroy # Destroy permanently ``` ### Remote clusters ```bash # Deploy to remote host -ncl cluster admin deploy --remote user@host --ssh-key ~/.ssh/id_rsa --name remote-cluster +nemoclaw cluster admin deploy --remote user@host --ssh-key ~/.ssh/id_rsa --name remote-cluster # Set up kubectl access -ncl cluster admin tunnel --name remote-cluster +nemoclaw cluster admin tunnel --name remote-cluster # Get cluster info -ncl cluster admin info --name remote-cluster +nemoclaw cluster admin info --name remote-cluster ``` --- @@ -534,19 +534,19 @@ ncl cluster admin info --name remote-cluster When you encounter a command or option not covered in this skill: -1. **Start broad**: `ncl --help` to see all command groups. -2. **Narrow down**: `ncl --help` to see subcommands (e.g., `ncl sandbox --help`). -3. **Get specific**: `ncl --help` for flags and usage (e.g., `ncl sandbox create --help`). +1. **Start broad**: `nemoclaw --help` to see all command groups. +2. **Narrow down**: `nemoclaw --help` to see subcommands (e.g., `nemoclaw sandbox --help`). +3. **Get specific**: `nemoclaw --help` for flags and usage (e.g., `nemoclaw sandbox create --help`). The CLI help is always authoritative. If the help output contradicts this skill, follow the help output -- the CLI may have been updated since this skill was written. ### Example: discovering an unfamiliar command ```bash -$ ncl sandbox --help +$ nemoclaw sandbox --help # Shows: create, get, list, delete, connect, sync, logs, ssh-config, forward, image, policy -$ ncl sandbox sync --help +$ nemoclaw sandbox sync --help # Shows: --up, --down flags, positional arguments, usage examples ``` @@ -556,24 +556,24 @@ $ ncl sandbox sync --help | Task | Command | |------|---------| -| Deploy local cluster | `ncl cluster admin deploy` | -| Check cluster health | `ncl cluster status` | -| Create sandbox (interactive) | `ncl sandbox create` | -| Create sandbox with tool | `ncl sandbox create -- claude` | -| Create with custom policy | `ncl sandbox create --policy ./p.yaml --keep` | -| Connect to sandbox | `ncl sandbox connect ` | -| Stream live logs | `ncl sandbox logs --tail` | -| Pull current policy | `ncl sandbox policy get --full > p.yaml` | -| Push updated policy | `ncl sandbox policy set --policy p.yaml --wait` | -| Policy revision history | `ncl sandbox policy list ` | -| Build & push custom image | `ncl sandbox image push --dockerfile ./Dockerfile` | -| Forward a port | `ncl sandbox forward start -d` | -| Create provider | `ncl provider create --name N --type T --from-existing` | -| List providers | `ncl provider list` | -| Create inference route | `ncl inference create --routing-hint H --base-url U --model-id M` | -| Delete sandbox | `ncl sandbox delete ` | -| Destroy cluster | `ncl cluster admin destroy` | -| Self-teach any command | `ncl --help` | +| Deploy local cluster | `nemoclaw cluster admin deploy` | +| Check cluster health | `nemoclaw cluster status` | +| Create sandbox (interactive) | `nemoclaw sandbox create` | +| Create sandbox with tool | `nemoclaw sandbox create -- claude` | +| Create with custom policy | `nemoclaw sandbox create --policy ./p.yaml --keep` | +| Connect to sandbox | `nemoclaw sandbox connect ` | +| Stream live logs | `nemoclaw sandbox logs --tail` | +| Pull current policy | `nemoclaw sandbox policy get --full > p.yaml` | +| Push updated policy | `nemoclaw sandbox policy set --policy p.yaml --wait` | +| Policy revision history | `nemoclaw sandbox policy list ` | +| Build & push custom image | `nemoclaw sandbox image push --dockerfile ./Dockerfile` | +| Forward a port | `nemoclaw sandbox forward start -d` | +| Create provider | `nemoclaw provider create --name N --type T --from-existing` | +| List providers | `nemoclaw provider list` | +| Create inference route | `nemoclaw inference create --routing-hint H --base-url U --model-id M` | +| Delete sandbox | `nemoclaw sandbox delete ` | +| Destroy cluster | `nemoclaw cluster admin destroy` | +| Self-teach any command | `nemoclaw --help` | ## Companion Skills @@ -581,4 +581,4 @@ $ ncl sandbox sync --help |-------|------------| | `generate-sandbox-policy` | Creating or modifying policy YAML content (network rules, L7 inspection, access presets, endpoint configuration) | | `debug-navigator-cluster` | Diagnosing cluster startup or health failures | -| `tui-development` | Developing features for the Gator TUI (`ncl gator`) | +| `tui-development` | Developing features for the Gator TUI (`nemoclaw gator`) | diff --git a/.agents/skills/nemoclaw-cli/cli-reference.md b/.agents/skills/nemoclaw-cli/cli-reference.md index 329f37bdb4..83a312b53a 100644 --- a/.agents/skills/nemoclaw-cli/cli-reference.md +++ b/.agents/skills/nemoclaw-cli/cli-reference.md @@ -1,8 +1,8 @@ # NemoClaw CLI Reference -Quick-reference for the `nemoclaw` (aliased as `ncl`) command-line interface. For workflow guidance, see [SKILL.md](SKILL.md). +Quick-reference for the `nemoclaw` command-line interface. For workflow guidance, see [SKILL.md](SKILL.md). -> **Self-teaching**: If a command or flag is not listed here, use `ncl --help` to discover it. The CLI has comprehensive built-in help at every level. +> **Self-teaching**: If a command or flag is not listed here, use `nemoclaw --help` to discover it. The CLI has comprehensive built-in help at every level. ## Global Options @@ -23,7 +23,7 @@ Quick-reference for the `nemoclaw` (aliased as `ncl`) command-line interface. Fo ## Complete Command Tree ``` -nemoclaw (ncl) +nemoclaw ├── cluster │ ├── status │ ├── use @@ -73,19 +73,19 @@ nemoclaw (ncl) ## Cluster Commands -### `ncl cluster status` +### `nemoclaw cluster status` Show server connectivity and version. -### `ncl cluster use ` +### `nemoclaw cluster use ` Set the active cluster. Writes to `~/.config/nemoclaw/active_cluster`. -### `ncl cluster list` +### `nemoclaw cluster list` List all provisioned clusters. Active cluster marked with `*`. -### `ncl cluster admin deploy` +### `nemoclaw cluster admin deploy` Provision or start a cluster (local or remote). @@ -100,7 +100,7 @@ Provision or start a cluster (local or remote). | `--update-kube-config` | false | Write kubeconfig into `~/.kube/config` | | `--get-kubeconfig` | false | Print kubeconfig to stdout | -### `ncl cluster admin stop` +### `nemoclaw cluster admin stop` Stop a cluster (preserves state for later restart). @@ -110,11 +110,11 @@ Stop a cluster (preserves state for later restart). | `--remote ` | SSH destination | | `--ssh-key ` | SSH private key | -### `ncl cluster admin destroy` +### `nemoclaw cluster admin destroy` Destroy a cluster and all its state. Same flags as `stop`. -### `ncl cluster admin info` +### `nemoclaw cluster admin info` Show deployment details: endpoint, kubeconfig path, kube port, remote host. @@ -122,7 +122,7 @@ Show deployment details: endpoint, kubeconfig path, kube port, remote host. |------|-------------| | `--name ` | Cluster name (defaults to active) | -### `ncl cluster admin tunnel` +### `nemoclaw cluster admin tunnel` Print or start an SSH tunnel for kubectl access to a remote cluster. @@ -137,14 +137,14 @@ Print or start an SSH tunnel for kubectl access to a remote cluster. ## Sandbox Commands -### `ncl sandbox create [OPTIONS] [-- COMMAND...]` +### `nemoclaw sandbox create [OPTIONS] [-- COMMAND...]` Create a sandbox, wait for readiness, then connect or execute the trailing command. Auto-bootstraps a cluster if none exists. | Flag | Description | |------|-------------| | `--name ` | Sandbox name (auto-generated if omitted) | -| `--image ` | Custom container image (BYOC) | +| `--from ` | Sandbox source: community name, Dockerfile path, directory, or image reference (BYOC) | | `--sync` | Sync local git-tracked files into sandbox at `/sandbox` | | `--keep` | Keep sandbox alive after non-interactive commands finish | | `--provider ` | Provider to attach (repeatable) | @@ -152,13 +152,15 @@ Create a sandbox, wait for readiness, then connect or execute the trailing comma | `--forward ` | Forward local port to sandbox (implies `--keep`) | | `--remote ` | SSH destination for auto-bootstrap | | `--ssh-key ` | SSH private key for auto-bootstrap | +| `--tty` | Force pseudo-terminal allocation | +| `--no-tty` | Disable pseudo-terminal allocation | | `[-- COMMAND...]` | Command to execute (defaults to interactive shell) | -### `ncl sandbox get ` +### `nemoclaw sandbox get ` Show sandbox details (id, name, namespace, phase, policy). -### `ncl sandbox list` +### `nemoclaw sandbox list` List sandboxes in a table. @@ -169,15 +171,15 @@ List sandboxes in a table. | `--ids` | false | Print only sandbox IDs | | `--names` | false | Print only sandbox names | -### `ncl sandbox delete ...` +### `nemoclaw sandbox delete ...` Delete one or more sandboxes by name. Stops any background port forwards. -### `ncl sandbox connect ` +### `nemoclaw sandbox connect ` Open an interactive SSH shell to a sandbox. -### `ncl sandbox sync {--up | --down } [dest]` +### `nemoclaw sandbox sync {--up | --down } [dest]` Sync files to/from a sandbox using tar-over-SSH. @@ -187,7 +189,7 @@ Sync files to/from a sandbox using tar-over-SSH. | `--down ` | Pull sandbox files to local | | `[DEST]` | Destination path (default: `/sandbox` for up, `.` for down) | -### `ncl sandbox logs ` +### `nemoclaw sandbox logs ` View sandbox logs. Supports one-shot and streaming. @@ -199,7 +201,7 @@ View sandbox logs. Supports one-shot and streaming. | `--source ` | `all` | Filter: `gateway`, `sandbox`, or `all` (repeatable) | | `--level ` | none | Minimum level: `error`, `warn`, `info`, `debug`, `trace` | -### `ncl sandbox ssh-config ` +### `nemoclaw sandbox ssh-config ` Print an SSH config `Host` block for a sandbox. Useful for VS Code Remote-SSH. @@ -207,7 +209,7 @@ Print an SSH config `Host` block for a sandbox. Useful for VS Code Remote-SSH. ## Port Forwarding Commands -### `ncl sandbox forward start ` +### `nemoclaw sandbox forward start ` Start forwarding a local port to a sandbox. @@ -217,11 +219,11 @@ Start forwarding a local port to a sandbox. | `` | Sandbox name | | `-d`, `--background` | Run in background | -### `ncl sandbox forward stop ` +### `nemoclaw sandbox forward stop ` Stop a background port forward. -### `ncl sandbox forward list` +### `nemoclaw sandbox forward list` List all active port forwards (sandbox, port, PID, status). @@ -229,7 +231,7 @@ List all active port forwards (sandbox, port, PID, status). ## Custom Image Commands (BYOC) -### `ncl sandbox image push` +### `nemoclaw sandbox image push` Build a container image and push it into the cluster's internal registry. @@ -244,7 +246,7 @@ Build a container image and push it into the cluster's internal registry. ## Policy Commands -### `ncl sandbox policy set --policy ` +### `nemoclaw sandbox policy set --policy ` Update the policy on a live sandbox. Only dynamic fields (`network_policies`, `inference`) can be changed at runtime. @@ -256,7 +258,7 @@ Update the policy on a live sandbox. Only dynamic fields (`network_policies`, `i Exit codes with `--wait`: 0 = loaded, 1 = failed, 124 = timeout. -### `ncl sandbox policy get ` +### `nemoclaw sandbox policy get ` Show current active policy for a sandbox. @@ -265,7 +267,7 @@ Show current active policy for a sandbox. | `--rev ` | 0 (latest) | Show a specific revision | | `--full` | false | Print the full policy as YAML (round-trips with `--policy` input) | -### `ncl sandbox policy list ` +### `nemoclaw sandbox policy list ` List policy revision history (version, hash, status, created, error). @@ -279,7 +281,7 @@ List policy revision history (version, hash, status, created, error). Supported provider types: `claude`, `opencode`, `codex`, `generic`, `nvidia`, `gitlab`, `github`, `outlook`. -### `ncl provider create --name --type ` +### `nemoclaw provider create --name --type ` Create a provider configuration. @@ -291,11 +293,11 @@ Create a provider configuration. | `--credential KEY[=VALUE]` | Credential pair. Bare `KEY` reads from env var. Repeatable. | | `--config KEY=VALUE` | Config key/value pair. Repeatable. | -### `ncl provider get ` +### `nemoclaw provider get ` Show provider details (id, name, type, credential keys, config keys). -### `ncl provider list` +### `nemoclaw provider list` List providers in a table. @@ -305,11 +307,11 @@ List providers in a table. | `--offset ` | 0 | Pagination offset | | `--names` | false | Print only names | -### `ncl provider update --type ` +### `nemoclaw provider update --type ` Update an existing provider. Same flags as `create`. -### `ncl provider delete ...` +### `nemoclaw provider delete ...` Delete one or more providers by name. @@ -317,7 +319,7 @@ Delete one or more providers by name. ## Inference Commands -### `ncl inference create` +### `nemoclaw inference create` Create an inference route. Auto-detects supported protocols if `--protocol` is omitted. @@ -331,15 +333,15 @@ Create an inference route. Auto-detects supported protocols if `--protocol` is o | `--model-id ` | -- | Model identifier (required) | | `--disabled` | false | Create in disabled state | -### `ncl inference update ` +### `nemoclaw inference update ` Update an existing inference route. Same flags as `create`. -### `ncl inference delete ...` +### `nemoclaw inference delete ...` Delete inference routes by name. -### `ncl inference list` +### `nemoclaw inference list` List inference routes. @@ -352,14 +354,14 @@ List inference routes. ## Other Commands -### `ncl gator` +### `nemoclaw gator` Launch the Gator interactive TUI. -### `ncl completions ` +### `nemoclaw completions ` Generate shell completion scripts. Supported shells: `bash`, `fish`, `zsh`, `powershell`. -### `ncl ssh-proxy` +### `nemoclaw ssh-proxy` SSH proxy used as a `ProxyCommand`. Not typically invoked directly. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a30df8942a..f5b856fc83 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,67 +2,116 @@ ## Prerequisites -Install [mise](https://mise.jdx.dev/). This is used to set up the development environment. +Install [mise](https://mise.jdx.dev/). This is used to setup the development environment. ```bash # Install mise (macOS/Linux) curl https://mise.run | sh ``` -After installing `mise`, activate it with `mise activate` or [add it to your shell](https://mise.jdx.dev/getting-started.html). -Shell setup examples: +After installing `mise` be sure to activate the environment by running `mise activate` or [add it to your shell](https://mise.jdx.dev/getting-started.html). + +Shell installation examples: + +Fish: ```bash -# Fish echo '~/.local/bin/mise activate fish | source' >> ~/.config/fish/config.fish +``` -# Zsh +Zsh (Mac OS Default): + +```bash echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc ``` -Project requirements: +Project uses Rust 1.88+ and Python 3.12+. Docker must be running for cluster and sandbox workflows. + +## Developer Certificate of Origin (DCO) -- Rust 1.88+ -- Python 3.12+ -- Docker (running) +All contributions to this project must include a `Signed-off-by` line in the commit message, certifying that you wrote or have the right to submit the code under the project's open-source license. This is the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). -## Getting Started +Add the sign-off automatically with `git commit -s`: ```bash -# One-time trust +git commit -s -m "feat(sandbox): add new capability" +``` + +This appends a line like: + +``` +Signed-off-by: Your Name +``` + +A DCO check runs on every pull request and will fail if any commit is missing the sign-off. + +## License Headers + +All source files must include an SPDX copyright header. Use the license header script to add or check headers: + +```bash +# Add/update headers on all source files +mise run license:update + +# Check that all files have headers (runs in CI and pre-commit) +mise run license:check +``` + +## Getting started + +```bash +# Trust the project config (one-time) mise trust -# Launch a sandbox (deploys a cluster if one isn't running) -mise run sandbox +# 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) +nemoclaw sandbox create -- claude +``` + +Note: `nemoclaw` builds the CLI from source on first run, which takes several minutes while Rust compiles. Subsequent runs are fast. + +### Other useful commands + +```bash +nemoclaw --help # CLI help +mise build # Debug build (without running) +mise test # Run all project tests +mise run sandbox # Run sandbox container interactively ``` -## `nemoclaw` Shortcut +## Shell Completions + +The CLI supports dynamic shell completions. Run `nemoclaw completions --help` for full per-shell setup instructions. + +## Sandbox SSH access -Inside this repository, `nemoclaw` is a local shortcut script at `scripts/bin/nemoclaw`. The script will +To connect to a running sandbox with SSH, use: -1. Builds `navigator-cli` if needed. -2. Runs the local debug CLI binary (`target/debug/nemoclaw`). +```bash +nemoclaw sandbox connect +``` -Because `mise` adds `scripts/bin` to `PATH` for this project, you can run `nemoclaw` directly from the repo. +To forward a local port into a sandbox (e.g., port 18789): ```bash -nemoclaw --help -nemoclaw sandbox create -- codex +nemoclaw sandbox forward start 18789 ``` -## Main Tasks +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. -These are the primary `mise` tasks for day-to-day development: +Relevant environment variables: -| Task | Purpose | -| ------------------ | ------------------------------------------------------- | -| `mise run cluster` | Bootstrap or incremental deploy | -| `mise run sandbox` | Create a sandbox on the running cluster | -| `mise run test` | Default test suite | -| `mise run e2e` | Default end-to-end test lane | -| `mise run ci` | Full local CI checks (lint, compile/type checks, tests) | -| `mise run clean` | Clean build artifacts | +- `NEMOCLAW_SSH_GATEWAY_HOST`, `NEMOCLAW_SSH_GATEWAY_PORT`, `NEMOCLAW_SSH_CONNECT_PATH` +- `NEMOCLAW_SANDBOX_SSH_PORT`, `NEMOCLAW_SSH_HANDSHAKE_SECRET`, `NEMOCLAW_SSH_HANDSHAKE_SKEW_SECS` +- `NEMOCLAW_SSH_LISTEN_ADDR` (set inside sandbox pods) ## Project Structure @@ -137,59 +186,44 @@ mise run sandbox # Run sandbox container with interactive shell ### Custom Container Images -Use `--from` to run a sandbox with any Linux container image, a community sandbox, or a -local Dockerfile: +Use `--image` to run a sandbox with any Linux container image: ```bash -# Use a community sandbox image -ncl sandbox create --from openclaw - # Run an interactive shell in an Ubuntu sandbox -ncl sandbox create --from ubuntu:24.04 +nemoclaw sandbox create --image ubuntu:24.04 # Run a command in a custom image -ncl sandbox create --from python:3.12-slim -- python3 -c "print('hello')" +nemoclaw sandbox create --image python:3.12-slim -- python3 -c "print('hello')" # Sync local files and run in a custom image -ncl sandbox create --from node:22 --sync -- npm test - -# Build from a local Dockerfile in one step -ncl sandbox create --from ./Dockerfile - -# Build from a directory containing a Dockerfile -ncl sandbox create --from ./my-sandbox/ +nemoclaw sandbox create --image node:22 --sync -- npm test ``` -The `--from` flag accepts community sandbox names (e.g., `openclaw`), paths to Dockerfiles -or directories, and full container image references. See `architecture/sandbox-custom-containers.md` -for the full resolution heuristic. - 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 (Manual Two-Step) +#### Building and Pushing Custom Images -Use `ncl sandbox image push` to build a Dockerfile and push the resulting image into the -cluster's containerd runtime separately (the `--from` flag does this automatically for -Dockerfile paths): +Use `nemoclaw 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`: ```bash # Build and push from a Dockerfile -ncl sandbox image push --dockerfile ./Dockerfile +nemoclaw sandbox image push --dockerfile ./Dockerfile # Specify a custom tag -ncl sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest +nemoclaw sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest # Specify a build context directory -ncl sandbox image push --dockerfile ./build/Dockerfile --context ./build +nemoclaw sandbox image push --dockerfile ./build/Dockerfile --context ./build # Pass build arguments -ncl sandbox image push --dockerfile ./Dockerfile --build-arg PYTHON_VERSION=3.12 +nemoclaw sandbox image push --dockerfile ./Dockerfile --build-arg PYTHON_VERSION=3.12 # Use the pushed image -ncl sandbox create --from my-sandbox:latest +nemoclaw sandbox create --image my-sandbox:latest ``` The command builds the image using the local Docker daemon and pushes it into the cluster @@ -266,20 +300,20 @@ 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 `ncl` CLI commands. +Once the cluster is deployed. You can interact with the cluster using standard `nemoclaw` 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 `ncl cluster admin deploy`, the +Secret containing `ca.crt`, `tls.crt`, and `tls.key`. During `nemoclaw cluster admin deploy`, the CLI bundle is automatically copied into `~/.config/nemoclaw/clusters//mtls`, where `` comes from `NEMOCLAW_CLUSTER_NAME` or the host in `NEMOCLAW_CLUSTER` (localhost defaults to `nemoclaw`). ### Debugging Cluster Issues -If a cluster fails to start or is unhealthy after `ncl 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. +If a cluster fails to start or is unhealthy after `nemoclaw 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 @@ -434,12 +468,11 @@ docs: update installation instructions chore(deps): bump tokio to 1.40 ``` -### DCO - -All contributions must include a `Signed-off-by` line in each commit message. This certifies you have the right to submit the work under the project license. See the [Developer Certificate of Origin](https://developercertificate.org/). +## Pull Requests -```bash -git commit -s -m "feat(sandbox): add new capability" -``` +1. Create a feature branch from `main` +2. Make your changes with tests +3. Run `mise run all` to verify +4. Open a PR with a clear description Use the `create-github-pr` skill to help with opening your pull request. diff --git a/architecture/sandbox-custom-containers.md b/architecture/sandbox-custom-containers.md index 5690adcd9a..54e6f4e462 100644 --- a/architecture/sandbox-custom-containers.md +++ b/architecture/sandbox-custom-containers.md @@ -1,6 +1,6 @@ # Sandbox Custom Containers -Users can run `ncl sandbox create --from ` to launch a sandbox with a custom container image while keeping the `navigator-sandbox` process supervisor in control. +Users can run `nemoclaw sandbox create --from ` to launch a sandbox with a custom container image while keeping the `navigator-sandbox` process supervisor in control. ## The `--from` Flag @@ -32,7 +32,7 @@ When `--from` points to a Dockerfile or directory, the CLI: 2. Pushes it into the cluster's containerd runtime using `docker save` / `ctr import`. 3. Creates the sandbox with the resulting image tag. -This is equivalent to running `ncl sandbox image push` followed by `ncl sandbox create --from ` in a single step. +This is equivalent to running `nemoclaw sandbox image push` followed by `nemoclaw sandbox create --from ` in a single step. ## How It Works @@ -72,13 +72,13 @@ These transforms apply to both generated templates and user-provided `pod_templa ### Creating a sandbox from a community image ```bash -ncl sandbox create --from openclaw +nemoclaw sandbox create --from openclaw ``` ### Creating a sandbox with a custom image ```bash -ncl sandbox create --from myimage:latest -- echo "hello from custom container" +nemoclaw sandbox create --from myimage:latest -- echo "hello from custom container" ``` When `--from` is set the CLI clears the default `run_as_user`/`run_as_group` policy (which expects a `sandbox` user) so that arbitrary images that lack that user can start without error. @@ -86,15 +86,15 @@ When `--from` is set the CLI clears the default `run_as_user`/`run_as_group` pol ### Building from a Dockerfile in one step ```bash -ncl sandbox create --from ./Dockerfile -- echo "built and running" -ncl sandbox create --from ./my-sandbox/ # directory with Dockerfile +nemoclaw sandbox create --from ./Dockerfile -- echo "built and running" +nemoclaw sandbox create --from ./my-sandbox/ # directory with Dockerfile ``` ### Pushing custom images into the cluster (manual two-step) ```bash -ncl sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest -ncl sandbox create --from my-sandbox:latest +nemoclaw sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest +nemoclaw sandbox create --from my-sandbox:latest ``` `nemoclaw sandbox image push` accepts: diff --git a/crates/navigator-cli/src/main.rs b/crates/navigator-cli/src/main.rs index 1e57c77314..d8b9ca8c95 100644 --- a/crates/navigator-cli/src/main.rs +++ b/crates/navigator-cli/src/main.rs @@ -44,16 +44,16 @@ fn resolve_cluster(cluster_flag: &Option) -> Result { .ok_or_else(|| { miette::miette!( "No active cluster.\n\ - Set one with: ncl cluster use \n\ - Or deploy a new cluster: ncl cluster admin deploy" + Set one with: nemoclaw cluster use \n\ + Or deploy a new cluster: nemoclaw cluster admin deploy" ) })?; let metadata = load_cluster_metadata(&name).map_err(|_| { miette::miette!( "Unknown cluster '{name}'.\n\ - Deploy it first: ncl cluster admin deploy --name {name}\n\ - Or list available clusters: ncl cluster list" + Deploy it first: nemoclaw cluster admin deploy --name {name}\n\ + Or list available clusters: nemoclaw cluster list" ) })?; @@ -137,10 +137,10 @@ enum Commands { /// Two mutually exclusive modes: /// /// **Token mode** (used internally by `sandbox connect`): - /// `ncl ssh-proxy --gateway --sandbox-id --token ` + /// `nemoclaw ssh-proxy --gateway --sandbox-id --token ` /// /// **Name mode** (for use in `~/.ssh/config`): - /// `ncl ssh-proxy --cluster --name ` + /// `nemoclaw ssh-proxy --cluster --name ` SshProxy { /// Gateway URL (e.g., ). /// Required in token mode. @@ -957,7 +957,7 @@ async fn main() -> Result<()> { if remote.is_some() { eprintln!( "{} --remote ignored: cluster '{}' is already active. \ - To redeploy, use: ncl cluster admin deploy", + To redeploy, use: nemoclaw cluster admin deploy", "!".yellow(), ctx.name, ); @@ -1340,8 +1340,8 @@ async fn main() -> Result<()> { let meta = load_cluster_metadata(&c).map_err(|_| { miette::miette!( "Unknown cluster '{c}'.\n\ - Deploy it first: ncl cluster admin deploy --name {c}\n\ - Or list available clusters: ncl cluster list" + Deploy it first: nemoclaw cluster admin deploy --name {c}\n\ + Or list available clusters: nemoclaw cluster list" ) })?; meta.gateway_endpoint diff --git a/crates/navigator-cli/src/run.rs b/crates/navigator-cli/src/run.rs index f5ba4a1f0a..22d0e6042c 100644 --- a/crates/navigator-cli/src/run.rs +++ b/crates/navigator-cli/src/run.rs @@ -513,8 +513,8 @@ pub fn cluster_use(name: &str) -> Result<()> { get_cluster_metadata(name).ok_or_else(|| { miette::miette!( "No cluster metadata found for '{name}'.\n\ - Deploy a cluster first with: ncl cluster admin deploy --name {name}\n\ - Or list available clusters: ncl cluster list" + Deploy a cluster first with: nemoclaw cluster admin deploy --name {name}\n\ + Or list available clusters: nemoclaw cluster list" ) })?; @@ -533,7 +533,7 @@ pub fn cluster_list(cluster_flag: &Option) -> Result<()> { println!(); println!( "Deploy a cluster with: {}", - "ncl cluster admin deploy".dimmed() + "nemoclaw cluster admin deploy".dimmed() ); return Ok(()); } @@ -868,7 +868,7 @@ pub fn cluster_admin_info(name: &str) -> Result<()> { let metadata = get_cluster_metadata(name).ok_or_else(|| { miette::miette!( "No cluster metadata found for '{name}'.\n\ - Deploy a cluster first with: ncl cluster admin deploy --name {name}" + Deploy a cluster first with: nemoclaw cluster admin deploy --name {name}" ) })?; @@ -903,7 +903,7 @@ pub fn cluster_admin_info(name: &str) -> Result<()> { if let (Some(host), Some(kube_port)) = (&metadata.remote_host, metadata.kube_port) { println!(); println!("{}", "SSH tunnel for kubectl access:".dimmed()); - println!(" ncl cluster admin tunnel --name {name}"); + println!(" nemoclaw cluster admin tunnel --name {name}"); println!("Or manually:"); println!(" ssh -L {kube_port}:127.0.0.1:6443 {host}"); } @@ -983,8 +983,8 @@ pub async fn sandbox_create_with_bootstrap( if !crate::bootstrap::confirm_bootstrap()? { return Err(miette::miette!( "No active cluster.\n\ - Set one with: ncl cluster use \n\ - Or deploy a new cluster: ncl cluster admin deploy" + Set one with: nemoclaw cluster use \n\ + Or deploy a new cluster: nemoclaw cluster admin deploy" )); } let (tls, server) = crate::bootstrap::run_bootstrap(remote, ssh_key).await?; @@ -1830,7 +1830,7 @@ pub async fn sandbox_image_push( eprintln!(); eprintln!( "Use it with: {}", - format!("ncl sandbox create --from {tag}").dimmed() + format!("nemoclaw sandbox create --from {tag}").dimmed() ); Ok(()) @@ -1917,7 +1917,7 @@ async fn ensure_required_providers( if !missing.is_empty() { if !std::io::stdin().is_terminal() { return Err(miette::miette!( - "missing required providers: {}. Create them first with `ncl provider create --type --name --from-existing`, or set them up manually from inside the sandbox", + "missing required providers: {}. Create them first with `nemoclaw provider create --type --name --from-existing`, or set them up manually from inside the sandbox", missing.join(", ") )); } diff --git a/crates/navigator-cli/src/ssh.rs b/crates/navigator-cli/src/ssh.rs index 45994a9916..b4fdcc96b2 100644 --- a/crates/navigator-cli/src/ssh.rs +++ b/crates/navigator-cli/src/ssh.rs @@ -236,7 +236,7 @@ pub async fn sandbox_exec( .stderr(std::process::Stdio::inherit()); // For interactive TTY sessions, replace this process with SSH via exec() - // to avoid signal handling issues (e.g. Ctrl+C killing the parent ncl + // to avoid signal handling issues (e.g. Ctrl+C killing the parent nemoclaw // process and orphaning the SSH child). if tty && std::io::stdin().is_terminal() { #[cfg(unix)] diff --git a/crates/navigator-tui/src/lib.rs b/crates/navigator-tui/src/lib.rs index 2bdbcaae7e..1638a3d4c2 100644 --- a/crates/navigator-tui/src/lib.rs +++ b/crates/navigator-tui/src/lib.rs @@ -634,7 +634,7 @@ async fn fetch_sandbox_detail(app: &mut App) { /// Suspend the TUI, launch an interactive SSH shell to the sandbox, resume on exit. /// -/// This replicates the `ncl sandbox connect` flow but uses `Command::status()` +/// This replicates the `nemoclaw sandbox connect` flow but uses `Command::status()` /// instead of `exec()` so the TUI process survives. async fn handle_shell_connect( app: &mut App, diff --git a/e2e/bash/test_sandbox_custom_image.sh b/e2e/bash/test_sandbox_custom_image.sh index 6c3a51f801..afe4322003 100755 --- a/e2e/bash/test_sandbox_custom_image.sh +++ b/e2e/bash/test_sandbox_custom_image.sh @@ -7,8 +7,8 @@ # with it. # # Verifies the full flow: -# 1. ncl sandbox image push --dockerfile (build + import into cluster) -# 2. ncl sandbox create --from -- (run sandbox with custom image) +# 1. nemoclaw sandbox image push --dockerfile (build + import into cluster) +# 2. nemoclaw sandbox create --from -- (run sandbox with custom image) # # Prerequisites: # - A running nemoclaw cluster (nemoclaw cluster admin deploy) diff --git a/examples/bring-your-own-container/README.md b/examples/bring-your-own-container/README.md index d8aeea37fe..68c2a11cab 100644 --- a/examples/bring-your-own-container/README.md +++ b/examples/bring-your-own-container/README.md @@ -29,7 +29,7 @@ nemoclaw sandbox image push \ ### 2. Create a sandbox with port forwarding ```bash -ncl sandbox create --from byoc-demo:latest --forward 8080 -- python /sandbox/app.py +nemoclaw sandbox create --from byoc-demo:latest --forward 8080 -- python /sandbox/app.py ``` The `--forward 8080` flag opens an SSH tunnel so `localhost:8080` on your diff --git a/examples/private-ip-routing/README.md b/examples/private-ip-routing/README.md index 1bdfa084bf..56e1bbfbb7 100644 --- a/examples/private-ip-routing/README.md +++ b/examples/private-ip-routing/README.md @@ -63,7 +63,7 @@ Create a sandbox and curl the private API through the proxy. Replace the IP with whatever `kubectl get pod` showed above: ```bash -nav sandbox create -- bash -c \ +nemoclaw sandbox create -- bash -c \ 'curl -s --proxytunnel -x http://10.200.0.1:3128 http://10.42.0.128:8080/' ``` diff --git a/tasks/scripts/cluster.sh b/tasks/scripts/cluster.sh index 5cb3f5f3f0..90273c66f6 100755 --- a/tasks/scripts/cluster.sh +++ b/tasks/scripts/cluster.sh @@ -16,4 +16,10 @@ if ! docker ps -q --filter "name=${CONTAINER_NAME}" | grep -q .; then exec tasks/scripts/cluster-bootstrap.sh fast fi +# Container is running but not healthy — tear it down and re-bootstrap. +if ! docker ps -q --filter "name=^${CONTAINER_NAME}$" --filter "health=healthy" | grep -q .; then + echo "Cluster container '${CONTAINER_NAME}' is running but not healthy. Recreating..." + exec tasks/scripts/cluster-bootstrap.sh fast +fi + exec tasks/scripts/cluster-deploy-fast.sh "$@" From 5c28a8b6c970f72f4acd6c25447f8ac713acb403 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 4 Mar 2026 20:56:33 -0800 Subject: [PATCH 3/3] refactor(cli): remove sandbox image subcommand in favor of --from The sandbox create --from flag already handles Dockerfile builds, image references, and community sandbox names. The separate sandbox image push command is redundant and adds unnecessary surface area to the CLI. --- .agents/skills/nemoclaw-cli/SKILL.md | 37 ++----- .agents/skills/nemoclaw-cli/cli-reference.md | 15 --- CONTRIBUTING.md | 30 +++--- architecture/sandbox-custom-containers.md | 20 ---- crates/navigator-bootstrap/src/build.rs | 2 +- crates/navigator-cli/src/main.rs | 69 +----------- crates/navigator-cli/src/run.rs | 108 ------------------- e2e/bash/test_sandbox_custom_image.sh | 28 +---- examples/bring-your-own-container/README.md | 27 ++--- 9 files changed, 36 insertions(+), 300 deletions(-) diff --git a/.agents/skills/nemoclaw-cli/SKILL.md b/.agents/skills/nemoclaw-cli/SKILL.md index 0d2422d910..eb64d8a1ef 100644 --- a/.agents/skills/nemoclaw-cli/SKILL.md +++ b/.agents/skills/nemoclaw-cli/SKILL.md @@ -317,37 +317,21 @@ nemoclaw sandbox policy get dev --rev 3 --full Build a custom container image and run it as a sandbox. -### Step 1: Build and push the image +### Step 1: Create a sandbox from a Dockerfile ```bash -nemoclaw sandbox image push \ - --dockerfile ./Dockerfile \ - --tag my-app:latest \ - --context . +nemoclaw sandbox create --from ./Dockerfile --keep --name my-app ``` -The image is built locally via Docker and imported directly into the cluster's containerd runtime. No external registry needed. +The `--from` flag accepts a Dockerfile path, a directory containing a Dockerfile, a full image reference (e.g. `myregistry.com/img:tag`), or a community sandbox name (e.g. `openclaw`). -Build arguments are supported: - -```bash -nemoclaw sandbox image push \ - --dockerfile ./Dockerfile \ - --tag my-app:v2 \ - --build-arg PYTHON_VERSION=3.12 -``` - -### Step 2: Create a sandbox with the custom image - -```bash -nemoclaw sandbox create --from my-app:latest --keep --name my-app -``` +When given a Dockerfile or directory, the image is built locally via Docker and imported directly into the cluster's containerd runtime. No external registry needed. When `--from` is specified, the CLI: - Clears default `run_as_user`/`run_as_group` (custom images may not have the `sandbox` user) - Uses a supervisor bootstrap pattern (init container copies the sandbox supervisor into a shared volume) -### Step 3: Forward ports (if the container runs a service) +### Step 2: Forward ports (if the container runs a service) ```bash # Foreground (blocks) @@ -359,7 +343,7 @@ nemoclaw sandbox forward start 8080 my-app -d The service is now reachable at `localhost:8080`. -### Step 4: Manage port forwards +### Step 3: Manage port forwards ```bash # List active forwards @@ -369,20 +353,19 @@ nemoclaw sandbox forward list nemoclaw sandbox forward stop 8080 my-app ``` -### Step 5: Iterate +### Step 4: Iterate To update the container: ```bash nemoclaw sandbox delete my-app -nemoclaw sandbox image push --dockerfile ./Dockerfile --tag my-app:v2 -nemoclaw sandbox create --from my-app:v2 --keep --name my-app --forward 8080 +nemoclaw sandbox create --from ./Dockerfile --keep --name my-app --forward 8080 ``` ### Shortcut: Create with port forward in one command ```bash -nemoclaw sandbox create --from my-app:latest --forward 8080 --keep -- ./start-server.sh +nemoclaw sandbox create --from ./Dockerfile --forward 8080 --keep -- ./start-server.sh ``` The `--forward` flag starts a background port forward before the command runs, so the service is reachable immediately. @@ -566,7 +549,7 @@ $ nemoclaw sandbox sync --help | Pull current policy | `nemoclaw sandbox policy get --full > p.yaml` | | Push updated policy | `nemoclaw sandbox policy set --policy p.yaml --wait` | | Policy revision history | `nemoclaw sandbox policy list ` | -| Build & push custom image | `nemoclaw sandbox image push --dockerfile ./Dockerfile` | +| Create sandbox from Dockerfile | `nemoclaw sandbox create --from ./Dockerfile --keep` | | Forward a port | `nemoclaw sandbox forward start -d` | | Create provider | `nemoclaw provider create --name N --type T --from-existing` | | List providers | `nemoclaw provider list` | diff --git a/.agents/skills/nemoclaw-cli/cli-reference.md b/.agents/skills/nemoclaw-cli/cli-reference.md index 83a312b53a..c26f05ac55 100644 --- a/.agents/skills/nemoclaw-cli/cli-reference.md +++ b/.agents/skills/nemoclaw-cli/cli-reference.md @@ -229,21 +229,6 @@ List all active port forwards (sandbox, port, PID, status). --- -## Custom Image Commands (BYOC) - -### `nemoclaw sandbox image push` - -Build a container image and push it into the cluster's internal registry. - -| Flag | Description | -|------|-------------| -| `--dockerfile ` | Path to Dockerfile (required) | -| `--tag ` | Image name and tag (default: `navigator/sandbox-custom:`) | -| `--context ` | Build context directory (default: Dockerfile parent) | -| `--build-arg KEY=VALUE` | Build argument (repeatable) | - ---- - ## Policy Commands ### `nemoclaw sandbox policy set --policy ` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5b856fc83..2d16976b3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -204,31 +204,25 @@ container. The default `run_as_user`/`run_as_group` policy is cleared for custom 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 +#### Building from a Dockerfile -Use `nemoclaw 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`: +Pass a Dockerfile path (or a directory containing one) to `--from` and the CLI will +build the image, push it into the cluster, and create the sandbox in a single step: ```bash -# Build and push from a Dockerfile -nemoclaw sandbox image push --dockerfile ./Dockerfile +# Build from a Dockerfile +nemoclaw sandbox create --from ./Dockerfile -# Specify a custom tag -nemoclaw sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest +# Build from a directory containing a Dockerfile +nemoclaw sandbox create --from ./my-sandbox/ -# Specify a build context directory -nemoclaw sandbox image push --dockerfile ./build/Dockerfile --context ./build - -# Pass build arguments -nemoclaw sandbox image push --dockerfile ./Dockerfile --build-arg PYTHON_VERSION=3.12 - -# Use the pushed image -nemoclaw sandbox create --image my-sandbox:latest +# Use a pre-built image +nemoclaw sandbox create --from 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. +The image is built using the local Docker daemon and pushed 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) diff --git a/architecture/sandbox-custom-containers.md b/architecture/sandbox-custom-containers.md index 54e6f4e462..64d297e41d 100644 --- a/architecture/sandbox-custom-containers.md +++ b/architecture/sandbox-custom-containers.md @@ -32,8 +32,6 @@ When `--from` points to a Dockerfile or directory, the CLI: 2. Pushes it into the cluster's containerd runtime using `docker save` / `ctr import`. 3. Creates the sandbox with the resulting image tag. -This is equivalent to running `nemoclaw sandbox image push` followed by `nemoclaw sandbox create --from ` in a single step. - ## How It Works When the resolved image differs from the server's default sandbox image, the server activates **supervisor bootstrap mode**. The supervisor binary is side-loaded from the default sandbox image via a Kubernetes init container: @@ -90,24 +88,6 @@ nemoclaw sandbox create --from ./Dockerfile -- echo "built and running" nemoclaw sandbox create --from ./my-sandbox/ # directory with Dockerfile ``` -### Pushing custom images into the cluster (manual two-step) - -```bash -nemoclaw sandbox image push --dockerfile ./Dockerfile --tag my-sandbox:latest -nemoclaw sandbox create --from my-sandbox:latest -``` - -`nemoclaw sandbox image push` accepts: - -| Flag | Description | -|------|-------------| -| `--dockerfile` (required) | Path to the Dockerfile | -| `--tag` | Image name and tag (default: `navigator/sandbox-custom:`) | -| `--context` | Build context directory (default: Dockerfile parent directory) | -| `--build-arg` | Repeatable `KEY=VALUE` Docker build arguments | - -The command builds the image locally via the Docker daemon (respecting `.dockerignore`), then imports it into the cluster's containerd runtime using a `docker save` / `ctr -n k8s.io images import` pipeline — the same mechanism used for component images during bootstrap. - ## Supervisor Behavior in Custom Images The `navigator-sandbox` supervisor adapts to arbitrary environments: diff --git a/crates/navigator-bootstrap/src/build.rs b/crates/navigator-bootstrap/src/build.rs index f4e9f7e401..ac2efb83a0 100644 --- a/crates/navigator-bootstrap/src/build.rs +++ b/crates/navigator-bootstrap/src/build.rs @@ -21,7 +21,7 @@ use crate::push::push_local_images; /// Build a container image from a Dockerfile and push it into the cluster. /// -/// This is the primary entry point for `nav sandbox image push`. It: +/// This is used by `nemoclaw sandbox create --from `. It: /// 1. Creates a tar archive of the build context directory. /// 2. Sends it to the local Docker daemon via `build_image()`. /// 3. Pushes the resulting image into the cluster's containerd via the diff --git a/crates/navigator-cli/src/main.rs b/crates/navigator-cli/src/main.rs index d8b9ca8c95..c755b20005 100644 --- a/crates/navigator-cli/src/main.rs +++ b/crates/navigator-cli/src/main.rs @@ -9,7 +9,6 @@ use clap_complete::env::CompleteEnv; use miette::Result; use owo_colors::OwoColorize; use std::io::Write; -use std::path::PathBuf; use navigator_bootstrap::{load_active_cluster, load_cluster_metadata}; use navigator_cli::completers; @@ -606,12 +605,6 @@ enum SandboxCommands { dest: Option, }, - /// Manage sandbox images. - Image { - #[command(subcommand)] - command: SandboxImageCommands, - }, - /// Manage sandbox policy. Policy { #[command(subcommand)] @@ -730,28 +723,6 @@ enum ForwardCommands { List, } -#[derive(Subcommand, Debug)] -enum SandboxImageCommands { - /// Build and push a container image into the cluster. - Push { - /// Path to the Dockerfile. - #[arg(long, value_hint = ValueHint::FilePath)] - dockerfile: PathBuf, - - /// Image name and tag (default: navigator/sandbox-custom:). - #[arg(long)] - tag: Option, - - /// Build context directory (default: Dockerfile parent directory). - #[arg(long, value_hint = ValueHint::DirPath)] - context: Option, - - /// Build argument in KEY=VALUE format (can be specified multiple times). - #[arg(long = "build-arg", value_name = "KEY=VALUE")] - build_args: Vec, - }, -} - #[derive(Subcommand, Debug)] enum InferenceCommands { /// Create an inference route. @@ -1054,31 +1025,12 @@ async fn main() -> Result<()> { } } } - SandboxCommands::Image { command } => match command { - SandboxImageCommands::Push { - dockerfile, - tag, - context, - build_args, - } => { - let cluster_name = resolve_cluster_name(&cli.cluster) - .unwrap_or_else(|| "nemoclaw".to_string()); - run::sandbox_image_push( - &dockerfile, - tag.as_deref(), - context.as_deref(), - &cluster_name, - &build_args, - ) - .await?; - } - }, other => { let ctx = resolve_cluster(&cli.cluster)?; let endpoint = &ctx.endpoint; let tls = tls.with_cluster_name(&ctx.name); match other { - SandboxCommands::Create { .. } | SandboxCommands::Image { .. } => { + SandboxCommands::Create { .. } => { unreachable!() } SandboxCommands::Sync { @@ -1458,25 +1410,6 @@ mod tests { 5, "Dockerfile", ), - ( - vec!["nemoclaw", "sandbox", "image", "push", "--dockerfile", "Do"], - 5, - "Dockerfile", - ), - ( - vec![ - "nemoclaw", - "sandbox", - "image", - "push", - "--dockerfile", - "Dockerfile", - "--context", - "c", - ], - 7, - "ctx/", - ), ]; for (raw_args, index, expected) in cases { diff --git a/crates/navigator-cli/src/run.rs b/crates/navigator-cli/src/run.rs index 22d0e6042c..6bf8abfd95 100644 --- a/crates/navigator-cli/src/run.rs +++ b/crates/navigator-cli/src/run.rs @@ -1728,114 +1728,6 @@ pub async fn sandbox_delete(server: &str, names: &[String], tls: &TlsOptions) -> Ok(()) } -/// Build and push a container image into the cluster. -pub async fn sandbox_image_push( - dockerfile: &Path, - tag: Option<&str>, - context: Option<&Path>, - cluster_name: &str, - build_args: &[String], -) -> Result<()> { - // Validate the Dockerfile exists. - if !dockerfile.exists() { - return Err(miette::miette!( - "Dockerfile not found: {}", - dockerfile.display() - )); - } - - // Resolve the Dockerfile to an absolute path. - let dockerfile = dockerfile - .canonicalize() - .into_diagnostic() - .wrap_err_with(|| { - format!( - "failed to resolve Dockerfile path: {}", - dockerfile.display() - ) - })?; - - // Resolve the build context directory (default: Dockerfile parent directory). - let context_dir = match context { - Some(ctx) => ctx - .canonicalize() - .into_diagnostic() - .wrap_err_with(|| format!("failed to resolve context path: {}", ctx.display()))?, - None => dockerfile - .parent() - .ok_or_else(|| miette::miette!("Dockerfile has no parent directory"))? - .to_path_buf(), - }; - - if !context_dir.is_dir() { - return Err(miette::miette!( - "Build context is not a directory: {}", - context_dir.display() - )); - } - - // Parse build args from KEY=VALUE strings. - let mut build_arg_map = HashMap::new(); - for arg in build_args { - let Some((key, value)) = arg.split_once('=') else { - return Err(miette::miette!( - "--build-arg expects KEY=VALUE, got '{arg}'" - )); - }; - build_arg_map.insert(key.to_string(), value.to_string()); - } - - // Generate a default tag if not provided. - let default_tag; - let tag = if let Some(t) = tag { - t - } else { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - default_tag = format!("navigator/sandbox-custom:{timestamp}"); - &default_tag - }; - - eprintln!( - "Building image {} from {}", - tag.cyan(), - dockerfile.display() - ); - eprintln!(" {} {}", "Context:".dimmed(), context_dir.display()); - eprintln!(" {} {}", "Cluster:".dimmed(), cluster_name); - eprintln!(); - - let mut on_log = |msg: String| { - eprintln!(" {msg}"); - }; - - navigator_bootstrap::build::build_and_push_image( - &dockerfile, - tag, - &context_dir, - cluster_name, - &build_arg_map, - &mut on_log, - ) - .await?; - - eprintln!(); - eprintln!( - "{} Image {} is available in the cluster.", - "✓".green().bold(), - tag.cyan(), - ); - eprintln!(); - eprintln!( - "Use it with: {}", - format!("nemoclaw sandbox create --from {tag}").dimmed() - ); - - Ok(()) -} - /// Return the provider type inferred from the trailing command, if any. fn inferred_provider_type(command: &[String]) -> Option { detect_provider_from_command(command).map(str::to_string) diff --git a/e2e/bash/test_sandbox_custom_image.sh b/e2e/bash/test_sandbox_custom_image.sh index afe4322003..b7d6973e04 100755 --- a/e2e/bash/test_sandbox_custom_image.sh +++ b/e2e/bash/test_sandbox_custom_image.sh @@ -7,8 +7,7 @@ # with it. # # Verifies the full flow: -# 1. nemoclaw sandbox image push --dockerfile (build + import into cluster) -# 2. nemoclaw sandbox create --from -- (run sandbox with custom image) +# nemoclaw sandbox create --from -- # # Prerequisites: # - A running nemoclaw cluster (nemoclaw cluster admin deploy) @@ -35,7 +34,6 @@ else NAV="nemoclaw" fi -IMAGE_TAG="e2e-custom-image:test-$(date +%s)" SANDBOX_NAME="" TMPDIR_ROOT="" @@ -95,32 +93,14 @@ CMD ["sleep", "infinity"] DOCKERFILE_CONTENT ############################################################################### -# Step 2 — Build and push the image into the cluster +# Step 2 — Create a sandbox from the Dockerfile and verify it works ############################################################################### -info "Building and pushing custom image: ${IMAGE_TAG}" - -PUSH_LOG=$(mktemp) -if ! "${NAV}" sandbox image push \ - --dockerfile "${DOCKERFILE}" \ - --tag "${IMAGE_TAG}" \ - > "${PUSH_LOG}" 2>&1; then - error "Image push failed" - cat "${PUSH_LOG}" >&2 - exit 1 -fi - -info "Image pushed successfully" - -############################################################################### -# Step 3 — Create a sandbox with the custom image and verify it works -############################################################################### - -info "Creating sandbox with custom image: ${IMAGE_TAG}" +info "Creating sandbox from Dockerfile" CREATE_LOG=$(mktemp) if ! "${NAV}" sandbox create \ - --from "${IMAGE_TAG}" \ + --from "${DOCKERFILE}" \ -- cat /opt/marker.txt \ > "${CREATE_LOG}" 2>&1; then error "Sandbox create failed" diff --git a/examples/bring-your-own-container/README.md b/examples/bring-your-own-container/README.md index 68c2a11cab..b69e12390c 100644 --- a/examples/bring-your-own-container/README.md +++ b/examples/bring-your-own-container/README.md @@ -18,19 +18,17 @@ your local machine through port forwarding. ## Quick start -### 1. Build and push the image +### 1. Create a sandbox from the Dockerfile with port forwarding ```bash -nemoclaw sandbox image push \ - --dockerfile examples/bring-your-own-container/Dockerfile \ - --tag byoc-demo:latest +nemoclaw sandbox create \ + --from examples/bring-your-own-container/Dockerfile \ + --forward 8080 \ + -- python /sandbox/app.py ``` -### 2. Create a sandbox with port forwarding - -```bash -nemoclaw sandbox create --from byoc-demo:latest --forward 8080 -- python /sandbox/app.py -``` +The `--from` flag accepts a Dockerfile path. The CLI builds the image, +pushes it into the cluster, and creates the sandbox in one step. The `--forward 8080` flag opens an SSH tunnel so `localhost:8080` on your machine reaches the REST API inside the sandbox. @@ -40,7 +38,7 @@ NemoClaw replaces it with the sandbox supervisor (which manages SSH access, network policy, etc.). You must pass your application's start command after `--` so it is executed via SSH once the sandbox is ready. -### 3. Hit the API +### 2. Hit the API ```bash curl http://localhost:8080/hello @@ -80,15 +78,6 @@ Port forwarding is entirely client-side: the CLI spawns a background `ssh -L` tunnel through the gateway. The sandbox's embedded SSH daemon bridges the tunnel to `127.0.0.1:` inside the container. -## Push flags - -| Flag | Description | -| -------------- | -------------------------------------------------------- | -| `--dockerfile` | Path to the Dockerfile (required) | -| `--tag` | Image name and tag (default: auto-generated) | -| `--context` | Docker build context directory for COPY/ADD (default: Dockerfile parent dir) | -| `--build-arg` | Repeatable `KEY=VALUE` Docker build arguments | - ## Cleanup Delete the sandbox when you're done (this also stops port forwards):