Commit Graph
269 Commits
Author SHA1 Message Date
Drew Newberry 6dc97171ed feat(sandbox): add image build/push and fix cluster deploy (!34)
Closes #44

## Summary

- Add `nav sandbox image push --dockerfile <path>` command to build and push custom container images into the cluster
- Fix sandbox Docker build failure caused by OpenClaw installer TTY access
- Fix sandbox proxy crash when network namespace is unavailable (fall back to loopback)
- Add e2e test for the full custom image build and sandbox creation flow
- Rename and rewrite BYOC architecture doc to reflect current state

## Changes

### `navigator-bootstrap`
- New `build` module (`src/build.rs`) with `build_and_push_image()` public API
- Builds via bollard `Docker::build_image()`, streams output to caller
- `.dockerignore` support with glob matching, 8 unit tests

### `navigator-cli`
- Added `SandboxImageCommands::Push` subcommand with `--dockerfile`, `--tag`, `--context`, `--build-arg` flags
- Added `test:e2e:custom-image` mise task

### `navigator-sandbox`
- Proxy now falls back to `127.0.0.1:3128` when no network namespace or explicit bind address is available, instead of erroring out

### Docker / Infra
- Sandbox Dockerfile: replaced OpenClaw native installer (`curl | bash`) with `npm install -g openclaw` to avoid `/dev/tty` failures in Docker build

### Documentation
- Renamed `architecture/sandbox-byoc.md` to `sandbox-custom-containers.md`
- Rewrote doc to describe current behavior (CLI usage, push pipeline, supervisor behavior, design decisions) instead of listing implementation changes

### Tests
- Added `e2e/bash/test_sandbox_custom_image.sh` covering image push + sandbox create with custom image
2026-02-22 20:32:18 -08:00
John Myers 82e6b81324 fix(security): reject CONNECT to internal IPs (SSRF defense-in-depth) (!37)
> **🔧 security-fix-agent**

Closes #62

## Security Fix

### Summary
The CONNECT proxy accepted hostnames from clients and connected to whatever IP they resolved to, with no validation against internal address ranges. While the OPA policy is default-deny, a misconfigured or overly permissive policy could allow SSRF to cloud metadata (169.254.169.254), localhost, or RFC1918 services. This fix adds DNS resolution before connecting and rejects any host that resolves to an internal IP.

### Severity Assessment
- **Impact:** Medium — if exploited, could reach cloud metadata (IAM creds), cluster-internal services, or host-local services
- **Exploitability:** Very low — requires OPA policy misconfiguration or DNS rebinding attack
- **Affected components:** `crates/navigator-sandbox/src/proxy.rs` — `handle_tcp_connection`

### Changes Made
- `crates/navigator-sandbox/src/proxy.rs`: Added `is_internal_ip()` helper that checks IPv4 loopback/private/link-local, IPv6 loopback/link-local, and IPv4-mapped IPv6. Added `resolve_and_reject_internal()` that resolves DNS and rejects internal IPs. Inserted check between OPA allow and `TcpStream::connect`, with control plane endpoints exempt.
- `architecture/security-policy.md`: Added SSRF Protection section with blocked ranges table and flow diagram
- `architecture/sandbox.md`: Updated proxy connection flow diagram and added SSRF protection subsection
- `architecture/README.md`: Added internal IP rejection step to proxy description

### Tests Added
- **Unit:** 17 tests in `proxy::tests` — covers IPv4 loopback/private/link-local, IPv6 loopback/link-local, IPv4-mapped IPv6, public IPs, DNS resolution of localhost/127.0.0.1/169.254.169.254, and DNS failure handling
- **Integration/E2E:** N/A — the proxy runs inside a Linux network namespace; unit tests for IP checking and DNS resolution cover the security boundary

### Documentation Updated
- `architecture/security-policy.md`: New SSRF Protection section with blocked IP ranges and Mermaid flowchart
- `architecture/sandbox.md`: Updated proxy flow diagram and added SSRF protection subsection
- `architecture/README.md`: Added step 4 to proxy description

### Verification
All 85 sandbox tests pass including 17 new proxy SSRF tests. Pre-commit (fmt, clippy, full test suite) passes clean with zero warnings.
2026-02-20 10:17:46 -08:00
Drew Newberry e1c5e5a965 chore(docs): update uv install directions to ensure latest 2026-02-20 07:58:40 -08:00
Drew Newberry ad388219c2 chore(tests): cleanup unused tests 2026-02-19 23:20:21 -08:00
Drew Newberry 98c0e08423 feat(sandbox): add --policy flag for custom sandbox policy and allow /dev/null in filesystem policy
Add a --policy CLI flag to sandbox create that overrides the built-in
default and NAVIGATOR_SANDBOX_POLICY env var for specifying a custom
sandbox policy YAML file. Also add /dev/null to the default filesystem
read_write list and update security-policy docs with formatting fixes.
2026-02-19 23:12:32 -08:00
Drew Newberry 14859fd48e fix(cluster): remove stale image on destroy and verify architecture after pull
Destroy now removes the cluster image so the next deploy always pulls the
latest version from the registry.  After pulling on a remote host the
image architecture is verified against the host platform, surfacing a
clear error instead of a cryptic 'exec format error' at container start.
2026-02-19 21:39:46 -08:00
John Myers 302551aca7 security skills (!36)
Add skills that we can use for reviewing and fixing security reports/issues.
2026-02-19 20:00:29 -08:00
John Myers ee989e8c80 Docs(organize docs add sub agents) (!35)
Add two sub-agents that have been useful thus far. The documentation agent has memory that is tracked.

Used the docs sub-agent to create detailed docs for all sub-systems + and overview doc.
2026-02-19 18:04:36 -08:00
Drew Newberry 1c939a255a feat(sandbox): enable port forwarding and setup openclaw (!33) 2026-02-19 16:03:23 -08:00
John Myers f1439727b9 feat(sandbox): L7 protocol-aware inspection with TLS termination (!29)
## Summary

Implements L7 (application-layer) protocol-aware policy enforcement for the sandbox proxy, enabling per-request allow/deny decisions based on HTTP method and path — not just host:port.

### Phase 1: HTTP/REST L7 Inspection (plaintext)
- New `l7/` module with provider trait, REST parser, and relay loop
- Parses HTTP/1.1 requests inside CONNECT tunnels, evaluates method+path against OPA/Rego policy
- Supports `access` presets (`full`, `read-only`, `read-write`) and explicit `rules` with glob path matching
- `enforcement` modes: `enforce` (deny with 403 JSON) vs `audit` (log only, forward traffic)
- Structured logging for every L7 decision (`L7_REQUEST` with protocol, action, target, decision)

### Phase 2: MITM TLS Termination for HTTPS
- Ephemeral CA generated at sandbox startup (`rcgen`)
- Per-hostname leaf certificate cache (256 cap) for dynamic cert presentation
- Client-side TLS termination (sandbox trusts ephemeral CA via `SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`, etc.)
- Upstream TLS connection with `webpki-roots` verification
- Endpoints with `tls: terminate` get full L7 inspection; others remain passthrough

### Additional
- Benign TLS/connection errors (close_notify, handshake EOF, reset) downgraded from WARN to DEBUG
- Generic `AsyncRead + AsyncWrite` bounds throughout L7 module (works with both `TcpStream` and `TlsStream`)
- Proto schema extended with `protocol`, `tls`, `enforcement`, `access`, `rules` fields on `NetworkEndpoint`
- CLI `nav sandbox run` wired with `--rego-policy`/`--rego-data` flags and L7 policy display

### E2E Test Coverage
- 7 L4 tests: no-matching-policy deny, wildcard binary, binary-restricted, wrong port, cross-policy isolation, non-CONNECT 405, structured log fields
- 7 L7 TLS tests: full access allow, read-only deny POST, audit mode, explicit path rules, CA trust store injection, deny response JSON format, structured log fields

## Key Files

| Area | Files |
|------|-------|
| L7 core | `l7/mod.rs`, `l7/provider.rs`, `l7/relay.rs`, `l7/rest.rs` |
| TLS termination | `l7/tls.rs` |
| Proxy integration | `proxy.rs`, `lib.rs`, `main.rs` |
| Env/process | `process.rs`, `ssh.rs` |
| OPA policy | `opa.rs`, `dev-sandbox-policy.rego` |
| Proto schema | `proto/sandbox.proto` |
| CLI | `navigator-cli/src/run.rs` |
| E2E tests | `e2e/python/test_sandbox_policy.py` |

## Test plan

- [x] 67 unit tests pass (`cargo test --workspace`)
- [x] `cargo clippy --workspace --all-targets` clean
- [x] 16 e2e policy tests pass (`mise run test:e2e:sandbox`)
- [x] Manual smoke test: sandbox with `tls: terminate` on `api.anthropic.com:443`

Closes #25
2026-02-19 09:01:26 -08:00
Piotr Mlocek a5400113fb chore(ci): add Linux-hosted macOS wheel builds (!30)
## Summary
- add a Docker-based macOS arm64 wheel build path so darwin wheels can be produced from Linux CI hosts
- wire macOS wheel builds into release publish workflows (`publish:main` and `publish:tag`) alongside existing Linux wheel builds
- mirror `crazymax/osxcross:latest` into `$CI_REGISTRY_IMAGE/third_party/osxcross:latest` and use that mirrored image in CI publish jobs

## Scope Notes
- this MR focuses on shipping reliable macOS wheel build support first
- temporary MR-only smoke-job wiring was removed after validation so the branch reflects final intended CI behavior
- broader cache optimization work is tracked separately in issue #43

## Test Plan
- run `mise run pre-commit`
- validate `.gitlab-ci.yml` with `uv run --with pyyaml python -c 'import yaml, pathlib; yaml.safe_load(pathlib.Path(".gitlab-ci.yml").read_text())'`
2026-02-17 22:25:52 -08:00
Drew Newberry 89a6e04c8c feat(providers): inject provider credentials into sandbox child processes at runtime (!26)
## Summary
- Add `GetSandboxProviderEnvironment` gRPC endpoint so the sandbox supervisor can fetch provider credentials at runtime instead of embedding them in the pod spec
- Sandbox supervisor (`navigator-sandbox`) fetches credentials on startup and injects them as environment variables into both entrypoint processes and SSH shell sessions via `Command::env()`
- Remove credential injection from sandbox creation-time pod spec in `navigator-server`
- Update `architecture/providers.md` to document the full runtime credential injection flow, discovery engine details, trait definitions, and end-to-end diagram

## Changes
- **proto**: Add `GetSandboxProviderEnvironment` RPC and request/response messages, add `providers` field to `SandboxSpec`
- **navigator-server**: Implement `GetSandboxProviderEnvironment` handler and `resolve_provider_environment()` with env var key validation and first-wins dedup; remove old creation-time credential injection from `sandbox/mod.rs`
- **navigator-sandbox**: Fetch provider env via gRPC on startup (`grpc_client.rs`), thread `provider_env` HashMap through to `process.rs` (entrypoint) and `ssh.rs` (shell sessions), inject via `cmd.env()`
- **navigator-cli**: Adjust sandbox create flow to set provider names in `SandboxSpec.providers`
- **e2e**: Add `test_sandbox_providers.py` end-to-end tests
- **docs**: Rewrite `architecture/providers.md` with implementation details

## Test Plan
- Unit tests for `resolve_provider_environment` in `grpc.rs`
- Integration tests updated in `provider_commands_integration.rs`, `mtls_integration.rs`, `multiplex_integration.rs`, `multiplex_tls_integration.rs`
- New e2e test suite `test_sandbox_providers.py`
2026-02-17 13:40:10 -08:00
Piotr Mlocek 6e85e1b913 chore(ci): enable wheel publishing on main 2026-02-17 10:43:12 -08:00
Drew Newberry 8a38d1b56f fix(cli): use raw cluster name for remote kubeconfig path lookup (!25)
Closes #28

## Summary
- `--update-kube-config` and `--get-kubeconfig` appended a `-remote` suffix to the cluster name when resolving the stored kubeconfig path, but the deploy step writes the kubeconfig under the raw name — so the suffixed path never existed, causing both flags to fail for remote clusters.
- Removed the suffix logic so the lookup path matches the deploy path. The `-remote` suffix is still applied to internal kubeconfig YAML entries (cluster/context/user names) by `rewrite_kubeconfig_remote`, which is unaffected.
- Inlined the `is_remote` variable since it was only used to compute the display-only `location` string.
2026-02-17 07:43:42 -08:00
Drew Newberry f362963c7b feat(sandbox): add provider entity to support configuring tools such as claude, outlook, etc (!23)
## Summary
- Add `Provider` entity for managing 3p deps from a sandbox
- Add provider CRUD API/server persistence and new CLI workflows (`nav provider create/get/list/update/delete`), including `--from-existing` laptop discovery.
- Integrate providers into sandbox create flow: infer from command (`-- claude`), support repeatable `--provider <type>`, prompt before auto-create, and allow manual in-sandbox setup.
- Add a dedicated `navigator-providers` crate with per-provider modules and mockable discovery test helpers.

## Key UX Changes
- `nav sandbox create --provider gitlab -- claude`
- Missing provider prompt now asks before creating from local state.
- `nav provider list --names` for scripting/cleanup.

## Test Plan
- `mise run cluster:deploy`
- `mise run test:e2e:sandbox`
- `mise run pre-commit`

Closes #19
Closes #22
Closes #11
2026-02-16 14:40:32 -08:00
Drew Newberry 58beed8502 chore: ssh session set_nodelay(true) 2026-02-16 12:17:19 -08:00
Drew Newberry 9f90874e09 chore: fix clippy warnings 2026-02-16 09:16:30 -08:00
Piotr Mlocek 71a2213a3f fix(ci): make the multiplatform wheel build work in CI 2026-02-13 19:01:14 -08:00
Piotr Mlocek 566010db18 fix(ci): install cargo:cargo-edit on the CI image, add file that got missed 2026-02-13 15:21:09 -08:00
Piotr Mlocek 96e9945cfd chore(ci): add Python wheel publishing + tag release in CI (!22)
## What changed

- Added tag-driven release flow in GitLab CI:
  - new `release` stage with tag-only jobs
  - `publish_tag_artifacts` publishes Docker + Python artifacts on `vX.Y.Z` tags
  - `create_release_notes` generates release notes from conventional commits via `git-cliff` and creates a GitLab release via `glab`
- Updated main-branch image publishing to version-aware tagging:
  - `publish_ecr_images` now runs `mise run publish:main`
  - main publishes `:dev`, `:latest`, and a versioned dev tag
- Added release-oriented mise tasks:
  - `publish:main`
  - `publish:tag`
  - `python:publish:macos` (manual macOS arm64 wheel publish)
- Switched Linux Python wheel builds to buildx:
  - added `deploy/docker/Dockerfile.python-wheels`
  - replaced old per-arch docker-run tasks with `python:build:multiarch`
- Added macOS arm64 wheel build path for local publishing:
  - `python:build:macos` builds `aarch64-apple-darwin`
  - intended to run locally on macOS after tag CI finishes
- Made Docker multiarch publish script tag-flexible:
  - `TAG_LATEST` is no longer hardcoded in ECR mode
  - supports `EXTRA_DOCKER_TAGS`
  - applies extra tags to sandbox/server/pki-job/cluster images
- Moved release tooling to `build/scripts/release.py` and updated all mise references
- Removed obsolete Docker Artifactory env vars from `mise.toml`

## Release behavior

- **Main branch CI**
  - Docker: `:dev`, `:latest`, and versioned dev tag
- **Tag CI (`vX.Y.Z`)**
  - Docker: `:X.Y.Z` only (no `:latest`)
  - Python: Linux wheels published from CI
  - GitLab release notes created from conventional commits
- **Manual macOS step (after tagging)**
  1. Checkout the tag locally on macOS
  2. Run `mise run python:publish:macos`

## Validation

- `mise run python:lint`
- `mise run version:print`
- `mise run python:build:macos`
- `uv run python build/scripts/release.py --help`
2026-02-13 12:13:04 -08:00
Drew Newberry 4ef5c0d2dd chore(ci): speed up ci builds and improve caching (!20) 2026-02-13 11:06:25 -08:00
Drew Newberry ae7378e5ce chore: ignore plans for now 2026-02-12 23:33:25 -08:00
Drew Newberry d2f3ca71d4 feat(sandbox): add callable python exec API and refresh e2e coverage (!19)
Closes #13

## Summary
- add Python sandbox execution APIs for command and callable workflows
- consolidate sandbox policy fixtures and expand e2e test coverage for policy and Python exec paths
- update CI/build config and images for sandbox e2e execution dependencies

## Test Plan
- mise run pre-commit
2026-02-12 23:30:24 -08:00
Piotr Mlocek 6cf0264170 refactor(sandbox): consolidate policy data into YAML, remove rego data file (!18)
Closes #26

## Summary

Consolidates sandbox policy data into `dev-sandbox-policy.yaml` and deletes `dev-sandbox-policy-data.rego`. The YAML file becomes the single source of truth for sandbox policy data across both paths:

- **`nav create sandbox`** (CLI reads YAML → proto → gRPC → server stores → sandbox fetches)
- **`mise run sandbox`** (sandbox reads YAML directly via regorus)

## Why YAML

The policy data file is user-facing — it's the interface users interact with to configure sandbox security policy (allowed endpoints, binary identities, filesystem access, etc.).

Keeping this as YAML rather than rego:

- **Decouples data from enforcement engine** — if we change from OPA/rego to a different policy engine, the user's data file stays the same
- **Still works with regorus** — regorus can natively load YAML data via its `yaml` feature, so there's no conversion step in the local dev path
- **Enables future indirection** — we can add a transformation layer between the YAML file and what the policy engine receives, without changing the user-facing format

## Changes

### Policy data (`dev-sandbox-policy.yaml`)
- Flat top-level structure: `filesystem_policy`, `landlock`, `process`, `inference`, `network_policies`
- Renamed `filesystem` → `filesystem_policy` to match rego/proto convention
- Deleted `dev-sandbox-policy-data.rego` (was the rego equivalent of this data)

### Rego rules (`dev-sandbox-policy.rego`)
- All data references updated: `data.sandbox.*` → `data.*` (dropped `sandbox.` prefix)
- `package navigator.sandbox` unchanged (that's the rego rule namespace, not the data path)

### Sandbox entrypoint (`navigator-sandbox`)
- `--rego-policy`/`--rego-data` → `--policy-rules`/`--policy-data`
- `NAVIGATOR_REGO_POLICY`/`NAVIGATOR_REGO_DATA` → `NAVIGATOR_POLICY_RULES`/`NAVIGATOR_POLICY_DATA`
- `from_files()`: loads YAML natively via `regorus::Value::from_yaml_file()`
- `from_strings()` and `reload()`: accept YAML data via `Value::from_yaml_str()`
- `proto_to_opa_data_json()`: emits data at root level (no `"sandbox"` wrapper)
- Enabled regorus `yaml` feature

### CLI (`navigator-cli`)
- `DevSandboxPolicyFile.filesystem` → `filesystem_policy`

### Tests
- All inline test data converted from rego/JSON to YAML
- `test_engine()` uses `from_strings(rules, yaml)` with the dev YAML file

### Documentation
- Updated `architecture/plans/sandbox-policy-refactor/plan.md`, `architecture/plans/opa/plan.md`, `architecture/sandbox.md`, `architecture/containers.md`
2026-02-12 17:09:07 -08:00
Piotr Mlocek aab1e34243 fix(sandbox): enforce network namespace and proxy policy in SSH sessions (!17)
Closes #24

## Problem

SSH sessions into the sandbox were **not entering the network namespace** or receiving proxy environment variables. This meant every command run via SSH (the only user-facing path) had unrestricted internet access, completely bypassing OPA network policy enforcement.

The root cause: the SSH server was started **before** the network namespace and proxy were created in `lib.rs`, so it never received the netns fd or proxy URL.

Additionally, **gRPC inference from within the sandbox did not work at all** — even after fixing the netns, multiple issues prevented the Python SDK from reaching the navigator server through the CONNECT proxy.

## Changes

### Sandbox binary

**Core fix — reorder startup + thread netns through SSH:**
- `lib.rs`: Move netns + proxy creation before SSH server start. Compute `ssh_netns_fd` and `ssh_proxy_url`, pass them to `run_ssh_server()`.
- `ssh.rs`: Thread `netns_fd` and `proxy_url` through the full SSH call chain into `spawn_pty_shell()`. Set proxy env vars on the shell command. Call `setns(fd, CLONE_NEWNET)` in `install_pre_exec()`.

**Proxy — control plane allowlist + IPv6 socket lookup:**
- `proxy.rs`: Connections to the navigator endpoint (derived from `NAVIGATOR_ENDPOINT`) are always allowed without OPA evaluation, logged with `engine=control_plane`. This is infrastructure the sandbox needs to function, not a user-configurable policy.
- `procfs.rs`: Extended `parse_proc_net_tcp` to also check `/proc/<pid>/net/tcp6`. gRPC C-core uses `AF_INET6` sockets even for IPv4 connections, so its TCP entries were invisible to the proxy's identity resolver. Also fixed port parsing to use `rsplit_once(':')` for correct IPv6 address handling.

**Proxy env vars — lowercase variants for gRPC C-core:**
- `process.rs` + `ssh.rs`: Added lowercase `http_proxy`, `https_proxy`, `grpc_proxy` alongside uppercase. gRPC C-core (libgrpc) checks lowercase first and was ignoring the uppercase-only vars.

### Server

- `sandbox/mod.rs`: Added `CAP_SYS_PTRACE` to sandbox pod security context. Required for the proxy (root) to read `/proc/<pid>/fd/` of sandbox-user processes for binary identity resolution.

### Python SDK

- `inference.py`: Strip `http://`/`https://` scheme from endpoint before passing to `grpc.insecure_channel()`, which expects `host:port`. Default `endpoint` and `sandbox_id` from `NAVIGATOR_ENDPOINT` / `NAVIGATOR_SANDBOX_ID` env vars so `Inference()` works inside sandboxes with no arguments.

### Build / infra

- `ci.toml`: Added `--cap-add=SYS_PTRACE` to `mise run sandbox` to mirror the k8s pod capabilities.

### Documentation

- `architecture/sandbox.md`: Documented `CAP_SYS_PTRACE` requirement and the full set of proxy env vars (uppercase + lowercase).

## Testing

All 29 sandbox unit tests pass. `mise run pre-commit` passes (fmt, clippy, all workspace tests, python lint).

E2E verified on a live cluster:

| Test | Result |
|------|--------|
| Proxy env vars (6 vars, upper+lowercase) | PASS |
| Blocked endpoints (google, anthropic via curl) | PASS — all denied |
| **gRPC inference from SSH session** (`Inference()` with no args, env var defaults) | **PASS** |
| Proxy log: navigator requests show `engine=control_plane` | PASS |
| Proxy log: blocked requests show `engine=opa` with correct deny reasons | PASS |
2026-02-12 14:48:00 -08:00
Piotr Mlocek c094769a43 feat(server): add an inference router (!13)
Closes #3
2026-02-11 19:09:14 -08:00
Piotr Mlocek cacadd7a98 fix(cluster): preserve gateway TLS settings during cluster deploy 2026-02-11 12:45:55 -08:00
Drew Newberry 0b6aa0f742 feat(cluster): push locally-built images into k3s containerd for local dev (!16)
## Summary
- Add a "push" path for local development that exports Docker images from the host daemon and imports them directly into the k3s containerd runtime, eliminating the need for a local registry or remote image pulls during `mise run cluster`
- Add `mise run cluster:deploy` task for fast iterative rebuilds (~25s) against a running cluster without recreating it
- Update `cluster-entrypoint.sh` to patch HelmChart manifest with local image tags and `IfNotPresent` pull policy when push mode is active

## Details

Previously, after the local registry (k3d + skaffold) was removed, `mise run cluster` only pulled component images from the remote CloudFront registry. This meant local code changes required a remote publish cycle to test in-cluster.

This MR adds a push path that sits alongside the existing pull path:
- **`push.rs`** — Uses bollard to `export_images()` all 3 component images into a single tar (with shared layer dedup), uploads via Docker `put_archive` API into the cluster container, then runs `ctr images import` to load them into containerd
- **`docker.rs`** — Passes `IMAGE_TAG` and `IMAGE_PULL_POLICY` env vars into the cluster container when `NAVIGATOR_PUSH_IMAGES` is set
- **`lib.rs`** — Wires push into the deploy flow between `clean_stale_nodes()` and `wait_for_cluster_ready()`, guarded by `remote_opts.is_none() && NAVIGATOR_PUSH_IMAGES`
- **`cluster-entrypoint.sh`** — Patches the baked-in HelmChart manifest with sed to use the local image tag and pull policy before k3s starts
- **`cluster.toml`** — Updated `[cluster]` task to tag and push images; new `[cluster:deploy]` task for iterative deploys via `docker save | ctr import` + `helm upgrade`

The remote deploy path (`nav cluster admin deploy --remote`) is completely unaffected — the push logic only activates for local deploys with the env var set.

## Test Plan
- `mise run cluster` — end-to-end success, cluster healthy with local images (~168s)
- `mise run cluster:deploy` — iterative deploy success, helm upgrade + rollout (~43s)
- `mise run pre-commit` — all checks pass (fmt, clippy, 82 tests, python lint)
- Verified images present in containerd: `ctr -a /run/k3s/containerd/containerd.sock images ls | grep navigator/`
- Remote path verified safe via code analysis (guards prevent push logic from running)
2026-02-11 09:26:02 -08:00
Drew Newberry 894c419e93 fix(ci): publish_ecr_images correctly publishes images (!15) 2026-02-10 23:04:32 -08:00
Drew Newberry d159cba8da fix(ci): unset TLS env vars before buildx to avoid context conflict 2026-02-10 18:52:39 -08:00
Drew Newberry 0109d7fdf6 fix(ci): create docker context for TLS-enabled DinD before buildx 2026-02-10 18:22:52 -08:00
Drew Newberry 868348ce00 fix(ci): create multi-platform buildx builder for ECR publish mode 2026-02-10 17:55:07 -08:00
Drew Newberry ba849c8540 fix(ci): install docker buildx plugin for multi-arch image builds 2026-02-10 17:25:13 -08:00
Drew Newberry b702b98265 chore: cleanup and organize build files + publish containers 2026-02-10 15:38:40 -08:00
Piotr Mlocek ccee9b4d33 fix(docs): update quickstart command 2026-02-10 15:16:24 -08:00
John Myers 87b2446501 feat(sandbox): OPA policy engine with process-identity binding 2026-02-10 10:57:31 -08:00
Drew Newberry 4f8c8fcd31 chore: remove plans for now 2026-02-10 10:05:51 -08:00
Drew Newberry c2a8fec205 feat: add skill for reviewing gitlab mrs 2026-02-10 08:52:34 -08:00
Drew Newberry 24b9654ea8 feat(cluster): add remote SSH deployment 2026-02-10 08:25:43 -08:00
Piotr Mlocek 53157595b5 fix(cluster): use iptables DNS proxy instead of host gateway for k3s DNS
The previous approach wrote the host.docker.internal IP to the k3s
resolv.conf, assuming it would serve DNS. On Docker Desktop, this IP
is reachable but doesn't run a DNS server, causing CoreDNS to fail
resolving external names and breaking the PKI job and Helm install.

The new approach discovers Docker's embedded DNS listener ports from
the DOCKER_OUTPUT iptables chain, then sets up DNAT rules to forward
DNS traffic from k3s pods (via PREROUTING) to Docker's actual DNS
resolver on 127.0.0.11. The container's eth0 IP is used as the
nameserver in the k3s resolv.conf since it's routable from all pods.
2026-02-06 17:34:40 -08:00
Piotr Mlocek eb1d67ba83 build: add publishing for docker images and python wheel 2026-02-05 19:27:42 -08:00
Drew Newberry 00f432de8c feat: add mtls support to plaform 2026-02-05 17:24:48 -08:00
John Myers ff127c6ab2 chore(sandbox): add networking tools to sandbox image 2026-02-05 15:54:02 -08:00
Drew Newberry 44f5c80921 chore: add claude/skills link to agent/skills 2026-02-05 09:25:48 -08:00
Drew Newberry d97183d65f feat(cli): add run semantics to sandbox create 2026-02-05 08:06:44 -08:00
Drew Newberry e34444adf9 feat(sandbox): add ssh connect to sandbox + build agent harness 2026-02-05 07:58:20 -08:00
Drew Newberry 3c580b7cbf test: add basic lint and test checks to ci 2026-02-05 00:14:13 -08:00
Drew Newberry 207ebe4a43 chore: cleanup docker/kube/helm infra 2026-02-04 23:00:17 -08:00
John Myers 718f54fefd Merge branch 'fix/sandbox-netns-isolation' into 'main'
fix(sandbox): add network namespace isolation for proxy mode

See merge request navigator/navigator!4
2026-02-04 15:54:56 -08:00
John Myers 96cd21fae9 docs(sandbox): document network namespace isolation 2026-02-04 15:03:33 -08:00