mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
refactor: rename navigator- crate prefix to openshell- (#277)
This commit is contained in:
@@ -253,7 +253,7 @@ User says: "Allow sandbox egress to private IP space via networking policy"
|
||||
1. Problem is clear — no clarification needed
|
||||
2. Fire `principal-engineer-reviewer` to investigate:
|
||||
- Finds `is_internal_ip()` SSRF check in `proxy.rs` that blocks RFC 1918 addresses
|
||||
- Reads OPA policy evaluation pipeline in `opa.rs` and `crates/navigator-sandbox/data/sandbox-policy.rego`
|
||||
- Reads OPA policy evaluation pipeline in `opa.rs` and `crates/openshell-sandbox/data/sandbox-policy.rego`
|
||||
- Reads proto definitions in `sandbox.proto` for `NetworkEndpoint`
|
||||
- Maps the 4-layer defense model: netns, seccomp, OPA, SSRF check
|
||||
- Reads `architecture/security-policy.md` and `architecture/sandbox.md`
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: debug-navigator-cluster
|
||||
name: debug-openshell-cluster
|
||||
description: Debug why a openshell cluster failed to start or is unhealthy. Use when the user has a failed `openshell gateway start`, cluster health check failure, or wants to diagnose cluster infrastructure issues. Trigger keywords - debug cluster, cluster failing, cluster not starting, deploy failed, cluster troubleshoot, cluster health, cluster diagnose, why won't my cluster start, health check failed, gateway start failed, gateway not starting.
|
||||
---
|
||||
|
||||
@@ -209,7 +209,7 @@ If `registries.yaml` is missing or has wrong values, verify env wiring (`OPENSHE
|
||||
|
||||
### Step 6: Check mTLS / PKI
|
||||
|
||||
TLS certificates are generated by the `navigator-bootstrap` crate (using `rcgen`) and stored as K8s secrets before the Helm release installs. There is no PKI job or cert-manager — certificates are applied directly via `kubectl apply`.
|
||||
TLS certificates are generated by the `openshell-bootstrap` crate (using `rcgen`) and stored as K8s secrets before the Helm release installs. There is no PKI job or cert-manager — certificates are applied directly via `kubectl apply`.
|
||||
|
||||
```bash
|
||||
# Check if the three TLS secrets exist
|
||||
@@ -542,5 +542,5 @@ private_services:
|
||||
|
||||
- Full policy schema: [architecture/security-policy.md](../../../architecture/security-policy.md)
|
||||
- Default policy: baked into the community base image (`ghcr.io/nvidia/openshell-community/sandboxes/base:latest`)
|
||||
- Rego evaluation rules: [sandbox-policy.rego](../../../crates/navigator-sandbox/data/sandbox-policy.rego)
|
||||
- Rego evaluation rules: [sandbox-policy.rego](../../../crates/openshell-sandbox/data/sandbox-policy.rego)
|
||||
- For translation examples from real API docs, see [examples.md](examples.md)
|
||||
|
||||
@@ -25,7 +25,7 @@ This is your primary fallback. Use it freely -- the CLI's help output is authori
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `openshell` is on the PATH (install via `cargo install --path crates/navigator-cli`)
|
||||
- `openshell` is on the PATH (install via `cargo install --path crates/openshell-cli`)
|
||||
- Docker is running (required for gateway operations and BYOC)
|
||||
- For remote clusters: SSH access to the target host
|
||||
|
||||
@@ -563,5 +563,5 @@ $ openshell sandbox upload --help
|
||||
| Skill | When to use |
|
||||
|-------|------------|
|
||||
| `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 |
|
||||
| `debug-openshell-cluster` | Diagnosing cluster startup or health failures |
|
||||
| `tui-development` | Developing features for the OpenShell TUI (`openshell term`) |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: tui-development
|
||||
description: Guide for developing the OpenShell TUI — a ratatui-based terminal UI for the OpenShell platform. Covers architecture, navigation, data fetching, theming, UX conventions, and development workflow. Trigger keywords - term, TUI, terminal UI, ratatui, navigator-tui, tui development, tui feature, tui bug.
|
||||
description: Guide for developing the OpenShell TUI — a ratatui-based terminal UI for the OpenShell platform. Covers architecture, navigation, data fetching, theming, UX conventions, and development workflow. Trigger keywords - term, TUI, terminal UI, ratatui, openshell-tui, tui development, tui feature, tui bug.
|
||||
---
|
||||
|
||||
# OpenShell TUI Development Guide
|
||||
@@ -12,14 +12,14 @@ Comprehensive reference for any agent working on the OpenShell TUI.
|
||||
The OpenShell TUI is a ratatui-based terminal UI for the OpenShell platform. It provides a keyboard-driven interface for managing gateways, sandboxes, and logs — the same operations available via the `openshell` CLI, but with a live, interactive dashboard.
|
||||
|
||||
- **Launched via:** `openshell term` or `mise run term`
|
||||
- **Crate:** `crates/navigator-tui/`
|
||||
- **Crate:** `crates/openshell-tui/`
|
||||
- **Key dependencies:**
|
||||
- `ratatui` (workspace version) — uses `frame.size()` (not `frame.area()`)
|
||||
- `crossterm` (workspace version) — terminal backend and event polling
|
||||
- `tonic` with TLS — gRPC client for the OpenShell gateway
|
||||
- `tokio` — async runtime for event loop, spawned tasks, and mpsc channels
|
||||
- `navigator-core` — proto-generated types (`NavigatorClient`, request/response structs)
|
||||
- `navigator-bootstrap` — cluster discovery (`list_clusters()`)
|
||||
- `openshell-core` — proto-generated types (`OpenShellClient`, request/response structs)
|
||||
- `openshell-bootstrap` — cluster discovery (`list_clusters()`)
|
||||
- **Theme:** Adaptive dark/light via `Theme` struct — NVIDIA-branded green accents. Controlled by `--theme` flag, `OPENSHELL_THEME` env var, or auto-detection.
|
||||
|
||||
## 2. Domain Object Hierarchy
|
||||
@@ -27,12 +27,12 @@ The OpenShell TUI is a ratatui-based terminal UI for the OpenShell platform. It
|
||||
The data model follows a strict hierarchy: **Gateway > Sandboxes > Logs**.
|
||||
|
||||
```
|
||||
Gateway (discovered via navigator_bootstrap::list_gateways())
|
||||
Gateway (discovered via openshell_bootstrap::list_gateways())
|
||||
└── Sandboxes (fetched via gRPC ListSandboxes)
|
||||
└── Logs (fetched via GetSandboxLogs + streamed via WatchSandbox)
|
||||
```
|
||||
|
||||
- **Gateways** are discovered from on-disk config via `navigator_bootstrap::list_gateways()`. Each gateway has a name, endpoint, and local/remote flag.
|
||||
- **Gateways** are discovered from on-disk config via `openshell_bootstrap::list_gateways()`. Each gateway has a name, endpoint, and local/remote flag.
|
||||
- **Sandboxes** belong to the active cluster. Fetched via `ListSandboxes` gRPC call with a periodic tick refresh. Each sandbox has: `id`, `name`, `phase`, `created_at_ms`, and `spec.template.image`.
|
||||
- **Logs** belong to a single sandbox. Initial batch fetched via `GetSandboxLogs` (500 lines), then live-tailed via `WatchSandbox` with `follow_logs: true`.
|
||||
|
||||
@@ -174,7 +174,7 @@ tokio::time::timeout(Duration::from_secs(5), client.health(req)).await
|
||||
|
||||
### Theme System (`theme.rs`)
|
||||
|
||||
Colors and styles are defined in `crates/navigator-tui/src/theme.rs` via the `Theme` struct. The TUI supports dark and light terminal backgrounds.
|
||||
Colors and styles are defined in `crates/openshell-tui/src/theme.rs` via the `Theme` struct. The TUI supports dark and light terminal backgrounds.
|
||||
|
||||
#### Theme selection
|
||||
|
||||
@@ -344,16 +344,16 @@ Same as above.
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `crates/navigator-tui/Cargo.toml` | Crate manifest — dependencies on `navigator-core`, `navigator-bootstrap`, `ratatui`, `crossterm`, `tonic`, `tokio` |
|
||||
| `crates/navigator-tui/src/lib.rs` | Entry point. Event loop, gRPC calls (`refresh_health`, `refresh_sandboxes`, `spawn_log_stream`, `handle_sandbox_delete`), gateway switching, mTLS channel building |
|
||||
| `crates/navigator-tui/src/app.rs` | `App` state struct, `Screen`/`Focus`/`InputMode`/`LogSourceFilter` enums, `LogLine` struct, `GatewayEntry`, all key handling logic |
|
||||
| `crates/navigator-tui/src/event.rs` | `Event` enum (`Key`, `Mouse`, `Tick`, `Resize`, `LogLines`), `EventHandler` with mpsc channels and crossterm polling |
|
||||
| `crates/navigator-tui/src/theme.rs` | `colors` module (NVIDIA_GREEN, EVERGLADE, BG, FG) and `styles` module (all `Style` constants) |
|
||||
| `crates/navigator-tui/src/ui/mod.rs` | Top-level `draw()` dispatcher, `draw_title_bar`, `draw_nav_bar`, `draw_command_bar`, screen routing |
|
||||
| `crates/navigator-tui/src/ui/dashboard.rs` | Dashboard screen — gateway list table (top) + sandbox table (bottom) |
|
||||
| `crates/navigator-tui/src/ui/sandboxes.rs` | Reusable sandbox table widget with columns: Name, Status, Created, Age, Image |
|
||||
| `crates/navigator-tui/src/ui/sandbox_detail.rs` | Sandbox detail view — name, status, image, created, age, delete confirmation dialog |
|
||||
| `crates/navigator-tui/src/ui/sandbox_logs.rs` | Structured log viewer — timestamp, source, level, target, message, key=value fields, scroll position, source filter |
|
||||
| `crates/openshell-tui/Cargo.toml` | Crate manifest — dependencies on `openshell-core`, `openshell-bootstrap`, `ratatui`, `crossterm`, `tonic`, `tokio` |
|
||||
| `crates/openshell-tui/src/lib.rs` | Entry point. Event loop, gRPC calls (`refresh_health`, `refresh_sandboxes`, `spawn_log_stream`, `handle_sandbox_delete`), gateway switching, mTLS channel building |
|
||||
| `crates/openshell-tui/src/app.rs` | `App` state struct, `Screen`/`Focus`/`InputMode`/`LogSourceFilter` enums, `LogLine` struct, `GatewayEntry`, all key handling logic |
|
||||
| `crates/openshell-tui/src/event.rs` | `Event` enum (`Key`, `Mouse`, `Tick`, `Resize`, `LogLines`), `EventHandler` with mpsc channels and crossterm polling |
|
||||
| `crates/openshell-tui/src/theme.rs` | `colors` module (NVIDIA_GREEN, EVERGLADE, BG, FG) and `styles` module (all `Style` constants) |
|
||||
| `crates/openshell-tui/src/ui/mod.rs` | Top-level `draw()` dispatcher, `draw_title_bar`, `draw_nav_bar`, `draw_command_bar`, screen routing |
|
||||
| `crates/openshell-tui/src/ui/dashboard.rs` | Dashboard screen — gateway list table (top) + sandbox table (bottom) |
|
||||
| `crates/openshell-tui/src/ui/sandboxes.rs` | Reusable sandbox table widget with columns: Name, Status, Created, Age, Image |
|
||||
| `crates/openshell-tui/src/ui/sandbox_detail.rs` | Sandbox detail view — name, status, image, created, age, delete confirmation dialog |
|
||||
| `crates/openshell-tui/src/ui/sandbox_logs.rs` | Structured log viewer — timestamp, source, level, target, message, key=value fields, scroll position, source filter |
|
||||
|
||||
### Module dependency flow
|
||||
|
||||
@@ -374,27 +374,27 @@ lib.rs (event loop, gRPC, async tasks)
|
||||
|
||||
### Dependency constraints
|
||||
|
||||
- **`navigator-tui` cannot depend on `navigator-cli`** — this would create a circular dependency. TLS channel building for gateway switching is done directly in `lib.rs` using `tonic::transport` primitives (`Certificate`, `Identity`, `ClientTlsConfig`, `Endpoint`).
|
||||
- **`openshell-tui` cannot depend on `openshell-cli`** — this would create a circular dependency. TLS channel building for gateway switching is done directly in `lib.rs` using `tonic::transport` primitives (`Certificate`, `Identity`, `ClientTlsConfig`, `Endpoint`).
|
||||
- mTLS certs are read from `~/.config/openshell/gateways/<name>/mtls/` (ca.crt, tls.crt, tls.key).
|
||||
|
||||
### Proto generated code
|
||||
|
||||
Proto types come from `navigator-core` which generates them from `OUT_DIR` via `include!`. They are **not** checked into the repo. Import paths look like:
|
||||
Proto types come from `openshell-core` which generates them from `OUT_DIR` via `include!`. They are **not** checked into the repo. Import paths look like:
|
||||
|
||||
```rust
|
||||
use navigator_core::proto::navigator_client::NavigatorClient;
|
||||
use navigator_core::proto::{ListSandboxesRequest, GetSandboxLogsRequest, ...};
|
||||
use openshell_core::proto::openshell_client::OpenShellClient;
|
||||
use openshell_core::proto::{ListSandboxesRequest, GetSandboxLogsRequest, ...};
|
||||
```
|
||||
|
||||
### Proto field gotchas
|
||||
|
||||
- `DeleteSandboxRequest` uses the `name` field (not `id`):
|
||||
```rust
|
||||
let req = navigator_core::proto::DeleteSandboxRequest { name: sandbox_name };
|
||||
let req = openshell_core::proto::DeleteSandboxRequest { name: sandbox_name };
|
||||
```
|
||||
- `WatchSandboxRequest` has extra fields beyond what you might need — always use `..Default::default()`:
|
||||
```rust
|
||||
let req = navigator_core::proto::WatchSandboxRequest {
|
||||
let req = openshell_core::proto::WatchSandboxRequest {
|
||||
id: sandbox_id,
|
||||
follow_status: false,
|
||||
follow_logs: true,
|
||||
@@ -441,7 +441,7 @@ The connect timeout for cluster switching is 10 seconds with HTTP/2 keepalive at
|
||||
|
||||
```bash
|
||||
# Build the crate
|
||||
cargo build -p navigator-tui
|
||||
cargo build -p openshell-tui
|
||||
|
||||
# Run the TUI against the active cluster
|
||||
mise run term
|
||||
@@ -450,10 +450,10 @@ mise run term
|
||||
mise run term:dev
|
||||
|
||||
# Format
|
||||
cargo fmt -p navigator-tui
|
||||
cargo fmt -p openshell-tui
|
||||
|
||||
# Lint
|
||||
cargo clippy -p navigator-tui
|
||||
cargo clippy -p openshell-tui
|
||||
```
|
||||
|
||||
### Pre-commit
|
||||
@@ -480,7 +480,7 @@ kubectl delete pod <pod-name> -n <namespace>
|
||||
|
||||
### Adding a new gRPC call
|
||||
|
||||
1. Check the proto definitions in `navigator-core` for available RPCs and message types.
|
||||
1. Check the proto definitions in `openshell-core` for available RPCs and message types.
|
||||
2. Add the call in `lib.rs` following the existing pattern (timeout wrapper, error handling, state update).
|
||||
3. If the call is triggered by a key press, add a `pending_*` flag to `App` and handle it in the event loop.
|
||||
4. If the call returns streaming data, spawn it as a background task and send results via `Event` variants.
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
# Arch Doc Writer Memory
|
||||
|
||||
## Project Structure
|
||||
- Crates: `navigator-cli`, `navigator-server`, `navigator-sandbox`, `navigator-bootstrap`, `navigator-core`, `navigator-providers`, `navigator-router`, `navigator-policy`
|
||||
- CLI entry: `crates/navigator-cli/src/main.rs` (clap parser + dispatch)
|
||||
- CLI logic: `crates/navigator-cli/src/run.rs` (all command implementations)
|
||||
- Sandbox entry: `crates/navigator-sandbox/src/lib.rs` (`run_sandbox()`)
|
||||
- OPA engine: `crates/navigator-sandbox/src/opa.rs` (single file, not a directory)
|
||||
- Identity cache: `crates/navigator-sandbox/src/identity.rs` (SHA256 TOFU, uses Mutex<HashMap> NOT DashMap)
|
||||
- L7 inspection: `crates/navigator-sandbox/src/l7/` (mod.rs, tls.rs, relay.rs, rest.rs, provider.rs, inference.rs)
|
||||
- Proxy: `crates/navigator-sandbox/src/proxy.rs`
|
||||
- Policy crate: `crates/navigator-policy/src/lib.rs` (YAML<->proto conversion, validation, restrictive default)
|
||||
- Server multiplex: `crates/navigator-server/src/multiplex.rs`
|
||||
- SSH tunnel: `crates/navigator-server/src/ssh_tunnel.rs`
|
||||
- Sandbox SSH server: `crates/navigator-sandbox/src/ssh.rs`
|
||||
- Providers: `crates/navigator-providers/src/providers/` (per-provider modules)
|
||||
- Bootstrap: `crates/navigator-bootstrap/src/lib.rs` (cluster lifecycle)
|
||||
- Proto files: `proto/` directory (navigator.proto, sandbox.proto, datamodel.proto, inference.proto)
|
||||
- Crates: `openshell-cli`, `openshell-server`, `openshell-sandbox`, `openshell-bootstrap`, `openshell-core`, `openshell-providers`, `openshell-router`, `openshell-policy`
|
||||
- CLI entry: `crates/openshell-cli/src/main.rs` (clap parser + dispatch)
|
||||
- CLI logic: `crates/openshell-cli/src/run.rs` (all command implementations)
|
||||
- Sandbox entry: `crates/openshell-sandbox/src/lib.rs` (`run_sandbox()`)
|
||||
- OPA engine: `crates/openshell-sandbox/src/opa.rs` (single file, not a directory)
|
||||
- Identity cache: `crates/openshell-sandbox/src/identity.rs` (SHA256 TOFU, uses Mutex<HashMap> NOT DashMap)
|
||||
- L7 inspection: `crates/openshell-sandbox/src/l7/` (mod.rs, tls.rs, relay.rs, rest.rs, provider.rs, inference.rs)
|
||||
- Proxy: `crates/openshell-sandbox/src/proxy.rs`
|
||||
- Policy crate: `crates/openshell-policy/src/lib.rs` (YAML<->proto conversion, validation, restrictive default)
|
||||
- Server multiplex: `crates/openshell-server/src/multiplex.rs`
|
||||
- SSH tunnel: `crates/openshell-server/src/ssh_tunnel.rs`
|
||||
- Sandbox SSH server: `crates/openshell-sandbox/src/ssh.rs`
|
||||
- Providers: `crates/openshell-providers/src/providers/` (per-provider modules)
|
||||
- Bootstrap: `crates/openshell-bootstrap/src/lib.rs` (cluster lifecycle)
|
||||
- Proto files: `proto/` directory (openshell.proto, sandbox.proto, datamodel.proto, inference.proto)
|
||||
|
||||
## Architecture Docs
|
||||
- Files renamed from numbered prefix format to descriptive names (e.g., `2 - server-architecture.md` -> `gateway-architecture.md`)
|
||||
- Current files: README.md, sandbox-providers.md, cluster-single-node.md, build-containers.md, sandbox-connect.md, sandbox.md, security-policy.md, gateway.md, gateway-security.md, sandbox-custom-containers.md, inference-routing.md
|
||||
- Cross-references use plain filenames: `[text](gateway.md)`
|
||||
- Naming convention: "gateway" in prose for the control plane component; code identifiers like `navigator-server` stay unchanged
|
||||
- Naming convention: "gateway" in prose for the control plane component; code identifiers like `openshell-server` stay unchanged
|
||||
|
||||
## Key Patterns
|
||||
- OPA baked-in rules: `include_str!("../data/sandbox-policy.rego")` in opa.rs
|
||||
- Policy loading: gRPC mode (OPENSHELL_SANDBOX_ID + OPENSHELL_ENDPOINT) or file mode (--policy-rules + --policy-data)
|
||||
- Env vars: sandbox uses OPENSHELL_* prefix (e.g., OPENSHELL_SANDBOX_ID, OPENSHELL_ENDPOINT, OPENSHELL_POLICY_RULES)
|
||||
- CLI flag: `--navigator-endpoint` (NOT `--openshell-endpoint`)
|
||||
- CLI flag: `--openshell-endpoint` (NOT `--openshell-endpoint`)
|
||||
- Provider env injection: both entrypoint process (tokio Command) and SSH shell (std Command)
|
||||
- Cluster bootstrap: `sandbox_create_with_bootstrap()` auto-deploys when no cluster exists (main.rs ~line 632)
|
||||
- CLI cluster resolution: --cluster flag > NAVIGATOR_CLUSTER env > active cluster file
|
||||
- CLI cluster resolution: --cluster flag > OPENSHELL_CLUSTER env > active cluster file
|
||||
|
||||
## Bootstrap Crate Details
|
||||
- `docker.rs`: `ensure_container()` sets ~12 env vars (REGISTRY_*, IMAGE_*, PUSH_IMAGE_REFS, etc.)
|
||||
@@ -39,24 +39,24 @@
|
||||
- `push.rs`: Uses `ctr` (not `k3s ctr`) with k3s containerd socket, `k8s.io` namespace
|
||||
- IMPORTANT: `ClusterHandle::destroy()` does NOT remove metadata; only CLI `cluster_admin_destroy()` in run.rs does
|
||||
- `ensure_image()`: Local-only refs (no `/`) get error with build instructions, not a Docker Hub pull attempt
|
||||
- Dockerfile.cluster: k3s v1.29.8-k3s1 base, manifests in `/opt/navigator/manifests/` (volume mount overwrites `/var/lib/`)
|
||||
- Dockerfile.cluster: k3s v1.29.8-k3s1 base, manifests in `/opt/openshell/manifests/` (volume mount overwrites `/var/lib/`)
|
||||
- Healthcheck: checks k8s readyz, StatefulSet ready, Gateway Programmed, conditionally mTLS secret
|
||||
|
||||
## Server Crate Details
|
||||
- Two gRPC services: Navigator (grpc.rs) and Inference (inference.rs), multiplexed via GrpcRouter by URI path
|
||||
- Two gRPC services: OpenShell (grpc.rs) and Inference (inference.rs), multiplexed via GrpcRouter by URI path
|
||||
- Gateway is control-plane only for inference: SetClusterInference + GetClusterInference + GetInferenceBundle
|
||||
- GetInferenceBundle: resolves managed route from provider record at request time, returns ResolvedRoute list + revision hash + generated_at_ms
|
||||
- SetClusterInference: takes provider_name + model_id, stores only references (endpoint/key/protocols resolved at bundle time)
|
||||
- Persistence: single `objects` table, protobuf payloads, Store enum dispatches SQLite vs Postgres by URL prefix
|
||||
- Persistence CRUD: upsert ON CONFLICT (id) not (object_type, id); list ORDER BY created_at_ms ASC, name ASC (not id!)
|
||||
- --db-url has no code default; Helm values.yaml sets `sqlite:/var/navigator/navigator.db`
|
||||
- --db-url has no code default; Helm values.yaml sets `sqlite:/var/openshell/openshell.db`
|
||||
- Object types: "sandbox", "provider", "ssh_session", "inference_route" -- each implements ObjectType/ObjectId/ObjectName
|
||||
- Config: `navigator_core::Config` in `crates/navigator-core/src/config.rs`, all flags have env var fallbacks
|
||||
- Config: `openshell_core::Config` in `crates/openshell-core/src/config.rs`, all flags have env var fallbacks
|
||||
- SSH handshake: "NSSH1" preface + HMAC-SHA256, used in both exec proxy (grpc.rs) and tunnel gateway (ssh_tunnel.rs)
|
||||
- Phase derivation: transient reasons (ReconcilerError, DependenciesNotReady) -> Provisioning; all others -> Error
|
||||
- Broadcast bus buffer sizes: SandboxWatchBus=128, TracingLogBus=1024, PlatformEventBus=1024
|
||||
- Sandbox CRD: `agents.x-k8s.io/v1alpha1/Sandbox`, labels: `navigator.ai/sandbox-id`, `navigator.ai/managed-by`
|
||||
- Proto files also include: `proto/inference.proto` (navigator.inference.v1)
|
||||
- Sandbox CRD: `agents.x-k8s.io/v1alpha1/Sandbox`, labels: `openshell.ai/sandbox-id`, `openshell.ai/managed-by`
|
||||
- Proto files also include: `proto/inference.proto` (openshell.inference.v1)
|
||||
|
||||
## Container/Build Details
|
||||
- Four runtime images: sandbox (5 stages), gateway (2 stages), cluster (k3s base), pki-job (Alpine)
|
||||
@@ -64,7 +64,7 @@
|
||||
- CI image: Dockerfile.ci (Ubuntu 24.04, pre-installs docker/buildx/aws/kubectl/helm/mise/uv/sccache/socat)
|
||||
- Cross-compilation: `deploy/docker/cross-build.sh` shared by sandbox + gateway Dockerfiles
|
||||
- Sandbox image has coding-agents stage: Claude CLI (native installer), OpenCode, Codex (npm)
|
||||
- Helm chart deploys a StatefulSet (NOT Deployment), PVC 1Gi at /var/navigator
|
||||
- Helm chart deploys a StatefulSet (NOT Deployment), PVC 1Gi at /var/openshell
|
||||
- Cluster image does NOT bundle image tarballs -- components pulled at runtime from distribution registry
|
||||
- PKI job generates CA + server cert + client cert for mTLS (RSA 2048, 10yr, Helm pre-install hook)
|
||||
- Build tasks in `tasks/*.toml`; scripts in `tasks/scripts/`
|
||||
@@ -75,7 +75,7 @@
|
||||
- DNS solution in cluster-entrypoint.sh: iptables DNAT proxy (NOT host-gateway resolv.conf)
|
||||
|
||||
## Sandbox Connect Details
|
||||
- CLI SSH module: `crates/navigator-cli/src/ssh.rs` (sandbox_connect, sandbox_exec, sandbox_rsync, sandbox_ssh_proxy)
|
||||
- CLI SSH module: `crates/openshell-cli/src/ssh.rs` (sandbox_connect, sandbox_exec, sandbox_rsync, sandbox_ssh_proxy)
|
||||
- Re-exported from run.rs: `pub use crate::ssh::{...}` for backward compat
|
||||
- ssh-proxy subcommand: `Commands::SshProxy` in main.rs (~line 139)
|
||||
- Gateway loopback resolution: `resolve_ssh_gateway()` in ssh.rs -- overrides loopback with cluster endpoint host
|
||||
@@ -86,7 +86,7 @@
|
||||
## Policy Reload Details
|
||||
- Poll loop: `run_policy_poll_loop()` in lib.rs, spawned after child process, gRPC mode only
|
||||
- `OpaEngine::reload_from_proto()`: reuses `from_proto()` pipeline, atomically swaps inner engine, LKG on failure
|
||||
- `CachedNavigatorClient` in grpc_client.rs: persistent mTLS channel for poll + status report (mirrors CachedInferenceClient)
|
||||
- `CachedOpenShellClient` in grpc_client.rs: persistent mTLS channel for poll + status report (mirrors CachedInferenceClient)
|
||||
- Dynamic domains: network_policies only (inference removed from policy). Static domains: filesystem, landlock, process (pre_exec, immutable)
|
||||
- Server-side: `UpdateSandboxPolicy` RPC rejects changes to static fields or network mode changes
|
||||
- Server-side validation: `validate_static_fields_unchanged()` + `validate_network_mode_unchanged()` in grpc.rs
|
||||
@@ -96,7 +96,7 @@
|
||||
- `supersede_pending_policies()`: marks older pending revisions as superseded when new version persisted
|
||||
- Status reporting: `ReportPolicyStatus` RPC with `PolicyStatus` enum (PENDING, LOADED, FAILED, SUPERSEDED)
|
||||
- `report_policy_status()` updates `sandbox.current_policy_version` on LOADED, notifies watch bus
|
||||
- Proto files: `ReportPolicyStatusRequest`/`Response` in navigator.proto, `GetSandboxPolicyResponse` in sandbox.proto
|
||||
- Proto files: `ReportPolicyStatusRequest`/`Response` in openshell.proto, `GetSandboxPolicyResponse` in sandbox.proto
|
||||
- `Sandbox.current_policy_version` (uint32) in datamodel.proto -- tracks active loaded version
|
||||
- Persistence: `PolicyRecord` in persistence/mod.rs (id, sandbox_id, version, policy_payload, policy_hash, status, load_error, timestamps)
|
||||
- CLI: `PolicyCommands` enum in main.rs (~line 516): Set, Get, List subcommands
|
||||
@@ -105,10 +105,10 @@
|
||||
- CLI: `sandbox_logs()` in run.rs (~line 3124): --source (all/gateway/sandbox) and --level (error/warn/info/debug/trace) filters
|
||||
- Deterministic hashing: `deterministic_policy_hash()` in grpc.rs (~line 1222): sorts network_policies by key, hashes fields individually, NO inference field
|
||||
- Idempotent UpdateSandboxPolicy: compares hash of new policy to latest stored hash, returns existing version if match
|
||||
- `policy_to_yaml()` in run.rs: converts proto to YAML via navigator_policy::serialize_sandbox_policy (moved to navigator-policy crate)
|
||||
- `policy_to_yaml()` in run.rs: converts proto to YAML via openshell_policy::serialize_sandbox_policy (moved to openshell-policy crate)
|
||||
- `policy_record_to_revision()` in grpc.rs (~line 1334): `include_policy` param controls whether full proto is included
|
||||
- Server-side log filtering: `source_matches()` + `level_matches()` in grpc.rs, applied in both get_sandbox_logs and watch_sandbox
|
||||
- Standalone `proxy_inference()` was removed; inference handled in-sandbox by navigator-router
|
||||
- Standalone `proxy_inference()` was removed; inference handled in-sandbox by openshell-router
|
||||
- Provider types: claude, codex, opencode, generic, openai, anthropic, nvidia, gitlab, github, outlook
|
||||
|
||||
## Policy System Details
|
||||
@@ -125,17 +125,17 @@
|
||||
- Behavioral trigger: `enforcement: enforce` -> deny at proxy; `audit` (default) -> log + forward
|
||||
- Access presets: read-only (GET/HEAD/OPTIONS), read-write (+POST/PUT/PATCH), full (*/*)
|
||||
- Validation: rules+access mutual exclusion, protocol requires rules/access, sql+enforce blocked, empty rules rejected
|
||||
- YAML policy parsing moved to navigator-policy crate (parse_sandbox_policy, serialize_sandbox_policy)
|
||||
- YAML policy parsing moved to openshell-policy crate (parse_sandbox_policy, serialize_sandbox_policy)
|
||||
- PolicyFile uses deny_unknown_fields for strict YAML parsing
|
||||
- restrictive_default_policy() in navigator-policy: no network policies, sandbox user, best_effort landlock
|
||||
- CONTAINER_POLICY_PATH: /etc/navigator/policy.yaml (well-known path for container-shipped policy)
|
||||
- restrictive_default_policy() in openshell-policy: no network policies, sandbox user, best_effort landlock
|
||||
- CONTAINER_POLICY_PATH: /etc/openshell/policy.yaml (well-known path for container-shipped policy)
|
||||
- clear_process_identity(): clears run_as_user/run_as_group for custom images
|
||||
- Policy safety validation: validate_sandbox_policy() checks root identity, path traversal, relative paths, overly broad paths, max 256 paths, max 4096 chars
|
||||
- Identity binding: /proc/net/tcp -> inode -> PID -> /proc/PID/exe + ancestors + cmdline, SHA256 TOFU cache
|
||||
- Network namespace: 10.200.0.1 (host/proxy) <-> 10.200.0.2 (sandbox), port 3128 default
|
||||
- Enforcement order in pre_exec: setns -> drop_privileges -> landlock -> seccomp
|
||||
- TLS cert cache: 256 entries max, overflow clears entire map
|
||||
- CA files: /etc/navigator-tls/navigator-ca.pem (standalone) + ca-bundle.pem (system CAs + sandbox CA)
|
||||
- CA files: /etc/openshell-tls/openshell-ca.pem (standalone) + ca-bundle.pem (system CAs + sandbox CA)
|
||||
- Trust env vars: NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE
|
||||
|
||||
## Proxy SSRF Protection
|
||||
@@ -147,25 +147,25 @@
|
||||
- Non-CP connections use pre-resolved addrs: `TcpStream::connect(addrs.as_slice())`
|
||||
|
||||
## Inference Routing Details
|
||||
- Sandbox-local execution via navigator-router crate
|
||||
- Sandbox-local execution via openshell-router crate
|
||||
- InferenceContext in proxy.rs: Router + patterns + `Arc<RwLock<Vec<ResolvedRoute>>>` route cache
|
||||
- Route sources: `--inference-routes` YAML file (standalone) > cluster bundle via gRPC; empty routes gracefully disable
|
||||
- Cluster bundle refreshed every ROUTE_REFRESH_INTERVAL_SECS (30s)
|
||||
- Patterns: POST /v1/chat/completions, /v1/completions, /v1/responses, /v1/messages; GET /v1/models, /v1/models/*
|
||||
- inference.local CONNECT intercepted BEFORE OPA evaluation in proxy
|
||||
- InferenceProviderProfile in navigator-core/src/inference.rs: centralized provider metadata
|
||||
- InferenceProviderProfile in openshell-core/src/inference.rs: centralized provider metadata
|
||||
- proxy.rs: ONLY CONNECT to inference.local is handled; non-CONNECT requests get 403 for ALL hosts
|
||||
- Buffer: INITIAL_INFERENCE_BUF=64KiB, MAX_INFERENCE_BUF=10MiB; grows by doubling
|
||||
- Dev sandbox: `mise run sandbox -e VAR_NAME` forwards host env vars; NVIDIA_API_KEY always passed
|
||||
|
||||
## Log Streaming Details
|
||||
- LogPushLayer: `crates/navigator-sandbox/src/log_push.rs` -- tracing layer + spawn_log_push_task()
|
||||
- LogPushLayer: `crates/openshell-sandbox/src/log_push.rs` -- tracing layer + spawn_log_push_task()
|
||||
- Initialized in main.rs before run_sandbox(), gRPC mode only
|
||||
- mpsc channel: 1024 lines (bounded), try_send (best-effort, never blocks)
|
||||
- Background task: batches up to 50 lines, flushes every 500ms via PushSandboxLogs client-streaming RPC
|
||||
- Secondary channel to gRPC call: mpsc::channel::<PushSandboxLogsRequest>(32) wrapped in ReceiverStream
|
||||
- CachedNavigatorClient.raw_client() returns clone of inner NavigatorClient for direct RPC calls
|
||||
- NAVIGATOR_LOG_PUSH_LEVEL env var (default INFO), parsed in LogPushLayer::new()
|
||||
- CachedOpenShellClient.raw_client() returns clone of inner OpenShellClient for direct RPC calls
|
||||
- OPENSHELL_LOG_PUSH_LEVEL env var (default INFO), parsed in LogPushLayer::new()
|
||||
- Server handler: push_sandbox_logs in grpc.rs, caps 100 lines/batch, forces source="sandbox" + sandbox_id
|
||||
- TracingLogBus.publish_external(): injects into same broadcast + tail buffer as SandboxLogLayer
|
||||
- Tail buffer: DEFAULT_TAIL = 2000 lines per sandbox (was 200, increased with log push)
|
||||
@@ -179,6 +179,6 @@
|
||||
- Proto: WatchSandboxRequest (log_sources, log_min_level fields)
|
||||
|
||||
## Naming Conventions
|
||||
- The project name "Navigator" appears in code but docs should use generic terms per user preference
|
||||
- CLI binary: `navigator` (aliased as `nav` in dev via mise)
|
||||
- The project name "OpenShell" appears in code but docs should use generic terms per user preference
|
||||
- CLI binary: `openshell` (aliased as `nav` in dev via mise)
|
||||
- Provider types: claude, codex, opencode, generic, openai, anthropic, nvidia, gitlab, github, outlook (see ProviderRegistry::new())
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# Principal Engineer Reviewer Memory
|
||||
|
||||
## Project Structure
|
||||
- Proto definitions: `proto/navigator.proto`, `proto/sandbox.proto`, `proto/sandbox_policy.proto`
|
||||
- Server gRPC handlers: `crates/navigator-server/src/grpc.rs`
|
||||
- TracingLogBus (log broadcast): `crates/navigator-server/src/tracing_bus.rs`
|
||||
- Sandbox watch bus: `crates/navigator-server/src/sandbox_watch.rs`
|
||||
- Server state: `crates/navigator-server/src/lib.rs` (ServerState struct)
|
||||
- Sandbox main: `crates/navigator-sandbox/src/main.rs`
|
||||
- Sandbox library: `crates/navigator-sandbox/src/lib.rs`
|
||||
- Sandbox gRPC client: `crates/navigator-sandbox/src/grpc_client.rs`
|
||||
- CLI commands: `crates/navigator-cli/src/main.rs` (clap defs), `crates/navigator-cli/src/run.rs` (impl)
|
||||
- Python SDK: `python/navigator/`
|
||||
- Proto definitions: `proto/openshell.proto`, `proto/sandbox.proto`, `proto/sandbox_policy.proto`
|
||||
- Server gRPC handlers: `crates/openshell-server/src/grpc.rs`
|
||||
- TracingLogBus (log broadcast): `crates/openshell-server/src/tracing_bus.rs`
|
||||
- Sandbox watch bus: `crates/openshell-server/src/sandbox_watch.rs`
|
||||
- Server state: `crates/openshell-server/src/lib.rs` (ServerState struct)
|
||||
- Sandbox main: `crates/openshell-sandbox/src/main.rs`
|
||||
- Sandbox library: `crates/openshell-sandbox/src/lib.rs`
|
||||
- Sandbox gRPC client: `crates/openshell-sandbox/src/grpc_client.rs`
|
||||
- CLI commands: `crates/openshell-cli/src/main.rs` (clap defs), `crates/openshell-cli/src/run.rs` (impl)
|
||||
- Python SDK: `python/openshell/`
|
||||
- Plans go in: `architecture/plans/`
|
||||
|
||||
## Key Patterns
|
||||
- TracingLogBus: per-sandbox broadcast::channel(1024) + VecDeque tail buffer (200 lines)
|
||||
- CachedNavigatorClient: reusable mTLS gRPC channel for sandbox->server calls
|
||||
- CachedOpenShellClient: reusable mTLS gRPC channel for sandbox->server calls
|
||||
- SandboxLogLayer: tracing Layer that captures events with sandbox_id field
|
||||
- Sandbox logging: stdout (ANSI, configurable level) + /var/log/navigator.log (info, no ANSI, non-blocking)
|
||||
- Sandbox logging: stdout (ANSI, configurable level) + /var/log/openshell.log (info, no ANSI, non-blocking)
|
||||
- WatchSandbox: server-streaming with select! loop over status_rx, log_rx, platform_rx
|
||||
- Proto codegen: `mise run proto`
|
||||
- Build: `mise run sandbox` for sandbox infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: arch-doc-writer
|
||||
description: "Use this agent when documentation in the `architecture/` directory needs to be updated or created for a specific file after implementing a feature, fix, refactor, or behavior change. Launch one instance of this agent per file that needs updating. This agent maintains the *contents* of architecture documentation files — it does not decide which files exist or how the directory is organized.\\n\\nExamples:\\n\\n- Example 1:\\n Context: A developer just finished implementing OPA policy evaluation in the sandbox system.\\n user: \"I just finished implementing the OPA engine in crates/navigator-sandbox/src/opa.rs. Update architecture/sandbox.md to reflect the new policy evaluation flow.\"\\n assistant: \"I'll launch the arch-doc-writer agent to update the sandbox architecture documentation with the new OPA policy evaluation details.\"\\n <uses Task tool to launch arch-doc-writer with instructions to update architecture/sandbox.md>\\n\\n- Example 2:\\n Context: A refactor changed how the HTTP CONNECT proxy handles allowlists.\\n user: \"The proxy allowlist logic was refactored. Please update architecture/proxy.md.\"\\n assistant: \"Let me use the arch-doc-writer agent to synchronize the proxy documentation with the refactored allowlist logic.\"\\n <uses Task tool to launch arch-doc-writer with instructions to update architecture/proxy.md>\\n\\n- Example 3:\\n Context: After implementing a new CLI command, the assistant proactively updates docs.\\n user: \"Add a --rego-policy flag to the CLI.\"\\n assistant: \"Here is the implementation of the --rego-policy flag.\"\\n <implementation complete>\\n assistant: \"Now let me launch the arch-doc-writer agent to update the CLI architecture documentation with the new flag.\"\\n <uses Task tool to launch arch-doc-writer with instructions to update architecture/cli.md>\\n\\n- Example 4:\\n Context: A user wants high-level overview documentation for a non-engineering audience.\\n user: \"Update architecture/overview.md with a non-engineer-friendly explanation of the sandbox system.\"\\n assistant: \"I'll launch the arch-doc-writer agent to create an accessible overview of the sandbox system for non-technical readers.\"\\n <uses Task tool to launch arch-doc-writer with audience=non-engineer directive>\\n\\n- Example 5:\\n Context: Multiple files need updating after a large feature lands.\\n user: \"I just landed the network namespace isolation feature. Update architecture/sandbox.md and architecture/networking.md.\"\\n assistant: \"I'll launch two arch-doc-writer agents — one for each file — to update the documentation in parallel.\"\\n <uses Task tool to launch arch-doc-writer for architecture/sandbox.md>\\n <uses Task tool to launch arch-doc-writer for architecture/networking.md>"
|
||||
description: "Use this agent when documentation in the `architecture/` directory needs to be updated or created for a specific file after implementing a feature, fix, refactor, or behavior change. Launch one instance of this agent per file that needs updating. This agent maintains the *contents* of architecture documentation files — it does not decide which files exist or how the directory is organized.\\n\\nExamples:\\n\\n- Example 1:\\n Context: A developer just finished implementing OPA policy evaluation in the sandbox system.\\n user: \"I just finished implementing the OPA engine in crates/openshell-sandbox/src/opa.rs. Update architecture/sandbox.md to reflect the new policy evaluation flow.\"\\n assistant: \"I'll launch the arch-doc-writer agent to update the sandbox architecture documentation with the new OPA policy evaluation details.\"\\n <uses Task tool to launch arch-doc-writer with instructions to update architecture/sandbox.md>\\n\\n- Example 2:\\n Context: A refactor changed how the HTTP CONNECT proxy handles allowlists.\\n user: \"The proxy allowlist logic was refactored. Please update architecture/proxy.md.\"\\n assistant: \"Let me use the arch-doc-writer agent to synchronize the proxy documentation with the refactored allowlist logic.\"\\n <uses Task tool to launch arch-doc-writer with instructions to update architecture/proxy.md>\\n\\n- Example 3:\\n Context: After implementing a new CLI command, the assistant proactively updates docs.\\n user: \"Add a --rego-policy flag to the CLI.\"\\n assistant: \"Here is the implementation of the --rego-policy flag.\"\\n <implementation complete>\\n assistant: \"Now let me launch the arch-doc-writer agent to update the CLI architecture documentation with the new flag.\"\\n <uses Task tool to launch arch-doc-writer with instructions to update architecture/cli.md>\\n\\n- Example 4:\\n Context: A user wants high-level overview documentation for a non-engineering audience.\\n user: \"Update architecture/overview.md with a non-engineer-friendly explanation of the sandbox system.\"\\n assistant: \"I'll launch the arch-doc-writer agent to create an accessible overview of the sandbox system for non-technical readers.\"\\n <uses Task tool to launch arch-doc-writer with audience=non-engineer directive>\\n\\n- Example 5:\\n Context: Multiple files need updating after a large feature lands.\\n user: \"I just landed the network namespace isolation feature. Update architecture/sandbox.md and architecture/networking.md.\"\\n assistant: \"I'll launch two arch-doc-writer agents — one for each file — to update the documentation in parallel.\"\\n <uses Task tool to launch arch-doc-writer for architecture/sandbox.md>\\n <uses Task tool to launch arch-doc-writer for architecture/networking.md>"
|
||||
model: opus
|
||||
color: yellow
|
||||
memory: project
|
||||
@@ -14,7 +14,7 @@ You maintain the contents of documentation files in the `architecture/` director
|
||||
|
||||
## Project Context
|
||||
|
||||
This is the Navigator project — a sandbox/isolation system built in Rust.
|
||||
This is the OpenShell project — a sandbox/isolation system built in Rust.
|
||||
|
||||
The docs in `architecture/` are structured as subsystem[-component].md. Key sub-systems are:
|
||||
|
||||
@@ -109,16 +109,16 @@ For practical examples, follow this pattern:
|
||||
Example format:
|
||||
```bash
|
||||
# User runs:
|
||||
navigator sandbox run --policy sandbox.yaml -- /bin/ls
|
||||
openshell sandbox run --policy sandbox.yaml -- /bin/ls
|
||||
```
|
||||
**Trace:**
|
||||
1. `crates/navigator-cli/src/main.rs` → `SandboxRunCmd::execute()`
|
||||
2. Policy loaded from YAML via `crates/navigator-sandbox/src/policy.rs` → `Policy::from_yaml()`
|
||||
1. `crates/openshell-cli/src/main.rs` → `SandboxRunCmd::execute()`
|
||||
2. Policy loaded from YAML via `crates/openshell-sandbox/src/policy.rs` → `Policy::from_yaml()`
|
||||
3. ... (continue through the actual code path)
|
||||
|
||||
### Cross-References
|
||||
- Link to other architecture docs when referencing related subsystems: `[Proxy Architecture](proxy.md)`
|
||||
- Reference source files with relative paths from repo root: `crates/navigator-sandbox/src/lib.rs`
|
||||
- Reference source files with relative paths from repo root: `crates/openshell-sandbox/src/lib.rs`
|
||||
- When referencing plans, link to `architecture/plans/`
|
||||
|
||||
### What NOT to Include
|
||||
@@ -129,7 +129,7 @@ navigator sandbox run --policy sandbox.yaml -- /bin/ls
|
||||
|
||||
## System Architecture Diagram
|
||||
|
||||
The file `architecture/system-architecture.md` contains a top-level Mermaid diagram of the entire Navigator system — all deployable components, external systems, communication protocols, and security boundaries. It is the single source of truth for the system's visual architecture.
|
||||
The file `architecture/system-architecture.md` contains a top-level Mermaid diagram of the entire OpenShell system — all deployable components, external systems, communication protocols, and security boundaries. It is the single source of truth for the system's visual architecture.
|
||||
|
||||
**After completing any documentation update**, check whether your changes affect the system-level architecture diagram. You MUST update `architecture/system-architecture.md` if any of the following are true:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ memory: project
|
||||
---
|
||||
|
||||
You are a principal engineer reviewing code, plans, and architecture for the
|
||||
Navigator project. Your reviews balance three priorities equally:
|
||||
OpenShell project. Your reviews balance three priorities equally:
|
||||
|
||||
1. **Pragmatism** — Does the solution match the complexity of the problem? Is
|
||||
the simplest viable approach being used? Flag over-engineering, unnecessary
|
||||
@@ -30,7 +30,7 @@ Navigator project. Your reviews balance three priorities equally:
|
||||
|
||||
## Project context
|
||||
|
||||
Navigator is a sandbox orchestration system written primarily in Rust with a user-facing
|
||||
OpenShell is a sandbox orchestration system written primarily in Rust with a user-facing
|
||||
Python CLI and SDK for installation and management.
|
||||
|
||||
For more detailed context on the project, you can find architectural documents
|
||||
|
||||
@@ -13,7 +13,7 @@ inputs:
|
||||
default: tcp://buildkit-arm64.buildkit:1234
|
||||
name:
|
||||
description: Builder instance name
|
||||
default: navigator
|
||||
default: openshell
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
@@ -69,6 +69,6 @@ jobs:
|
||||
|
||||
- name: Build ${{ inputs.component }} image
|
||||
env:
|
||||
DOCKER_BUILDER: navigator
|
||||
DOCKER_BUILDER: openshell
|
||||
OPENSHELL_CARGO_VERSION: ${{ steps.version.outputs.cargo_version }}
|
||||
run: mise run --no-prepare docker:build:${{ inputs.component }}
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
env:
|
||||
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }}
|
||||
NAV_PYPI_S3_BUCKET: navigator-pypi-artifacts
|
||||
NAV_PYPI_S3_BUCKET: navigator-pypi-artifacts # TODO: rename bucket to openshell-pypi-artifacts
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-west-2
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAV_PYPI_S3_BUCKET: navigator-pypi-artifacts
|
||||
NAV_PYPI_S3_BUCKET: navigator-pypi-artifacts # TODO: rename bucket to openshell-pypi-artifacts
|
||||
NAV_PYPI_REPOSITORY_URL: https://urm.nvidia.com/artifactory/api/pypi/nv-shared-pypi-local
|
||||
NAV_PYPI_USERNAME: ${{ secrets.NAV_PYPI_USERNAME }}
|
||||
NAV_PYPI_PASSWORD: ${{ secrets.NAV_PYPI_PASSWORD }}
|
||||
@@ -250,10 +250,10 @@ jobs:
|
||||
- name: Scope workspace to CLI crates
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Remove workspace members that are not needed for navigator-cli.
|
||||
# Remove workspace members that are not needed for openshell-cli.
|
||||
# This avoids Cargo feature-unification pulling in aws-lc-sys (via
|
||||
# russh in navigator-sandbox / navigator-server).
|
||||
sed -i 's|members = \["crates/\*"\]|members = ["crates/navigator-cli", "crates/navigator-core", "crates/navigator-bootstrap", "crates/navigator-policy", "crates/navigator-providers", "crates/navigator-tui"]|' Cargo.toml
|
||||
# russh in openshell-sandbox / openshell-server).
|
||||
sed -i 's|members = \["crates/\*"\]|members = ["crates/openshell-cli", "crates/openshell-core", "crates/openshell-bootstrap", "crates/openshell-policy", "crates/openshell-providers", "crates/openshell-tui"]|' Cargo.toml
|
||||
|
||||
- name: Patch workspace version
|
||||
if: steps.version.outputs.cargo_version != ''
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${{ steps.version.outputs.cargo_version }}"'"/}' Cargo.toml
|
||||
|
||||
- name: Build ${{ matrix.target }}
|
||||
run: mise x -- cargo build --release --target ${{ matrix.target }} -p navigator-cli
|
||||
run: mise x -- cargo build --release --target ${{ matrix.target }} -p openshell-cli
|
||||
|
||||
- name: sccache stats
|
||||
if: always()
|
||||
|
||||
@@ -33,7 +33,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions on how to perform common
|
||||
|
||||
## Cluster Infrastructure Changes
|
||||
|
||||
- If you change cluster bootstrap infrastructure (e.g., `navigator-bootstrap` crate, `Dockerfile.cluster`, `cluster-entrypoint.sh`, `cluster-healthcheck.sh`, deploy logic in `navigator-cli`), update the `debug-navigator-cluster` skill in `.agent/skills/debug-navigator-cluster/SKILL.md` to reflect those changes.
|
||||
- If you change cluster bootstrap infrastructure (e.g., `openshell-bootstrap` crate, `Dockerfile.cluster`, `cluster-entrypoint.sh`, `cluster-healthcheck.sh`, deploy logic in `openshell-cli`), update the `debug-openshell-cluster` skill in `.agent/skills/debug-openshell-cluster/SKILL.md` to reflect those changes.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
+3
-3
@@ -41,7 +41,7 @@ mise run sandbox
|
||||
|
||||
Inside this repository, `openshell` is a local shortcut script at `scripts/bin/openshell`. The script will
|
||||
|
||||
1. Build `navigator-cli` if needed.
|
||||
1. Build `openshell-cli` if needed.
|
||||
2. Run the local debug CLI binary under `target/debug/openshell`.
|
||||
|
||||
Because `mise` adds `scripts/bin` to `PATH` for this project, you can run `openshell` directly from the repo.
|
||||
@@ -64,9 +64,9 @@ These work for both local and remote gateways (SSH is handled automatically). Ex
|
||||
|
||||
```bash
|
||||
kubectl get pods -A
|
||||
kubectl logs -n navigator statefulset/navigator
|
||||
kubectl logs -n openshell statefulset/openshell
|
||||
k9s
|
||||
k9s -n navigator
|
||||
k9s -n openshell
|
||||
```
|
||||
|
||||
## Main Tasks
|
||||
|
||||
Generated
+410
-410
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -41,7 +41,7 @@ Created sandbox:
|
||||
|
||||
Id: cddeeb6d-a4d3-4158-a4d1-bd931f743700
|
||||
Name: sandbox-cddeeb6d
|
||||
Namespace: navigator
|
||||
Namespace: openshell
|
||||
```
|
||||
|
||||
Bad:
|
||||
@@ -50,5 +50,5 @@ Bad:
|
||||
Created sandbox:
|
||||
Id: cddeeb6d-a4d3-4158-a4d1-bd931f743700
|
||||
Name: sandbox-cddeeb6d
|
||||
Namespace: navigator
|
||||
Namespace: openshell
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ When the agent (or any tool running inside the sandbox) tries to connect to a re
|
||||
3. **Evaluates the request against policy** using the OPA engine. The policy can allow or deny connections based on the destination hostname, port, and the identity of the requesting program.
|
||||
4. **Rejects connections to internal IP addresses** as a defense against SSRF (Server-Side Request Forgery). Even if the policy allows a hostname, the proxy resolves DNS before connecting and blocks any result that points to a private network address (e.g., cloud metadata endpoints, localhost, or RFC 1918 ranges). This prevents an attacker from redirecting an allowed hostname to internal infrastructure.
|
||||
5. **Performs protocol-aware inspection (L7)** for configured endpoints. The proxy can terminate TLS, inspect the underlying HTTP traffic, and enforce rules on individual API requests -- not just connection-level allow/deny. This operates in either audit mode (log violations but allow traffic) or enforce mode (block violations).
|
||||
6. **Intercepts inference API calls** to `inference.local`. When the agent sends an HTTPS CONNECT request to `inference.local`, the proxy bypasses OPA evaluation entirely and handles the connection through a dedicated inference interception path. It TLS-terminates the connection, parses the HTTP request, detects known inference API patterns (OpenAI, Anthropic, model discovery), and routes matching requests locally through the sandbox's embedded inference router (`navigator-router`). Non-inference requests to `inference.local` are denied with 403.
|
||||
6. **Intercepts inference API calls** to `inference.local`. When the agent sends an HTTPS CONNECT request to `inference.local`, the proxy bypasses OPA evaluation entirely and handles the connection through a dedicated inference interception path. It TLS-terminates the connection, parses the HTTP request, detects known inference API patterns (OpenAI, Anthropic, model discovery), and routes matching requests locally through the sandbox's embedded inference router (`openshell-router`). Non-inference requests to `inference.local` are denied with 403.
|
||||
|
||||
The proxy generates an ephemeral certificate authority at startup and injects it into the sandbox's trust store. This allows it to transparently inspect HTTPS traffic when L7 inspection is configured for an endpoint, and to serve TLS for `inference.local` interception.
|
||||
|
||||
@@ -166,7 +166,7 @@ For more detail, see [Providers](sandbox-providers.md).
|
||||
|
||||
### Inference Routing
|
||||
|
||||
The inference routing system transparently intercepts AI inference API calls from sandboxed agents and routes them to configured backends. Routing happens locally within the sandbox -- the proxy intercepts connections to `inference.local`, and the embedded `navigator-router` forwards requests directly to the backend without traversing the gateway at request time.
|
||||
The inference routing system transparently intercepts AI inference API calls from sandboxed agents and routes them to configured backends. Routing happens locally within the sandbox -- the proxy intercepts connections to `inference.local`, and the embedded `openshell-router` forwards requests directly to the backend without traversing the gateway at request time.
|
||||
|
||||
**How it works end-to-end:**
|
||||
|
||||
@@ -174,7 +174,7 @@ The inference routing system transparently intercepts AI inference API calls fro
|
||||
2. When a sandbox starts, the supervisor fetches an inference bundle from the gateway via the `GetInferenceBundle` RPC. The gateway resolves the stored provider reference into a complete route: endpoint URL, API key, supported protocols, provider type, and auth metadata. The sandbox refreshes this bundle eagerly in the background every 5 seconds by default (override with `OPENSHELL_ROUTE_REFRESH_INTERVAL_SECS`).
|
||||
3. The agent sends requests to `https://inference.local` using standard OpenAI or Anthropic SDK calls.
|
||||
4. The sandbox proxy intercepts the HTTPS CONNECT to `inference.local` (bypassing OPA policy evaluation), TLS-terminates the connection using the sandbox's ephemeral CA, and parses the HTTP request.
|
||||
5. Known inference API patterns are detected (e.g., `POST /v1/chat/completions` for OpenAI, `POST /v1/messages` for Anthropic, `GET /v1/models` for model discovery). Matching requests are forwarded to the first compatible route by the `navigator-router`, which rewrites the auth header, injects provider-specific default headers (e.g., `anthropic-version` for Anthropic), and overrides the model field in the request body.
|
||||
5. Known inference API patterns are detected (e.g., `POST /v1/chat/completions` for OpenAI, `POST /v1/messages` for Anthropic, `GET /v1/models` for model discovery). Matching requests are forwarded to the first compatible route by the `openshell-router`, which rewrites the auth header, injects provider-specific default headers (e.g., `anthropic-version` for Anthropic), and overrides the model field in the request body.
|
||||
6. Non-inference requests to `inference.local` are denied with 403.
|
||||
|
||||
**Key design properties:**
|
||||
@@ -182,7 +182,7 @@ The inference routing system transparently intercepts AI inference API calls fro
|
||||
- Agents need zero code changes -- standard OpenAI/Anthropic SDK calls work transparently when pointed at `inference.local`.
|
||||
- The sandbox never sees the real API key for the backend -- credential isolation is maintained through the gateway's bundle resolution.
|
||||
- Routing is explicit via `inference.local`; OPA network policy is not involved in inference routing.
|
||||
- Provider-specific behavior (auth header style, default headers, supported protocols) is centralized in `InferenceProviderProfile` definitions in `navigator-core`. Supported inference provider types are openai, anthropic, and nvidia.
|
||||
- Provider-specific behavior (auth header style, default headers, supported protocols) is centralized in `InferenceProviderProfile` definitions in `openshell-core`. Supported inference provider types are openai, anthropic, and nvidia.
|
||||
- Cluster inference is managed via CLI (`openshell cluster inference set/get`).
|
||||
|
||||
**Inference routes** are stored on the gateway as protobuf objects (`InferenceRoute` in `proto/inference.proto`). Cluster inference uses a managed singleton route entry keyed by `inference.local` and configured from provider + model settings. Endpoint, credentials, and protocols are resolved from the referenced provider record at bundle fetch time, so rotating a provider's API key takes effect on the next bundle refresh without reconfiguring the route.
|
||||
@@ -191,11 +191,11 @@ The inference routing system transparently intercepts AI inference API calls fro
|
||||
|
||||
| Component | Location | Role |
|
||||
|---|---|---|
|
||||
| Proxy inference interception | `crates/navigator-sandbox/src/proxy.rs` | Intercepts `inference.local` CONNECT requests, TLS-terminates, dispatches to router |
|
||||
| Inference pattern detection | `crates/navigator-sandbox/src/l7/inference.rs` | Matches HTTP method + path against known inference API patterns |
|
||||
| Local inference router | `crates/navigator-router/src/lib.rs` | Selects a compatible route by protocol and proxies to the backend |
|
||||
| Provider profiles | `crates/navigator-core/src/inference.rs` | Centralized auth, headers, protocols, and endpoint defaults per provider type |
|
||||
| Gateway inference service | `crates/navigator-server/src/inference.rs` | Stores cluster inference config, resolves bundles with credentials from provider records |
|
||||
| Proxy inference interception | `crates/openshell-sandbox/src/proxy.rs` | Intercepts `inference.local` CONNECT requests, TLS-terminates, dispatches to router |
|
||||
| Inference pattern detection | `crates/openshell-sandbox/src/l7/inference.rs` | Matches HTTP method + path against known inference API patterns |
|
||||
| Local inference router | `crates/openshell-router/src/lib.rs` | Selects a compatible route by protocol and proxies to the backend |
|
||||
| Provider profiles | `crates/openshell-core/src/inference.rs` | Centralized auth, headers, protocols, and endpoint defaults per provider type |
|
||||
| Gateway inference service | `crates/openshell-server/src/inference.rs` | Stores cluster inference config, resolves bundles with credentials from provider records |
|
||||
| Proto definitions | `proto/inference.proto` | `ClusterInferenceConfig`, `ResolvedRoute`, bundle RPCs |
|
||||
|
||||
|
||||
|
||||
@@ -9,17 +9,17 @@ The gateway runs the control plane API server. It is deployed as a StatefulSet i
|
||||
- **Dockerfile**: `deploy/docker/Dockerfile.gateway`
|
||||
- **Registry**: `ghcr.io/nvidia/openshell/gateway:latest`
|
||||
- **Pulled when**: Cluster startup (the Helm chart triggers the pull)
|
||||
- **Entrypoint**: `navigator-server --port 8080` (gRPC + HTTP, mTLS)
|
||||
- **Entrypoint**: `openshell-server --port 8080` (gRPC + HTTP, mTLS)
|
||||
|
||||
## Cluster (`openshell/cluster`)
|
||||
|
||||
The cluster image is a single-container Kubernetes distribution that bundles the Helm charts, Kubernetes manifests, and the `navigator-sandbox` supervisor binary needed to bootstrap the control plane.
|
||||
The cluster image is a single-container Kubernetes distribution that bundles the Helm charts, Kubernetes manifests, and the `openshell-sandbox` supervisor binary needed to bootstrap the control plane.
|
||||
|
||||
- **Dockerfile**: `deploy/docker/Dockerfile.cluster`
|
||||
- **Registry**: `ghcr.io/nvidia/openshell/cluster:latest`
|
||||
- **Pulled when**: `openshell gateway start`
|
||||
|
||||
The supervisor binary (`navigator-sandbox`) is cross-compiled in a build stage and placed at `/opt/openshell/bin/navigator-sandbox`. It is exposed to sandbox pods at runtime via a read-only `hostPath` volume mount — it is not baked into sandbox images.
|
||||
The supervisor binary (`openshell-sandbox`) is cross-compiled in a build stage and placed at `/opt/openshell/bin/openshell-sandbox`. It is exposed to sandbox pods at runtime via a read-only `hostPath` volume mount — it is not baked into sandbox images.
|
||||
|
||||
## Sandbox Images
|
||||
|
||||
@@ -42,8 +42,8 @@ The incremental deploy (`cluster-deploy-fast.sh`) fingerprints local Git changes
|
||||
| Changed files | Rebuild triggered |
|
||||
|---|---|
|
||||
| Cargo manifests, proto definitions, cross-build script | Gateway + supervisor |
|
||||
| `crates/navigator-server/*`, `Dockerfile.gateway` | Gateway |
|
||||
| `crates/navigator-sandbox/*`, `crates/navigator-policy/*` | Supervisor |
|
||||
| `crates/openshell-server/*`, `Dockerfile.gateway` | Gateway |
|
||||
| `crates/openshell-sandbox/*`, `crates/openshell-policy/*` | Supervisor |
|
||||
| `deploy/helm/openshell/*` | Helm upgrade |
|
||||
|
||||
When no local changes are detected, the command is a no-op.
|
||||
|
||||
@@ -8,7 +8,7 @@ This document describes how the CLI resolves a gateway and communicates with it
|
||||
|
||||
### Gateway resolution
|
||||
|
||||
When any CLI command needs to talk to the gateway, it resolves the target through a priority chain (`crates/navigator-cli/src/main.rs` -- `resolve_gateway()`):
|
||||
When any CLI command needs to talk to the gateway, it resolves the target through a priority chain (`crates/openshell-cli/src/main.rs` -- `resolve_gateway()`):
|
||||
|
||||
1. `--gateway-endpoint <URL>` flag (direct URL, reusing stored metadata when the gateway is known).
|
||||
2. `--cluster <NAME>` / `-g <NAME>` flag.
|
||||
@@ -45,7 +45,7 @@ graph TD
|
||||
|
||||
### mTLS connection (default)
|
||||
|
||||
**File**: `crates/navigator-cli/src/tls.rs` -- `build_channel()`
|
||||
**File**: `crates/openshell-cli/src/tls.rs` -- `build_channel()`
|
||||
|
||||
The default mode for self-deployed gateways. The CLI loads three PEM files from `~/.config/openshell/clusters/<name>/mtls/`:
|
||||
|
||||
@@ -67,12 +67,12 @@ sequenceDiagram
|
||||
CLI->>GW: TLS handshake (present client cert)
|
||||
GW->>GW: Verify client cert against CA
|
||||
GW-->>CLI: TLS established (HTTP/2 via ALPN)
|
||||
CLI->>GW: gRPC requests (Navigator / Inference service)
|
||||
CLI->>GW: gRPC requests (OpenShell / Inference service)
|
||||
```
|
||||
|
||||
### Edge-authenticated connection
|
||||
|
||||
**Files**: `crates/navigator-cli/src/edge_tunnel.rs`, `crates/navigator-cli/src/auth.rs`
|
||||
**Files**: `crates/openshell-cli/src/edge_tunnel.rs`, `crates/openshell-cli/src/auth.rs`
|
||||
|
||||
For gateways behind an edge proxy (e.g., Cloudflare Access), the CLI routes traffic through a local WebSocket tunnel proxy:
|
||||
|
||||
@@ -108,12 +108,12 @@ openshell/
|
||||
|
||||
For gateways that are already deployed behind an edge proxy (e.g., Cloudflare Access), deployment is not needed -- only registration.
|
||||
|
||||
**File**: `crates/navigator-cli/src/run.rs` -- `gateway_add()`
|
||||
**File**: `crates/openshell-cli/src/run.rs` -- `gateway_add()`
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant CLI as navigator-cli
|
||||
participant CLI as openshell-cli
|
||||
participant Browser as Browser
|
||||
participant Edge as Edge Proxy
|
||||
participant GW as Gateway
|
||||
|
||||
@@ -13,17 +13,17 @@ This document covers the certificate hierarchy, the bootstrap process, how gatew
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph PKI["PKI (generated at bootstrap)"]
|
||||
CA["navigator-ca<br/>(self-signed root)"]
|
||||
SERVER_CERT["navigator-server cert<br/>(signed by CA)"]
|
||||
CLIENT_CERT["navigator-client cert<br/>(signed by CA, shared)"]
|
||||
CA["openshell-ca<br/>(self-signed root)"]
|
||||
SERVER_CERT["openshell-server cert<br/>(signed by CA)"]
|
||||
CLIENT_CERT["openshell-client cert<br/>(signed by CA, shared)"]
|
||||
CA --> SERVER_CERT
|
||||
CA --> CLIENT_CERT
|
||||
end
|
||||
|
||||
subgraph CLUSTER["Kubernetes Cluster"]
|
||||
S1["navigator-server-tls<br/>Secret (server cert+key)"]
|
||||
S2["navigator-server-client-ca<br/>Secret (CA cert)"]
|
||||
S3["navigator-client-tls<br/>Secret (client cert+key+CA)"]
|
||||
S1["openshell-server-tls<br/>Secret (server cert+key)"]
|
||||
S2["openshell-server-client-ca<br/>Secret (CA cert)"]
|
||||
S3["openshell-client-tls<br/>Secret (client cert+key+CA)"]
|
||||
GW["Gateway Process<br/>(tokio-rustls)"]
|
||||
SBX["Sandbox Pod"]
|
||||
end
|
||||
@@ -49,17 +49,17 @@ graph TD
|
||||
|
||||
## Certificate Hierarchy
|
||||
|
||||
The PKI is a single-tier CA hierarchy generated by the `navigator-bootstrap` crate using `rcgen`. All certificates are created in a single pass at cluster bootstrap time.
|
||||
The PKI is a single-tier CA hierarchy generated by the `openshell-bootstrap` crate using `rcgen`. All certificates are created in a single pass at cluster bootstrap time.
|
||||
|
||||
```
|
||||
navigator-ca (Self-signed Root CA, O=navigator, CN=navigator-ca)
|
||||
├── navigator-server (Leaf cert, CN=navigator-server)
|
||||
│ SANs: navigator, navigator.navigator.svc,
|
||||
│ navigator.navigator.svc.cluster.local,
|
||||
openshell-ca (Self-signed Root CA, O=openshell, CN=openshell-ca)
|
||||
├── openshell-server (Leaf cert, CN=openshell-server)
|
||||
│ SANs: openshell, openshell.openshell.svc,
|
||||
│ openshell.openshell.svc.cluster.local,
|
||||
│ localhost, host.docker.internal, 127.0.0.1
|
||||
│ + extra SANs for remote deployments
|
||||
│
|
||||
└── navigator-client (Leaf cert, CN=navigator-client)
|
||||
└── openshell-client (Leaf cert, CN=openshell-client)
|
||||
Shared by the CLI and all sandbox pods.
|
||||
```
|
||||
|
||||
@@ -69,19 +69,19 @@ Key design decisions:
|
||||
- **Long-lived certificates**: Certificates use `rcgen` defaults (validity ~1975--4096), which effectively never expire. This is appropriate for an internal dev-cluster PKI where certificates are ephemeral to the cluster's lifetime.
|
||||
- **CA key not persisted**: The CA private key is used only during generation and is not stored in any Kubernetes secret. Re-signing requires regenerating the entire PKI.
|
||||
|
||||
See `crates/navigator-bootstrap/src/pki.rs:35` for the `generate_pki()` implementation and `crates/navigator-bootstrap/src/pki.rs:18` for the default SAN list.
|
||||
See `crates/openshell-bootstrap/src/pki.rs:35` for the `generate_pki()` implementation and `crates/openshell-bootstrap/src/pki.rs:18` for the default SAN list.
|
||||
|
||||
## Kubernetes Secret Distribution
|
||||
|
||||
The PKI bundle is distributed as three Kubernetes secrets in the `navigator` namespace:
|
||||
The PKI bundle is distributed as three Kubernetes secrets in the `openshell` namespace:
|
||||
|
||||
| Secret Name | Type | Contents | Consumed By |
|
||||
|---|---|---|---|
|
||||
| `navigator-server-tls` | `kubernetes.io/tls` | `tls.crt` (server cert), `tls.key` (server key) | Gateway StatefulSet |
|
||||
| `navigator-server-client-ca` | `Opaque` | `ca.crt` (CA cert) | Gateway StatefulSet (client verification) |
|
||||
| `navigator-client-tls` | `Opaque` | `tls.crt` (client cert), `tls.key` (client key), `ca.crt` (CA cert) | Sandbox pods, CLI (via local filesystem) |
|
||||
| `openshell-server-tls` | `kubernetes.io/tls` | `tls.crt` (server cert), `tls.key` (server key) | Gateway StatefulSet |
|
||||
| `openshell-server-client-ca` | `Opaque` | `ca.crt` (CA cert) | Gateway StatefulSet (client verification) |
|
||||
| `openshell-client-tls` | `Opaque` | `tls.crt` (client cert), `tls.key` (client key), `ca.crt` (CA cert) | Sandbox pods, CLI (via local filesystem) |
|
||||
|
||||
Secret names are defined as constants in `crates/navigator-bootstrap/src/constants.rs:6-10`.
|
||||
Secret names are defined as constants in `crates/openshell-bootstrap/src/constants.rs:6-10`.
|
||||
|
||||
### Gateway Mounts
|
||||
|
||||
@@ -89,8 +89,8 @@ The Helm StatefulSet (`deploy/helm/openshell/templates/statefulset.yaml`) mounts
|
||||
|
||||
| Volume | Mount Path | Source Secret |
|
||||
|---|---|---|
|
||||
| `tls-cert` | `/etc/openshell-tls/server/` (read-only) | `navigator-server-tls` |
|
||||
| `tls-client-ca` | `/etc/openshell-tls/client-ca/` (read-only) | `navigator-server-client-ca` |
|
||||
| `tls-cert` | `/etc/openshell-tls/server/` (read-only) | `openshell-server-tls` |
|
||||
| `tls-client-ca` | `/etc/openshell-tls/client-ca/` (read-only) | `openshell-server-client-ca` |
|
||||
|
||||
Environment variables point the gateway binary to these paths:
|
||||
|
||||
@@ -102,9 +102,9 @@ OPENSHELL_TLS_CLIENT_CA=/etc/openshell-tls/client-ca/ca.crt
|
||||
|
||||
### Sandbox Pod Mounts
|
||||
|
||||
When the gateway creates a sandbox pod (`crates/navigator-server/src/sandbox/mod.rs:681`), it injects:
|
||||
When the gateway creates a sandbox pod (`crates/openshell-server/src/sandbox/mod.rs:681`), it injects:
|
||||
|
||||
- A volume backed by the `navigator-client-tls` secret.
|
||||
- A volume backed by the `openshell-client-tls` secret.
|
||||
- A read-only mount at `/etc/openshell-tls/client/` on the agent container.
|
||||
- Environment variables for the sandbox gRPC client:
|
||||
|
||||
@@ -112,7 +112,7 @@ When the gateway creates a sandbox pod (`crates/navigator-server/src/sandbox/mod
|
||||
OPENSHELL_TLS_CA=/etc/openshell-tls/client/ca.crt
|
||||
OPENSHELL_TLS_CERT=/etc/openshell-tls/client/tls.crt
|
||||
OPENSHELL_TLS_KEY=/etc/openshell-tls/client/tls.key
|
||||
OPENSHELL_ENDPOINT=https://navigator.navigator.svc.cluster.local:8080
|
||||
OPENSHELL_ENDPOINT=https://openshell.openshell.svc.cluster.local:8080
|
||||
```
|
||||
|
||||
### CLI Local Storage
|
||||
@@ -126,21 +126,21 @@ $XDG_CONFIG_HOME/openshell/gateways/<gateway-name>/mtls/
|
||||
└── tls.key
|
||||
```
|
||||
|
||||
Files are written atomically using a temp-dir -> validate -> rename strategy with backup and rollback on failure. See `crates/navigator-bootstrap/src/mtls.rs:10`.
|
||||
Files are written atomically using a temp-dir -> validate -> rename strategy with backup and rollback on failure. See `crates/openshell-bootstrap/src/mtls.rs:10`.
|
||||
|
||||
## PKI Bootstrap Sequence
|
||||
|
||||
PKI provisioning occurs during `deploy_cluster_with_logs()` (`crates/navigator-bootstrap/src/lib.rs:284`). The full sequence:
|
||||
PKI provisioning occurs during `deploy_cluster_with_logs()` (`crates/openshell-bootstrap/src/lib.rs:284`). The full sequence:
|
||||
|
||||
1. **Cluster container launched** -- a k3s container is created via Docker with a persistent volume.
|
||||
2. **k3s readiness** -- the bootstrap waits for k3s to become ready inside the container.
|
||||
3. **Extra SANs computed** -- for remote deployments, the SSH destination hostname and its resolved IP are added to the server certificate's SANs. For local deployments, the detected gateway host (if any) is added.
|
||||
4. **`reconcile_pki()` called** (`crates/navigator-bootstrap/src/lib.rs:515`):
|
||||
1. Wait for the `navigator` namespace to exist (created by the Helm controller).
|
||||
4. **`reconcile_pki()` called** (`crates/openshell-bootstrap/src/lib.rs:515`):
|
||||
1. Wait for the `openshell` namespace to exist (created by the Helm controller).
|
||||
2. Attempt to load existing PKI from the three K8s secrets via `kubectl get secret` exec'd inside the container. Each field is base64-decoded and validated for PEM markers.
|
||||
3. **If secrets exist and are valid**: reuse them and return `rotated=false`.
|
||||
4. **If secrets are missing, incomplete, or malformed**: generate fresh PKI via `generate_pki()`, apply all three secrets via `kubectl apply`, and return `rotated=true`.
|
||||
5. **Workload restart on rotation** -- if `rotated=true` and the navigator StatefulSet already exists, the bootstrap performs `kubectl rollout restart` and waits for completion. This ensures the server picks up new TLS secrets before the CLI writes its local copy.
|
||||
5. **Workload restart on rotation** -- if `rotated=true` and the openshell StatefulSet already exists, the bootstrap performs `kubectl rollout restart` and waits for completion. This ensures the server picks up new TLS secrets before the CLI writes its local copy.
|
||||
6. **CLI-side credential storage** -- `store_pki_bundle()` writes `ca.crt`, `tls.crt`, `tls.key` to the local filesystem.
|
||||
|
||||
```mermaid
|
||||
@@ -150,7 +150,7 @@ sequenceDiagram
|
||||
participant K8s as k3s / K8s API
|
||||
|
||||
CLI->>Docker: Create container, wait for k3s
|
||||
CLI->>K8s: Wait for navigator namespace
|
||||
CLI->>K8s: Wait for openshell namespace
|
||||
CLI->>K8s: Read existing TLS secrets
|
||||
alt Secrets valid
|
||||
CLI->>CLI: Reuse existing PKI
|
||||
@@ -175,7 +175,7 @@ The gateway supports three transport modes:
|
||||
|
||||
### Server Configuration
|
||||
|
||||
`TlsAcceptor::from_files()` (`crates/navigator-server/src/tls.rs:27`) constructs the `rustls::ServerConfig`:
|
||||
`TlsAcceptor::from_files()` (`crates/openshell-server/src/tls.rs:27`) constructs the `rustls::ServerConfig`:
|
||||
|
||||
1. **Server identity**: loads the server certificate and private key from PEM files (supports PKCS#1, PKCS#8, and SEC1 key formats).
|
||||
2. **Client verification**: builds a `WebPkiClientVerifier` from the CA certificate. In the default mode it requires a valid client certificate; in dual-auth mode it also accepts no-certificate clients and defers authentication to the HTTP/gRPC layer.
|
||||
@@ -216,7 +216,7 @@ The e2e test suite (`e2e/python/test_security_tls.py`) validates four scenarios:
|
||||
|
||||
## Sandbox-to-Gateway mTLS
|
||||
|
||||
Sandbox pods connect back to the gateway at startup to fetch their policy and provider credentials. The gRPC client (`crates/navigator-sandbox/src/grpc_client.rs:18`) reads three environment variables to configure mTLS:
|
||||
Sandbox pods connect back to the gateway at startup to fetch their policy and provider credentials. The gRPC client (`crates/openshell-sandbox/src/grpc_client.rs:18`) reads three environment variables to configure mTLS:
|
||||
|
||||
| Env Var | Value |
|
||||
|---|---|
|
||||
@@ -291,9 +291,9 @@ Traffic flows through several layers from the host to the gateway process:
|
||||
| Layer | Port | Configurable Via |
|
||||
|---|---|---|
|
||||
| Host (Docker) | `8080` (default) | `--port` flag on `nav deploy` |
|
||||
| Container | `30051` | Hardcoded in `crates/navigator-bootstrap/src/docker.rs` |
|
||||
| k3s NodePort | `30051` | `deploy/helm/navigator/values.yaml` (`service.nodePort`) |
|
||||
| k3s Service | `8080` | `deploy/helm/navigator/values.yaml` (`service.port`) |
|
||||
| Container | `30051` | Hardcoded in `crates/openshell-bootstrap/src/docker.rs` |
|
||||
| k3s NodePort | `30051` | `deploy/helm/openshell/values.yaml` (`service.nodePort`) |
|
||||
| k3s Service | `8080` | `deploy/helm/openshell/values.yaml` (`service.port`) |
|
||||
| Server bind | `8080` | `--port` flag / `OPENSHELL_SERVER_PORT` env var |
|
||||
|
||||
Docker maps `host_port → 30051/tcp`. Inside k3s, the NodePort service maps `30051 → 8080 (pod port)`. The server binds `0.0.0.0:8080`.
|
||||
@@ -341,12 +341,12 @@ graph LR
|
||||
|
||||
### What Is Not Authenticated (by Design)
|
||||
|
||||
- **Individual sandbox identity at the TLS layer**: all sandboxes share one client certificate (`CN=navigator-client`). Post-TLS identification uses the `x-sandbox-id` gRPC metadata header, which is trusted because it arrives over an mTLS-authenticated channel.
|
||||
- **Individual sandbox identity at the TLS layer**: all sandboxes share one client certificate (`CN=openshell-client`). Post-TLS identification uses the `x-sandbox-id` gRPC metadata header, which is trusted because it arrives over an mTLS-authenticated channel.
|
||||
- **Health endpoints in reverse-proxy mode**: when the gateway is deployed behind Cloudflare or another trusted edge, `/health`, `/healthz`, and `/readyz` are protected by that upstream boundary rather than by direct mTLS at the gateway.
|
||||
|
||||
### Gateway Security Context
|
||||
|
||||
The gateway container runs with a hardened security context (`deploy/helm/navigator/values.yaml:25`):
|
||||
The gateway container runs with a hardened security context (`deploy/helm/openshell/values.yaml:25`):
|
||||
|
||||
```yaml
|
||||
securityContext:
|
||||
@@ -405,7 +405,7 @@ This section defines the primary attacker profiles, what the current design prot
|
||||
### Out of Scope / Not Defended By This Layer
|
||||
|
||||
- A fully compromised Kubernetes control plane or cluster-admin account.
|
||||
- A malicious actor with direct access to Kubernetes secrets in the `navigator` namespace.
|
||||
- A malicious actor with direct access to Kubernetes secrets in the `openshell` namespace.
|
||||
- Host-level compromise of the developer workstation running the CLI.
|
||||
- Application-layer authorization bugs after mTLS authentication succeeds.
|
||||
|
||||
@@ -422,7 +422,7 @@ Separate from the cluster mTLS infrastructure, each sandbox has an independent T
|
||||
|
||||
When a sandbox policy configures `tls: terminate` on an endpoint, the sandbox proxy performs TLS man-in-the-middle inspection:
|
||||
|
||||
1. **Ephemeral sandbox CA**: a per-sandbox CA (`CN=Navigator Sandbox CA, O=Navigator`) is generated at sandbox startup. This CA is completely independent of the cluster mTLS CA.
|
||||
1. **Ephemeral sandbox CA**: a per-sandbox CA (`CN=OpenShell Sandbox CA, O=OpenShell`) is generated at sandbox startup. This CA is completely independent of the cluster mTLS CA.
|
||||
2. **Trust injection**: the sandbox CA is written to the sandbox filesystem and injected via `NODE_EXTRA_CA_CERTS` and `SSL_CERT_FILE` so processes inside the sandbox trust it.
|
||||
3. **Dynamic leaf certs**: for each target hostname, the proxy generates and caches a leaf certificate signed by the sandbox CA (up to 256 entries).
|
||||
4. **Upstream verification**: the proxy verifies upstream server certificates against Mozilla root CAs (`webpki-roots`), not against the cluster CA.
|
||||
|
||||
@@ -4,7 +4,7 @@ This document describes how OpenShell bootstraps a single-node k3s gateway insid
|
||||
|
||||
## Goals and Scope
|
||||
|
||||
- Provide a single bootstrap flow through `navigator-bootstrap` for local and remote gateway lifecycle.
|
||||
- Provide a single bootstrap flow through `openshell-bootstrap` for local and remote gateway lifecycle.
|
||||
- Keep Docker as the only runtime dependency for provisioning and lifecycle operations.
|
||||
- Package the OpenShell gateway as one container image, transferred to the target host via registry pull.
|
||||
- Support idempotent `deploy` behavior (safe to re-run).
|
||||
@@ -17,18 +17,18 @@ Out of scope:
|
||||
|
||||
## Components
|
||||
|
||||
- `crates/navigator-cli/src/main.rs`: CLI entry point; `clap`-based command parsing.
|
||||
- `crates/navigator-cli/src/run.rs`: CLI command implementations (`gateway_start`, `gateway_stop`, `gateway_destroy`, `gateway_info`, `doctor_logs`).
|
||||
- `crates/navigator-cli/src/bootstrap.rs`: Auto-bootstrap helpers for `sandbox create` (offers to deploy a gateway when one is unreachable).
|
||||
- `crates/navigator-bootstrap/src/lib.rs`: Gateway lifecycle orchestration (`deploy_gateway`, `deploy_gateway_with_logs`, `gateway_handle`, `check_existing_deployment`).
|
||||
- `crates/navigator-bootstrap/src/docker.rs`: Docker API wrappers (network, volume, container, image operations).
|
||||
- `crates/navigator-bootstrap/src/image.rs`: Remote image registry pull with XOR-obfuscated distribution credentials.
|
||||
- `crates/navigator-bootstrap/src/runtime.rs`: In-container operations via `docker exec` (health polling, stale node cleanup, deployment restart).
|
||||
- `crates/navigator-bootstrap/src/metadata.rs`: Gateway metadata creation, storage, and active gateway tracking.
|
||||
- `crates/navigator-bootstrap/src/mtls.rs`: Gateway TLS detection and CLI mTLS bundle extraction.
|
||||
- `crates/navigator-bootstrap/src/push.rs`: Local development image push into k3s containerd.
|
||||
- `crates/navigator-bootstrap/src/paths.rs`: XDG path resolution.
|
||||
- `crates/navigator-bootstrap/src/constants.rs`: Shared constants (image name, network name, container/volume naming).
|
||||
- `crates/openshell-cli/src/main.rs`: CLI entry point; `clap`-based command parsing.
|
||||
- `crates/openshell-cli/src/run.rs`: CLI command implementations (`gateway_start`, `gateway_stop`, `gateway_destroy`, `gateway_info`, `doctor_logs`).
|
||||
- `crates/openshell-cli/src/bootstrap.rs`: Auto-bootstrap helpers for `sandbox create` (offers to deploy a gateway when one is unreachable).
|
||||
- `crates/openshell-bootstrap/src/lib.rs`: Gateway lifecycle orchestration (`deploy_gateway`, `deploy_gateway_with_logs`, `gateway_handle`, `check_existing_deployment`).
|
||||
- `crates/openshell-bootstrap/src/docker.rs`: Docker API wrappers (network, volume, container, image operations).
|
||||
- `crates/openshell-bootstrap/src/image.rs`: Remote image registry pull with XOR-obfuscated distribution credentials.
|
||||
- `crates/openshell-bootstrap/src/runtime.rs`: In-container operations via `docker exec` (health polling, stale node cleanup, deployment restart).
|
||||
- `crates/openshell-bootstrap/src/metadata.rs`: Gateway metadata creation, storage, and active gateway tracking.
|
||||
- `crates/openshell-bootstrap/src/mtls.rs`: Gateway TLS detection and CLI mTLS bundle extraction.
|
||||
- `crates/openshell-bootstrap/src/push.rs`: Local development image push into k3s containerd.
|
||||
- `crates/openshell-bootstrap/src/paths.rs`: XDG path resolution.
|
||||
- `crates/openshell-bootstrap/src/constants.rs`: Shared constants (image name, network name, container/volume naming).
|
||||
- `deploy/docker/Dockerfile.cluster`: Container image definition (k3s base + Helm charts + manifests + entrypoint).
|
||||
- `deploy/docker/cluster-entrypoint.sh`: Container entrypoint (DNS proxy, registry config, manifest injection).
|
||||
- `deploy/docker/cluster-healthcheck.sh`: Docker HEALTHCHECK script.
|
||||
@@ -76,8 +76,8 @@ Fast mode ensures a local registry (`127.0.0.1:5000`) is running and configures
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant C as navigator-cli
|
||||
participant B as navigator-bootstrap
|
||||
participant C as openshell-cli
|
||||
participant B as openshell-bootstrap
|
||||
participant L as Local Docker daemon
|
||||
participant R as Remote Docker daemon (SSH)
|
||||
|
||||
@@ -97,7 +97,7 @@ sequenceDiagram
|
||||
B->>R: start_container
|
||||
B->>R: clean_stale_nodes (kubectl delete node)
|
||||
B->>R: wait_for_gateway_ready (180 attempts, 2s apart)
|
||||
B->>R: poll for secret navigator-cli-client (90 attempts, 2s apart)
|
||||
B->>R: poll for secret openshell-cli-client (90 attempts, 2s apart)
|
||||
R-->>B: ca.crt, tls.crt, tls.key
|
||||
B->>B: atomically store mTLS bundle
|
||||
B->>B: create and persist gateway metadata JSON
|
||||
@@ -110,7 +110,7 @@ sequenceDiagram
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph WS[User workstation]
|
||||
NAV[navigator-cli]
|
||||
NAV[openshell-cli]
|
||||
MTLS[mTLS bundle ca.crt, tls.crt, tls.key]
|
||||
end
|
||||
|
||||
@@ -134,7 +134,7 @@ flowchart LR
|
||||
|
||||
### 1) Entry and client selection
|
||||
|
||||
`deploy_gateway(DeployOptions)` in `crates/navigator-bootstrap/src/lib.rs` chooses execution mode:
|
||||
`deploy_gateway(DeployOptions)` in `crates/openshell-bootstrap/src/lib.rs` chooses execution mode:
|
||||
|
||||
- `DeployOptions` fields: `name: String`, `image_ref: Option<String>`, `remote: Option<RemoteOptions>`, `port: u16` (default 8080).
|
||||
- `RemoteOptions` fields: `destination: String`, `ssh_key: Option<String>`.
|
||||
@@ -185,7 +185,7 @@ For the target daemon (local or remote):
|
||||
After the container starts:
|
||||
|
||||
1. **Clean stale nodes**: `clean_stale_nodes()` finds `NotReady` nodes via `kubectl get nodes` and deletes them. This is needed when a container is recreated but reuses the persistent volume -- k3s registers a new node (using the container ID as hostname) while old node entries persist in etcd. Non-fatal on error; returns the count of removed nodes.
|
||||
2. **Push local images** (optional, local deploy only): If `OPENSHELL_PUSH_IMAGES` is set, the comma-separated image refs are exported from the local Docker daemon as a single tar, uploaded into the container via `docker put_archive`, and imported into containerd via `ctr images import` in the `k8s.io` namespace. After import, `kubectl rollout restart deployment/navigator -n navigator` is run, followed by `kubectl rollout status --timeout=180s` to wait for completion. See `crates/navigator-bootstrap/src/push.rs`.
|
||||
2. **Push local images** (optional, local deploy only): If `OPENSHELL_PUSH_IMAGES` is set, the comma-separated image refs are exported from the local Docker daemon as a single tar, uploaded into the container via `docker put_archive`, and imported into containerd via `ctr images import` in the `k8s.io` namespace. After import, `kubectl rollout restart deployment/openshell openshell` is run, followed by `kubectl rollout status --timeout=180s` to wait for completion. See `crates/openshell-bootstrap/src/push.rs`.
|
||||
3. **Wait for gateway health**: `wait_for_gateway_ready()` polls the Docker HEALTHCHECK status up to 180 times, 2 seconds apart (6 min total). A background task streams container logs during this wait. Failure modes:
|
||||
- Container exits during polling: error includes recent log lines.
|
||||
- Container has no HEALTHCHECK instruction: fails immediately.
|
||||
@@ -193,7 +193,7 @@ After the container starts:
|
||||
|
||||
### 5) mTLS bundle capture
|
||||
|
||||
TLS is always required. `fetch_and_store_cli_mtls()` polls for Kubernetes secret `navigator-cli-client` in namespace `navigator` (90 attempts, 2 seconds apart, 3 min total). Each attempt checks the container is still running. The secret's base64-encoded `ca.crt`, `tls.crt`, and `tls.key` fields are decoded and stored.
|
||||
TLS is always required. `fetch_and_store_cli_mtls()` polls for Kubernetes secret `openshell-cli-client` in namespace `openshell` (90 attempts, 2 seconds apart, 3 min total). Each attempt checks the container is still running. The secret's base64-encoded `ca.crt`, `tls.crt`, and `tls.key` fields are decoded and stored.
|
||||
|
||||
Storage location: `~/.config/openshell/gateways/{name}/mtls/`
|
||||
|
||||
@@ -236,10 +236,10 @@ Layers added:
|
||||
1. Custom entrypoint: `deploy/docker/cluster-entrypoint.sh` -> `/usr/local/bin/cluster-entrypoint.sh`
|
||||
2. Healthcheck script: `deploy/docker/cluster-healthcheck.sh` -> `/usr/local/bin/cluster-healthcheck.sh`
|
||||
3. Packaged Helm charts: `deploy/docker/.build/charts/*.tgz` -> `/var/lib/rancher/k3s/server/static/charts/`
|
||||
4. Kubernetes manifests: `deploy/kube/manifests/*.yaml` -> `/opt/navigator/manifests/`
|
||||
4. Kubernetes manifests: `deploy/kube/manifests/*.yaml` -> `/opt/openshell/manifests/`
|
||||
|
||||
Bundled manifests include:
|
||||
- `navigator-helmchart.yaml` (OpenShell Helm chart auto-deploy)
|
||||
- `openshell-helmchart.yaml` (OpenShell Helm chart auto-deploy)
|
||||
- `envoy-gateway-helmchart.yaml` (Envoy Gateway for Gateway API)
|
||||
- `agent-sandbox.yaml`
|
||||
|
||||
@@ -267,11 +267,11 @@ Writes `/etc/rancher/k3s/registries.yaml` from `REGISTRY_HOST`, `REGISTRY_ENDPOI
|
||||
|
||||
### Manifest injection
|
||||
|
||||
Copies bundled manifests from `/opt/navigator/manifests/` to `/var/lib/rancher/k3s/server/manifests/`. This is needed because the volume mount on `/var/lib/rancher/k3s` overwrites any files baked into that path at image build time.
|
||||
Copies bundled manifests from `/opt/openshell/manifests/` to `/var/lib/rancher/k3s/server/manifests/`. This is needed because the volume mount on `/var/lib/rancher/k3s` overwrites any files baked into that path at image build time.
|
||||
|
||||
### Image configuration overrides
|
||||
|
||||
When environment variables are set, the entrypoint modifies the HelmChart manifest at `/var/lib/rancher/k3s/server/manifests/navigator-helmchart.yaml`:
|
||||
When environment variables are set, the entrypoint modifies the HelmChart manifest at `/var/lib/rancher/k3s/server/manifests/openshell-helmchart.yaml`:
|
||||
|
||||
- `IMAGE_REPO_BASE`: Rewrites `repository:`, `sandboxImage:`, and `jobImage:` in the HelmChart.
|
||||
- `PUSH_IMAGE_REFS`: In push mode, parses comma-separated image refs and rewrites the exact gateway, sandbox, and pki-job image references (matching on path component `/gateway:`, `/sandbox:`, `/pki-job:`).
|
||||
@@ -285,15 +285,15 @@ When environment variables are set, the entrypoint modifies the HelmChart manife
|
||||
`deploy/docker/cluster-healthcheck.sh` validates cluster readiness through a series of checks:
|
||||
|
||||
1. **Kubernetes API**: `kubectl get --raw='/readyz'`
|
||||
2. **OpenShell StatefulSet**: Checks that `statefulset/navigator` in namespace `navigator` exists and has 1 ready replica.
|
||||
3. **Gateway**: Checks that `gateway/navigator-gateway` in namespace `navigator` has the `Programmed` condition.
|
||||
4. **mTLS secret** (conditional): If `NAV_GATEWAY_TLS_ENABLED` is true (or inferred from the HelmChart manifest using the same two-path detection logic as the bootstrap code), checks that secret `navigator-cli-client` exists with non-empty `ca.crt`, `tls.crt`, and `tls.key` data.
|
||||
2. **OpenShell StatefulSet**: Checks that `statefulset/openshell` in namespace `openshell` exists and has 1 ready replica.
|
||||
3. **Gateway**: Checks that `gateway/openshell-gateway` in namespace `openshell` has the `Programmed` condition.
|
||||
4. **mTLS secret** (conditional): If `NAV_GATEWAY_TLS_ENABLED` is true (or inferred from the HelmChart manifest using the same two-path detection logic as the bootstrap code), checks that secret `openshell-cli-client` exists with non-empty `ca.crt`, `tls.crt`, and `tls.key` data.
|
||||
|
||||
## GPU Enablement
|
||||
|
||||
GPU support is part of the single-node gateway bootstrap path rather than a separate architecture.
|
||||
|
||||
- `openshell gateway start --gpu` threads a boolean deploy option through `crates/navigator-cli`, `crates/navigator-bootstrap`, and `crates/navigator-bootstrap/src/docker.rs`.
|
||||
- `openshell gateway start --gpu` threads a boolean deploy option through `crates/openshell-cli`, `crates/openshell-bootstrap`, and `crates/openshell-bootstrap/src/docker.rs`.
|
||||
- When enabled, the cluster container is created with Docker `DeviceRequests`, which is the API equivalent of `docker run --gpus all`.
|
||||
- `deploy/docker/Dockerfile.cluster` installs NVIDIA Container Toolkit packages in a dedicated Ubuntu stage and copies the runtime binaries, config, and `libnvidia-container` shared libraries into the final Ubuntu-based cluster image.
|
||||
- `deploy/docker/cluster-entrypoint.sh` checks `GPU_ENABLED=true` and copies GPU-only manifests from `/opt/openshell/gpu-manifests/` into k3s's manifests directory.
|
||||
@@ -374,7 +374,7 @@ flowchart LR
|
||||
|
||||
When `openshell sandbox create` cannot connect to a gateway (connection refused, DNS error, missing default TLS certs), the CLI offers to bootstrap one automatically:
|
||||
|
||||
1. `should_attempt_bootstrap()` in `crates/navigator-cli/src/bootstrap.rs` checks the error type. It returns `true` for connectivity errors and missing default TLS materials, but `false` for TLS handshake/auth errors.
|
||||
1. `should_attempt_bootstrap()` in `crates/openshell-cli/src/bootstrap.rs` checks the error type. It returns `true` for connectivity errors and missing default TLS materials, but `false` for TLS handshake/auth errors.
|
||||
2. If running in a terminal, the user is prompted to confirm.
|
||||
3. `run_bootstrap()` deploys a gateway named `"openshell"`, sets it as active, and returns fresh `TlsOptions` pointing to the newly-written mTLS certs.
|
||||
|
||||
@@ -411,7 +411,7 @@ Environment variables that affect bootstrap behavior when set on the host:
|
||||
| `DOCKER_HOST` | When `tcp://` and non-loopback, the host is added as a TLS SAN and used as the gateway endpoint |
|
||||
| `OPENSHELL_PUSH_IMAGES` | Comma-separated image refs to push into the gateway's containerd (local deploy only) |
|
||||
| `OPENSHELL_REGISTRY_HOST` | Override the distribution registry host |
|
||||
| `OPENSHELL_REGISTRY_NAMESPACE` | Override the registry namespace (default: `"navigator"`) |
|
||||
| `OPENSHELL_REGISTRY_NAMESPACE` | Override the registry namespace (default: `"openshell"`) |
|
||||
| `IMAGE_REPO_BASE` / `OPENSHELL_IMAGE_REPO_BASE` | Override the image repository base path |
|
||||
| `OPENSHELL_REGISTRY_INSECURE` | Use HTTP instead of HTTPS for registry mirror |
|
||||
| `OPENSHELL_REGISTRY_ENDPOINT` | Custom registry mirror endpoint |
|
||||
@@ -437,20 +437,20 @@ openshell/
|
||||
|
||||
## Implementation References
|
||||
|
||||
- `crates/navigator-bootstrap/src/lib.rs` -- public API, deploy orchestration
|
||||
- `crates/navigator-bootstrap/src/docker.rs` -- Docker API wrappers
|
||||
- `crates/navigator-bootstrap/src/image.rs` -- registry pull, XOR credentials
|
||||
- `crates/navigator-bootstrap/src/runtime.rs` -- exec, health polling, stale node cleanup
|
||||
- `crates/navigator-bootstrap/src/metadata.rs` -- metadata CRUD, active gateway, SSH resolution
|
||||
- `crates/navigator-bootstrap/src/mtls.rs` -- TLS detection, secret extraction, atomic write
|
||||
- `crates/navigator-bootstrap/src/push.rs` -- local image push into k3s containerd
|
||||
- `crates/navigator-bootstrap/src/constants.rs` -- naming conventions
|
||||
- `crates/navigator-bootstrap/src/paths.rs` -- XDG path helpers
|
||||
- `crates/navigator-cli/src/main.rs` -- CLI command definitions
|
||||
- `crates/navigator-cli/src/run.rs` -- CLI command implementations
|
||||
- `crates/navigator-cli/src/bootstrap.rs` -- auto-bootstrap from sandbox create
|
||||
- `crates/openshell-bootstrap/src/lib.rs` -- public API, deploy orchestration
|
||||
- `crates/openshell-bootstrap/src/docker.rs` -- Docker API wrappers
|
||||
- `crates/openshell-bootstrap/src/image.rs` -- registry pull, XOR credentials
|
||||
- `crates/openshell-bootstrap/src/runtime.rs` -- exec, health polling, stale node cleanup
|
||||
- `crates/openshell-bootstrap/src/metadata.rs` -- metadata CRUD, active gateway, SSH resolution
|
||||
- `crates/openshell-bootstrap/src/mtls.rs` -- TLS detection, secret extraction, atomic write
|
||||
- `crates/openshell-bootstrap/src/push.rs` -- local image push into k3s containerd
|
||||
- `crates/openshell-bootstrap/src/constants.rs` -- naming conventions
|
||||
- `crates/openshell-bootstrap/src/paths.rs` -- XDG path helpers
|
||||
- `crates/openshell-cli/src/main.rs` -- CLI command definitions
|
||||
- `crates/openshell-cli/src/run.rs` -- CLI command implementations
|
||||
- `crates/openshell-cli/src/bootstrap.rs` -- auto-bootstrap from sandbox create
|
||||
- `deploy/docker/Dockerfile.cluster` -- container image definition
|
||||
- `deploy/docker/cluster-entrypoint.sh` -- container entrypoint script
|
||||
- `deploy/docker/cluster-healthcheck.sh` -- Docker HEALTHCHECK script
|
||||
- `deploy/kube/manifests/navigator-helmchart.yaml` -- OpenShell Helm chart manifest
|
||||
- `deploy/kube/manifests/openshell-helmchart.yaml` -- OpenShell Helm chart manifest
|
||||
- `deploy/kube/manifests/envoy-gateway-helmchart.yaml` -- Envoy Gateway manifest
|
||||
|
||||
+55
-55
@@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
`navigator-server` is the gateway -- the central control plane for a cluster. It exposes two gRPC services (Navigator and Inference) and HTTP endpoints on a single multiplexed port, manages sandbox lifecycle through Kubernetes CRDs, persists state in SQLite or Postgres, and provides SSH tunneling into sandbox pods. The gateway coordinates all interactions between clients, the Kubernetes cluster, and the persistence layer.
|
||||
`openshell-server` is the gateway -- the central control plane for a cluster. It exposes two gRPC services (OpenShell and Inference) and HTTP endpoints on a single multiplexed port, manages sandbox lifecycle through Kubernetes CRDs, persists state in SQLite or Postgres, and provides SSH tunneling into sandbox pods. The gateway coordinates all interactions between clients, the Kubernetes cluster, and the persistence layer.
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
@@ -15,7 +15,7 @@ graph TD
|
||||
TLS["TLS Acceptor<br/>(optional)"]
|
||||
MUX["MultiplexedService"]
|
||||
GRPC_ROUTER["GrpcRouter"]
|
||||
NAV["NavigatorServer<br/>(Navigator service)"]
|
||||
NAV["OpenShellServer<br/>(OpenShell service)"]
|
||||
INF["InferenceServer<br/>(Inference service)"]
|
||||
HTTP["HTTP Router<br/>(Axum)"]
|
||||
HEALTH["Health Endpoints"]
|
||||
@@ -34,7 +34,7 @@ graph TD
|
||||
TLS --> MUX
|
||||
MUX -->|"content-type: application/grpc"| GRPC_ROUTER
|
||||
MUX -->|"other"| HTTP
|
||||
GRPC_ROUTER -->|"/navigator.inference.v1.Inference/*"| INF
|
||||
GRPC_ROUTER -->|"/openshell.inference.v1.Inference/*"| INF
|
||||
GRPC_ROUTER -->|"all other paths"| NAV
|
||||
HTTP --> HEALTH
|
||||
HTTP --> SSH_TUNNEL
|
||||
@@ -57,42 +57,42 @@ graph TD
|
||||
|
||||
| Module | File | Purpose |
|
||||
|--------|------|---------|
|
||||
| Entry point | `crates/navigator-server/src/main.rs` | CLI argument parsing, config assembly, tracing setup, calls `run_server` |
|
||||
| Gateway runtime | `crates/navigator-server/src/lib.rs` | `ServerState` struct, `run_server()` accept loop |
|
||||
| Protocol mux | `crates/navigator-server/src/multiplex.rs` | `MultiplexService`, `MultiplexedService`, `GrpcRouter`, `BoxBody` |
|
||||
| gRPC: Navigator | `crates/navigator-server/src/grpc.rs` | `NavigatorService` -- sandbox CRUD, provider CRUD, watch, exec, SSH sessions, policy delivery |
|
||||
| gRPC: Inference | `crates/navigator-server/src/inference.rs` | `InferenceService` -- cluster inference config (set/get) and sandbox inference bundle delivery |
|
||||
| HTTP | `crates/navigator-server/src/http.rs` | Health endpoints, merged with SSH tunnel router |
|
||||
| Browser auth | `crates/navigator-server/src/auth.rs` | Cloudflare browser login relay at `/auth/connect` |
|
||||
| SSH tunnel | `crates/navigator-server/src/ssh_tunnel.rs` | HTTP CONNECT handler at `/connect/ssh` |
|
||||
| WS tunnel | `crates/navigator-server/src/ws_tunnel.rs` | WebSocket tunnel handler at `/_ws_tunnel` for Cloudflare-fronted clients |
|
||||
| TLS | `crates/navigator-server/src/tls.rs` | `TlsAcceptor` wrapping rustls with ALPN |
|
||||
| Persistence | `crates/navigator-server/src/persistence/mod.rs` | `Store` enum (SQLite/Postgres), generic object CRUD, protobuf codec |
|
||||
| Persistence: SQLite | `crates/navigator-server/src/persistence/sqlite.rs` | `SqliteStore` with sqlx |
|
||||
| Persistence: Postgres | `crates/navigator-server/src/persistence/postgres.rs` | `PostgresStore` with sqlx |
|
||||
| Sandbox K8s | `crates/navigator-server/src/sandbox/mod.rs` | `SandboxClient`, CRD creation/deletion, Kubernetes watcher, phase derivation |
|
||||
| Sandbox index | `crates/navigator-server/src/sandbox_index.rs` | `SandboxIndex` -- in-memory name/pod-to-id correlation |
|
||||
| Watch bus | `crates/navigator-server/src/sandbox_watch.rs` | `SandboxWatchBus`, `PlatformEventBus`, Kubernetes event tailer |
|
||||
| Tracing bus | `crates/navigator-server/src/tracing_bus.rs` | `TracingLogBus` -- captures tracing events keyed by `sandbox_id` |
|
||||
| Entry point | `crates/openshell-server/src/main.rs` | CLI argument parsing, config assembly, tracing setup, calls `run_server` |
|
||||
| Gateway runtime | `crates/openshell-server/src/lib.rs` | `ServerState` struct, `run_server()` accept loop |
|
||||
| Protocol mux | `crates/openshell-server/src/multiplex.rs` | `MultiplexService`, `MultiplexedService`, `GrpcRouter`, `BoxBody` |
|
||||
| gRPC: OpenShell | `crates/openshell-server/src/grpc.rs` | `OpenShellService` -- sandbox CRUD, provider CRUD, watch, exec, SSH sessions, policy delivery |
|
||||
| gRPC: Inference | `crates/openshell-server/src/inference.rs` | `InferenceService` -- cluster inference config (set/get) and sandbox inference bundle delivery |
|
||||
| HTTP | `crates/openshell-server/src/http.rs` | Health endpoints, merged with SSH tunnel router |
|
||||
| Browser auth | `crates/openshell-server/src/auth.rs` | Cloudflare browser login relay at `/auth/connect` |
|
||||
| SSH tunnel | `crates/openshell-server/src/ssh_tunnel.rs` | HTTP CONNECT handler at `/connect/ssh` |
|
||||
| WS tunnel | `crates/openshell-server/src/ws_tunnel.rs` | WebSocket tunnel handler at `/_ws_tunnel` for Cloudflare-fronted clients |
|
||||
| TLS | `crates/openshell-server/src/tls.rs` | `TlsAcceptor` wrapping rustls with ALPN |
|
||||
| Persistence | `crates/openshell-server/src/persistence/mod.rs` | `Store` enum (SQLite/Postgres), generic object CRUD, protobuf codec |
|
||||
| Persistence: SQLite | `crates/openshell-server/src/persistence/sqlite.rs` | `SqliteStore` with sqlx |
|
||||
| Persistence: Postgres | `crates/openshell-server/src/persistence/postgres.rs` | `PostgresStore` with sqlx |
|
||||
| Sandbox K8s | `crates/openshell-server/src/sandbox/mod.rs` | `SandboxClient`, CRD creation/deletion, Kubernetes watcher, phase derivation |
|
||||
| Sandbox index | `crates/openshell-server/src/sandbox_index.rs` | `SandboxIndex` -- in-memory name/pod-to-id correlation |
|
||||
| Watch bus | `crates/openshell-server/src/sandbox_watch.rs` | `SandboxWatchBus`, `PlatformEventBus`, Kubernetes event tailer |
|
||||
| Tracing bus | `crates/openshell-server/src/tracing_bus.rs` | `TracingLogBus` -- captures tracing events keyed by `sandbox_id` |
|
||||
|
||||
Proto definitions consumed by the gateway:
|
||||
|
||||
| Proto file | Package | Defines |
|
||||
|------------|---------|---------|
|
||||
| `proto/navigator.proto` | `navigator.v1` | `Navigator` service, sandbox/provider/SSH/watch messages |
|
||||
| `proto/inference.proto` | `navigator.inference.v1` | `Inference` service: `SetClusterInference`, `GetClusterInference`, `GetInferenceBundle` |
|
||||
| `proto/datamodel.proto` | `navigator.datamodel.v1` | `Sandbox`, `SandboxSpec`, `SandboxStatus`, `Provider`, `SandboxPhase` |
|
||||
| `proto/sandbox.proto` | `navigator.sandbox.v1` | `SandboxPolicy`, `NetworkPolicyRule` |
|
||||
| `proto/openshell.proto` | `openshell.v1` | `OpenShell` service, sandbox/provider/SSH/watch messages |
|
||||
| `proto/inference.proto` | `openshell.inference.v1` | `Inference` service: `SetClusterInference`, `GetClusterInference`, `GetInferenceBundle` |
|
||||
| `proto/datamodel.proto` | `openshell.datamodel.v1` | `Sandbox`, `SandboxSpec`, `SandboxStatus`, `Provider`, `SandboxPhase` |
|
||||
| `proto/sandbox.proto` | `openshell.sandbox.v1` | `SandboxPolicy`, `NetworkPolicyRule` |
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
The gateway boots in `main()` (`crates/navigator-server/src/main.rs`) and proceeds through these steps:
|
||||
The gateway boots in `main()` (`crates/openshell-server/src/main.rs`) and proceeds through these steps:
|
||||
|
||||
1. **Install rustls crypto provider** -- `aws_lc_rs::default_provider().install_default()`.
|
||||
2. **Parse CLI arguments** -- `Args::parse()` via `clap`. Every flag has a corresponding environment variable (see [Configuration](#configuration)).
|
||||
3. **Initialize tracing** -- Creates a `TracingLogBus` and installs a tracing subscriber that writes to stdout and publishes log events keyed by `sandbox_id` into the bus.
|
||||
4. **Build `Config`** -- Assembles a `navigator_core::Config` from the parsed arguments.
|
||||
5. **Call `run_server()`** (`crates/navigator-server/src/lib.rs`):
|
||||
4. **Build `Config`** -- Assembles a `openshell_core::Config` from the parsed arguments.
|
||||
5. **Call `run_server()`** (`crates/openshell-server/src/lib.rs`):
|
||||
1. Connect to the persistence store (`Store::connect`), which auto-detects SQLite vs Postgres from the URL prefix and runs migrations.
|
||||
2. Create `SandboxClient` (initializes a `kube::Client` from in-cluster or kubeconfig).
|
||||
3. Build `ServerState` (shared via `Arc<ServerState>` across all handlers).
|
||||
@@ -118,7 +118,7 @@ All configuration is via CLI flags with environment variable fallbacks. The `--d
|
||||
| `--disable-tls` | `OPENSHELL_DISABLE_TLS` | `false` | Listen on plaintext HTTP behind a trusted reverse proxy or tunnel |
|
||||
| `--disable-gateway-auth` | `OPENSHELL_DISABLE_GATEWAY_AUTH` | `false` | Keep TLS enabled but allow no-certificate clients and rely on application-layer auth |
|
||||
| `--client-tls-secret-name` | `OPENSHELL_CLIENT_TLS_SECRET_NAME` | None | K8s secret name to mount into sandbox pods for mTLS |
|
||||
| `--db-url` | `OPENSHELL_DB_URL` | *required* | Database URL (`sqlite:...` or `postgres://...`). The Helm chart defaults to `sqlite:/var/navigator/navigator.db` (persistent volume). In-memory SQLite (`sqlite::memory:?cache=shared`) works for ephemeral/test environments but data is lost on restart. |
|
||||
| `--db-url` | `OPENSHELL_DB_URL` | *required* | Database URL (`sqlite:...` or `postgres://...`). The Helm chart defaults to `sqlite:/var/openshell/openshell.db` (persistent volume). In-memory SQLite (`sqlite::memory:?cache=shared`) works for ephemeral/test environments but data is lost on restart. |
|
||||
| `--sandbox-namespace` | `OPENSHELL_SANDBOX_NAMESPACE` | `default` | Kubernetes namespace for sandbox CRDs |
|
||||
| `--sandbox-image` | `OPENSHELL_SANDBOX_IMAGE` | None | Default container image for sandbox pods |
|
||||
| `--grpc-endpoint` | `OPENSHELL_GRPC_ENDPOINT` | None | gRPC endpoint reachable from within the cluster (for sandbox callbacks) |
|
||||
@@ -131,7 +131,7 @@ All configuration is via CLI flags with environment variable fallbacks. The `--d
|
||||
|
||||
## Shared State
|
||||
|
||||
All handlers share an `Arc<ServerState>` (`crates/navigator-server/src/lib.rs`):
|
||||
All handlers share an `Arc<ServerState>` (`crates/openshell-server/src/lib.rs`):
|
||||
|
||||
```rust
|
||||
pub struct ServerState {
|
||||
@@ -156,7 +156,7 @@ All traffic (gRPC and HTTP) shares a single TCP port. Multiplexing happens at th
|
||||
|
||||
### Connection Handling
|
||||
|
||||
`MultiplexService::serve()` (`crates/navigator-server/src/multiplex.rs`) creates per-connection service instances:
|
||||
`MultiplexService::serve()` (`crates/openshell-server/src/multiplex.rs`) creates per-connection service instances:
|
||||
|
||||
1. Each accepted TCP stream (optionally TLS-wrapped) is passed to `hyper_util::server::conn::auto::Builder`, which auto-negotiates HTTP/1.1 or HTTP/2.
|
||||
2. The builder calls `serve_connection_with_upgrades()`, which supports HTTP upgrades (needed for the SSH tunnel's CONNECT method).
|
||||
@@ -166,10 +166,10 @@ All traffic (gRPC and HTTP) shares a single TCP port. Multiplexing happens at th
|
||||
|
||||
### gRPC Sub-Routing
|
||||
|
||||
`GrpcRouter` (`crates/navigator-server/src/multiplex.rs`) further routes gRPC requests by URI path prefix:
|
||||
`GrpcRouter` (`crates/openshell-server/src/multiplex.rs`) further routes gRPC requests by URI path prefix:
|
||||
|
||||
- Paths starting with `/navigator.inference.v1.Inference/` go to `InferenceServer`.
|
||||
- All other gRPC paths go to `NavigatorServer`.
|
||||
- Paths starting with `/openshell.inference.v1.Inference/` go to `InferenceServer`.
|
||||
- All other gRPC paths go to `OpenShellServer`.
|
||||
|
||||
### Body Type Normalization
|
||||
|
||||
@@ -177,22 +177,22 @@ Both gRPC and HTTP handlers produce different response body types. `MultiplexedS
|
||||
|
||||
### TLS + mTLS
|
||||
|
||||
When TLS is enabled (`crates/navigator-server/src/tls.rs`):
|
||||
When TLS is enabled (`crates/openshell-server/src/tls.rs`):
|
||||
|
||||
- `TlsAcceptor::from_files()` loads PEM certificates and keys via `rustls_pemfile`, builds a `rustls::ServerConfig`, and configures ALPN to advertise `h2` and `http/1.1`.
|
||||
- When a client CA path is provided (`--tls-client-ca`), the server enforces mutual TLS using `WebPkiClientVerifier` by default. In Cloudflare-fronted deployments, `--disable-gateway-auth` keeps TLS enabled but allows no-certificate clients so the edge can forward a JWT instead.
|
||||
- `--disable-tls` removes gateway-side TLS entirely and serves plaintext HTTP behind a trusted reverse proxy or tunnel.
|
||||
- Supports PKCS#1, PKCS#8, and SEC1 private key formats.
|
||||
- The TLS handshake happens before the stream reaches Hyper's auto builder, so ALPN negotiation and HTTP version detection work together transparently.
|
||||
- Certificates are generated at cluster bootstrap time by the `navigator-bootstrap` crate using `rcgen`, not by a Helm Job. The bootstrap reconciles three K8s secrets: `navigator-server-tls` (server cert+key), `navigator-server-client-ca` (CA cert), and `navigator-client-tls` (client cert+key+CA, shared by CLI and sandbox pods).
|
||||
- Certificates are generated at cluster bootstrap time by the `openshell-bootstrap` crate using `rcgen`, not by a Helm Job. The bootstrap reconciles three K8s secrets: `openshell-server-tls` (server cert+key), `openshell-server-client-ca` (CA cert), and `openshell-client-tls` (client cert+key+CA, shared by CLI and sandbox pods).
|
||||
- **Certificate lifetime**: Certificates use `rcgen` defaults (effectively never expire), which is appropriate for an internal dev-cluster PKI where certs are ephemeral to the cluster's lifetime.
|
||||
- **Redeploy behavior**: On redeploy, existing cluster TLS secrets are loaded and reused if they are complete and valid PEM. If secrets are missing, incomplete, or malformed, fresh PKI is generated. If rotation occurs and the navigator workload is already running, the bootstrap performs a rollout restart and waits for completion before persisting CLI-side credentials.
|
||||
- **Redeploy behavior**: On redeploy, existing cluster TLS secrets are loaded and reused if they are complete and valid PEM. If secrets are missing, incomplete, or malformed, fresh PKI is generated. If rotation occurs and the openshell workload is already running, the bootstrap performs a rollout restart and waits for completion before persisting CLI-side credentials.
|
||||
|
||||
## gRPC Services
|
||||
|
||||
### Navigator Service
|
||||
### OpenShell Service
|
||||
|
||||
Defined in `proto/navigator.proto`, implemented in `crates/navigator-server/src/grpc.rs` as `NavigatorService`.
|
||||
Defined in `proto/openshell.proto`, implemented in `crates/openshell-server/src/grpc.rs` as `OpenShellService`.
|
||||
|
||||
#### Sandbox Management
|
||||
|
||||
@@ -250,7 +250,7 @@ These RPCs support the sandbox-initiated policy recommendation pipeline. The san
|
||||
|
||||
### Inference Service
|
||||
|
||||
Defined in `proto/inference.proto`, implemented in `crates/navigator-server/src/inference.rs` as `InferenceService`.
|
||||
Defined in `proto/inference.proto`, implemented in `crates/openshell-server/src/inference.rs` as `InferenceService`.
|
||||
|
||||
The gateway acts as the control plane for inference configuration. It stores a single managed cluster inference route (named `inference.local`) and delivers resolved route bundles to sandbox pods. The gateway does not execute inference requests -- sandboxes connect directly to inference backends using the credentials and endpoints provided in the bundle.
|
||||
|
||||
@@ -268,7 +268,7 @@ The gateway manages a single cluster-wide inference route that maps to a provide
|
||||
|
||||
The `GetInferenceBundle` RPC resolves the managed cluster route into a `GetInferenceBundleResponse` containing fully materialized route data that sandboxes can use directly.
|
||||
|
||||
The trait method delegates to `resolve_inference_bundle(store)` (`crates/navigator-server/src/inference.rs`), which takes `&Store` instead of `&self`. This extraction decouples bundle resolution from `ServerState`, enabling direct unit testing against an in-memory SQLite store without constructing a full server.
|
||||
The trait method delegates to `resolve_inference_bundle(store)` (`crates/openshell-server/src/inference.rs`), which takes `&Store` instead of `&self`. This extraction decouples bundle resolution from `ServerState`, enabling direct unit testing against an in-memory SQLite store without constructing a full server.
|
||||
|
||||
The `GetInferenceBundleResponse` includes:
|
||||
|
||||
@@ -278,14 +278,14 @@ The `GetInferenceBundleResponse` includes:
|
||||
|
||||
#### Provider-Based Route Resolution
|
||||
|
||||
Managed route resolution in `resolve_managed_cluster_route()` (`crates/navigator-server/src/inference.rs`):
|
||||
Managed route resolution in `resolve_managed_cluster_route()` (`crates/openshell-server/src/inference.rs`):
|
||||
|
||||
1. Load the managed route by name (`inference.local`).
|
||||
2. Skip (return `None`) if the route does not exist, has no spec, or is disabled.
|
||||
3. Validate that `provider_name` and `model_id` are non-empty.
|
||||
4. Fetch the referenced provider record from the store.
|
||||
5. Resolve the provider into a `ResolvedProviderRoute` via `resolve_provider_route()`:
|
||||
- Look up the `InferenceProviderProfile` for the provider's type via `navigator_core::inference::profile_for()`. Supported types: `openai`, `anthropic`, `nvidia`.
|
||||
- Look up the `InferenceProviderProfile` for the provider's type via `openshell_core::inference::profile_for()`. Supported types: `openai`, `anthropic`, `nvidia`.
|
||||
- Search the provider's credentials map for an API key using the profile's preferred key name (e.g., `OPENAI_API_KEY`), falling back to the first non-empty credential in sorted key order.
|
||||
- Resolve the base URL from the provider's config map using the profile-specific key (e.g., `OPENAI_BASE_URL`), falling back to the profile's default URL.
|
||||
- Derive protocols from the profile (e.g., `openai_chat_completions`, `openai_completions`, `openai_responses`, `model_discovery` for OpenAI-compatible providers).
|
||||
@@ -295,7 +295,7 @@ The `ClusterInferenceConfig` stored in the database contains only `provider_name
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
The HTTP router (`crates/navigator-server/src/http.rs`) merges two sub-routers:
|
||||
The HTTP router (`crates/openshell-server/src/http.rs`) merges two sub-routers:
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
@@ -322,7 +322,7 @@ See [SSH Tunnel Gateway](#ssh-tunnel-gateway) for details.
|
||||
|
||||
## Watch Sandbox Stream
|
||||
|
||||
The `WatchSandbox` RPC (`crates/navigator-server/src/grpc.rs`) provides a multiplexed server-streaming response that can include sandbox status snapshots, gateway log lines, and platform events.
|
||||
The `WatchSandbox` RPC (`crates/openshell-server/src/grpc.rs`) provides a multiplexed server-streaming response that can include sandbox status snapshots, gateway log lines, and platform events.
|
||||
|
||||
### Request Options
|
||||
|
||||
@@ -381,7 +381,7 @@ Broadcast lag is translated to `Status::resource_exhausted` via `broadcast_to_st
|
||||
|
||||
## Remote Exec via SSH
|
||||
|
||||
The `ExecSandbox` RPC (`crates/navigator-server/src/grpc.rs`) executes a command inside a sandbox pod over SSH and streams stdout/stderr/exit back to the client.
|
||||
The `ExecSandbox` RPC (`crates/openshell-server/src/grpc.rs`) executes a command inside a sandbox pod over SSH and streams stdout/stderr/exit back to the client.
|
||||
|
||||
### Execution Flow
|
||||
|
||||
@@ -412,7 +412,7 @@ The `ssh_handshake_skew_secs` configuration controls how much clock skew is tole
|
||||
|
||||
## SSH Tunnel Gateway
|
||||
|
||||
The SSH tunnel endpoint (`crates/navigator-server/src/ssh_tunnel.rs`) allows external SSH clients to reach sandbox pods through the gateway using HTTP CONNECT upgrades.
|
||||
The SSH tunnel endpoint (`crates/openshell-server/src/ssh_tunnel.rs`) allows external SSH clients to reach sandbox pods through the gateway using HTTP CONNECT upgrades.
|
||||
|
||||
### Request Flow
|
||||
|
||||
@@ -429,12 +429,12 @@ The SSH tunnel endpoint (`crates/navigator-server/src/ssh_tunnel.rs`) allows ext
|
||||
|
||||
### Store Architecture
|
||||
|
||||
The `Store` enum (`crates/navigator-server/src/persistence/mod.rs`) dispatches to either `SqliteStore` or `PostgresStore` based on the database URL prefix:
|
||||
The `Store` enum (`crates/openshell-server/src/persistence/mod.rs`) dispatches to either `SqliteStore` or `PostgresStore` based on the database URL prefix:
|
||||
|
||||
- `sqlite:*` -- uses `sqlx::SqlitePool` (1 connection for in-memory, 5 for file-based).
|
||||
- `postgres://` or `postgresql://` -- uses `sqlx::PgPool` (max 10 connections).
|
||||
|
||||
Both backends auto-run migrations on connect from `crates/navigator-server/migrations/{sqlite,postgres}/`.
|
||||
Both backends auto-run migrations on connect from `crates/openshell-server/migrations/{sqlite,postgres}/`.
|
||||
|
||||
### Schema
|
||||
|
||||
@@ -476,9 +476,9 @@ The `generate_name()` function produces random 6-character lowercase alphabetic
|
||||
|
||||
### Deployment Storage
|
||||
|
||||
The gateway runs as a Kubernetes **StatefulSet** with a `volumeClaimTemplate` that provisions a 1Gi `ReadWriteOnce` PersistentVolumeClaim mounted at `/var/navigator`. On k3s clusters this uses the built-in `local-path-provisioner` StorageClass (the cluster default). The SQLite database file at `/var/navigator/navigator.db` survives pod restarts and rescheduling.
|
||||
The gateway runs as a Kubernetes **StatefulSet** with a `volumeClaimTemplate` that provisions a 1Gi `ReadWriteOnce` PersistentVolumeClaim mounted at `/var/openshell`. On k3s clusters this uses the built-in `local-path-provisioner` StorageClass (the cluster default). The SQLite database file at `/var/openshell/openshell.db` survives pod restarts and rescheduling.
|
||||
|
||||
The Helm chart template is at `deploy/helm/navigator/templates/statefulset.yaml`.
|
||||
The Helm chart template is at `deploy/helm/openshell/templates/statefulset.yaml`.
|
||||
|
||||
### CRUD Semantics
|
||||
|
||||
@@ -490,15 +490,15 @@ The Helm chart template is at `deploy/helm/navigator/templates/statefulset.yaml`
|
||||
|
||||
### Sandbox CRD Management
|
||||
|
||||
`SandboxClient` (`crates/navigator-server/src/sandbox/mod.rs`) manages `agents.x-k8s.io/v1alpha1/Sandbox` CRDs.
|
||||
`SandboxClient` (`crates/openshell-server/src/sandbox/mod.rs`) manages `agents.x-k8s.io/v1alpha1/Sandbox` CRDs.
|
||||
|
||||
- **Create**: Translates a `Sandbox` proto into a Kubernetes `DynamicObject` with labels (`navigator.ai/sandbox-id`, `navigator.ai/managed-by: navigator`) and a spec that includes the pod template, environment variables, and gateway-required env vars (`OPENSHELL_SANDBOX_ID`, `OPENSHELL_ENDPOINT`, `OPENSHELL_SSH_LISTEN_ADDR`, etc.).
|
||||
- **Create**: Translates a `Sandbox` proto into a Kubernetes `DynamicObject` with labels (`openshell.ai/sandbox-id`, `openshell.ai/managed-by: openshell`) and a spec that includes the pod template, environment variables, and gateway-required env vars (`OPENSHELL_SANDBOX_ID`, `OPENSHELL_ENDPOINT`, `OPENSHELL_SSH_LISTEN_ADDR`, etc.).
|
||||
- **Delete**: Calls the Kubernetes API to delete the CRD by name. Returns `false` if already gone (404).
|
||||
- **Pod IP resolution**: `agent_pod_ip()` fetches the agent pod and reads `status.podIP`.
|
||||
|
||||
### Sandbox Watcher
|
||||
|
||||
`spawn_sandbox_watcher()` (`crates/navigator-server/src/sandbox/mod.rs`) runs a Kubernetes watcher on `Sandbox` CRDs and processes three event types:
|
||||
`spawn_sandbox_watcher()` (`crates/openshell-server/src/sandbox/mod.rs`) runs a Kubernetes watcher on `Sandbox` CRDs and processes three event types:
|
||||
|
||||
- **Applied**: Extracts the sandbox ID from labels (or falls back to name prefix stripping), reads the CRD status, derives the phase, and upserts the sandbox record in the store. Notifies the watch bus.
|
||||
- **Deleted**: Removes the sandbox record from the store and the index. Notifies the watch bus.
|
||||
@@ -521,7 +521,7 @@ All other `Ready=False` reasons are treated as terminal failures (`Error` phase)
|
||||
|
||||
### Kubernetes Event Tailer
|
||||
|
||||
`spawn_kube_event_tailer()` (`crates/navigator-server/src/sandbox_watch.rs`) watches all Kubernetes `Event` objects in the sandbox namespace and correlates them to sandbox IDs using `SandboxIndex`:
|
||||
`spawn_kube_event_tailer()` (`crates/openshell-server/src/sandbox_watch.rs`) watches all Kubernetes `Event` objects in the sandbox namespace and correlates them to sandbox IDs using `SandboxIndex`:
|
||||
|
||||
- Events involving `kind: Sandbox` are correlated by sandbox name.
|
||||
- Events involving `kind: Pod` are correlated by agent pod name.
|
||||
@@ -531,7 +531,7 @@ Matched events are published to the `PlatformEventBus` as `SandboxStreamEvent::E
|
||||
|
||||
## Sandbox Index
|
||||
|
||||
`SandboxIndex` (`crates/navigator-server/src/sandbox_index.rs`) maintains two in-memory maps protected by an `RwLock`:
|
||||
`SandboxIndex` (`crates/openshell-server/src/sandbox_index.rs`) maintains two in-memory maps protected by an `RwLock`:
|
||||
|
||||
- `sandbox_name_to_id: HashMap<String, String>`
|
||||
- `agent_pod_to_id: HashMap<String, String>`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Inference routing gives sandboxed agents access to LLM APIs through a single, explicit endpoint: `inference.local`. There is no implicit catch-all interception for arbitrary hosts. Requests are routed only when the process targets `inference.local` via HTTPS and the request matches a supported inference API pattern.
|
||||
|
||||
All inference execution happens locally inside the sandbox via the `navigator-router` crate. The gateway is control-plane only: it stores configuration and delivers resolved route bundles to sandboxes over gRPC.
|
||||
All inference execution happens locally inside the sandbox via the `openshell-router` crate. The gateway is control-plane only: it stores configuration and delivers resolved route bundles to sandboxes over gRPC.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
@@ -10,7 +10,7 @@ All inference execution happens locally inside the sandbox via the `navigator-ro
|
||||
sequenceDiagram
|
||||
participant Agent as Agent Process
|
||||
participant Proxy as Sandbox Proxy
|
||||
participant Router as navigator-router
|
||||
participant Router as openshell-router
|
||||
participant Gateway as Gateway (gRPC)
|
||||
participant Backend as Inference Backend
|
||||
|
||||
@@ -37,7 +37,7 @@ sequenceDiagram
|
||||
|
||||
## Provider Profiles
|
||||
|
||||
File: `crates/navigator-core/src/inference.rs`
|
||||
File: `crates/openshell-core/src/inference.rs`
|
||||
|
||||
`InferenceProviderProfile` is the single source of truth for provider-specific inference knowledge: default endpoint, supported protocols, credential key lookup order, auth header style, and default headers.
|
||||
|
||||
@@ -55,7 +55,7 @@ Unknown provider types return `None` from `profile_for()` and default to `Bearer
|
||||
|
||||
## Control Plane (Gateway)
|
||||
|
||||
File: `crates/navigator-server/src/inference.rs`
|
||||
File: `crates/openshell-server/src/inference.rs`
|
||||
|
||||
The gateway implements the `Inference` gRPC service defined in `proto/inference.proto`.
|
||||
|
||||
@@ -100,10 +100,10 @@ Key messages:
|
||||
|
||||
Files:
|
||||
|
||||
- `crates/navigator-sandbox/src/proxy.rs` -- proxy interception, inference context, request routing
|
||||
- `crates/navigator-sandbox/src/l7/inference.rs` -- pattern detection, HTTP parsing, response formatting
|
||||
- `crates/navigator-sandbox/src/lib.rs` -- inference context initialization, route refresh
|
||||
- `crates/navigator-sandbox/src/grpc_client.rs` -- `fetch_inference_bundle()`
|
||||
- `crates/openshell-sandbox/src/proxy.rs` -- proxy interception, inference context, request routing
|
||||
- `crates/openshell-sandbox/src/l7/inference.rs` -- pattern detection, HTTP parsing, response formatting
|
||||
- `crates/openshell-sandbox/src/lib.rs` -- inference context initialization, route refresh
|
||||
- `crates/openshell-sandbox/src/grpc_client.rs` -- `fetch_inference_bundle()`
|
||||
|
||||
In cluster mode, the sandbox starts a background refresh loop as soon as the inference context is created. The loop polls the gateway every 5 seconds by default (`OPENSHELL_ROUTE_REFRESH_INTERVAL_SECS` override) and uses the bundle revision hash to skip no-op cache writes.
|
||||
|
||||
@@ -122,7 +122,7 @@ When a `CONNECT inference.local:443` arrives:
|
||||
|
||||
### Request classification
|
||||
|
||||
File: `crates/navigator-sandbox/src/l7/inference.rs` -- `default_patterns()` and `detect_inference_pattern()`
|
||||
File: `crates/openshell-sandbox/src/l7/inference.rs` -- `default_patterns()` and `detect_inference_pattern()`
|
||||
|
||||
Supported built-in patterns:
|
||||
|
||||
@@ -148,15 +148,15 @@ If no pattern matches, the proxy returns `403 Forbidden` with `{"error": "connec
|
||||
|
||||
### Bundle-to-route conversion
|
||||
|
||||
`bundle_to_resolved_routes()` in `lib.rs` converts proto `ResolvedRoute` messages to router `ResolvedRoute` structs. Auth header style and default headers are derived from `provider_type` using `navigator_core::inference::auth_for_provider_type()`.
|
||||
`bundle_to_resolved_routes()` in `lib.rs` converts proto `ResolvedRoute` messages to router `ResolvedRoute` structs. Auth header style and default headers are derived from `provider_type` using `openshell_core::inference::auth_for_provider_type()`.
|
||||
|
||||
## Router Behavior
|
||||
|
||||
Files:
|
||||
|
||||
- `crates/navigator-router/src/lib.rs` -- `Router`, `proxy_with_candidates()`, `proxy_with_candidates_streaming()`
|
||||
- `crates/navigator-router/src/backend.rs` -- `proxy_to_backend()`, `proxy_to_backend_streaming()`, URL construction
|
||||
- `crates/navigator-router/src/config.rs` -- `RouteConfig`, `ResolvedRoute`, YAML loading
|
||||
- `crates/openshell-router/src/lib.rs` -- `Router`, `proxy_with_candidates()`, `proxy_with_candidates_streaming()`
|
||||
- `crates/openshell-router/src/backend.rs` -- `proxy_to_backend()`, `proxy_to_backend_streaming()`, URL construction
|
||||
- `crates/openshell-router/src/config.rs` -- `RouteConfig`, `ResolvedRoute`, YAML loading
|
||||
|
||||
### Route selection
|
||||
|
||||
@@ -204,9 +204,9 @@ This eliminates full-body buffering for streaming responses (SSE). Time-to-first
|
||||
|
||||
### Mock routes
|
||||
|
||||
File: `crates/navigator-router/src/mock.rs`
|
||||
File: `crates/openshell-router/src/mock.rs`
|
||||
|
||||
Routes with `mock://` scheme endpoints return canned responses without making HTTP requests. Mock responses are protocol-aware (OpenAI chat completion, OpenAI completion, Anthropic messages, or generic JSON). Mock routes include an `x-navigator-mock: true` response header.
|
||||
Routes with `mock://` scheme endpoints return canned responses without making HTTP requests. Mock responses are protocol-aware (OpenAI chat completion, OpenAI completion, Anthropic messages, or generic JSON). Mock routes include an `x-openshell-mock: true` response header.
|
||||
|
||||
### HTTP client
|
||||
|
||||
@@ -214,7 +214,7 @@ The router uses a `reqwest::Client` with a 60-second timeout. Timeouts and conne
|
||||
|
||||
## Standalone Route File
|
||||
|
||||
File: `crates/navigator-router/src/config.rs`
|
||||
File: `crates/openshell-router/src/config.rs`
|
||||
|
||||
Standalone sandboxes can load static routes from YAML via `--inference-routes`:
|
||||
|
||||
@@ -307,14 +307,14 @@ The `--provider` flag references a provider record name (not a provider type). T
|
||||
|
||||
Files:
|
||||
|
||||
- `crates/navigator-providers/src/lib.rs` -- `ProviderRegistry`, `ProviderPlugin` trait
|
||||
- `crates/navigator-providers/src/providers/openai.rs` -- `OpenaiProvider`
|
||||
- `crates/navigator-providers/src/providers/anthropic.rs` -- `AnthropicProvider`
|
||||
- `crates/navigator-providers/src/providers/nvidia.rs` -- `NvidiaProvider`
|
||||
- `crates/openshell-providers/src/lib.rs` -- `ProviderRegistry`, `ProviderPlugin` trait
|
||||
- `crates/openshell-providers/src/providers/openai.rs` -- `OpenaiProvider`
|
||||
- `crates/openshell-providers/src/providers/anthropic.rs` -- `AnthropicProvider`
|
||||
- `crates/openshell-providers/src/providers/nvidia.rs` -- `NvidiaProvider`
|
||||
|
||||
Provider discovery and inference routing are separate concerns:
|
||||
|
||||
- `ProviderPlugin` (in `navigator-providers`) handles credential *discovery* -- scanning environment variables to find API keys.
|
||||
- `InferenceProviderProfile` (in `navigator-core`) handles how to *use* discovered credentials to make inference API calls.
|
||||
- `ProviderPlugin` (in `openshell-providers`) handles credential *discovery* -- scanning environment variables to find API keys.
|
||||
- `InferenceProviderProfile` (in `openshell-core`) handles how to *use* discovered credentials to make inference API calls.
|
||||
|
||||
The `openai`, `anthropic`, and `nvidia` provider plugins each discover credentials from their canonical environment variable (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `NVIDIA_API_KEY`). These credentials are stored in provider records and looked up by the gateway at bundle resolution time.
|
||||
|
||||
@@ -25,7 +25,7 @@ The key architectural decision: **all analysis runs sandbox-side**. The gateway
|
||||
|
||||
### Denial Aggregator (Sandbox Side)
|
||||
|
||||
The `DenialAggregator` (`crates/navigator-sandbox/src/denial_aggregator.rs`) runs as a background tokio task inside the sandbox supervisor. It:
|
||||
The `DenialAggregator` (`crates/openshell-sandbox/src/denial_aggregator.rs`) runs as a background tokio task inside the sandbox supervisor. It:
|
||||
|
||||
1. Receives `DenialEvent` structs from the proxy via an unbounded MPSC channel
|
||||
2. Deduplicates events by `(host, port, binary)` key with running counters
|
||||
@@ -48,7 +48,7 @@ L7 (per-request) denials from `l7/relay.rs` are captured via tracing in the curr
|
||||
|
||||
### Mechanistic Mapper (Sandbox Side)
|
||||
|
||||
The `mechanistic_mapper` module (`crates/navigator-sandbox/src/mechanistic_mapper.rs`) generates draft policy recommendations deterministically, without requiring an LLM:
|
||||
The `mechanistic_mapper` module (`crates/openshell-sandbox/src/mechanistic_mapper.rs`) generates draft policy recommendations deterministically, without requiring an LLM:
|
||||
|
||||
1. Groups denial summaries by `(host, port, binary)` — one proposal per unique triple
|
||||
2. For each group, generates a `NetworkPolicyRule` allowing that endpoint for that binary
|
||||
@@ -65,7 +65,7 @@ The mapper runs in `flush_proposals_to_gateway` after the aggregator drains. It
|
||||
|
||||
### Gateway: Validate and Persist
|
||||
|
||||
The gateway's `SubmitPolicyAnalysis` handler (`crates/navigator-server/src/grpc.rs`) is deliberately thin:
|
||||
The gateway's `SubmitPolicyAnalysis` handler (`crates/openshell-server/src/grpc.rs`) is deliberately thin:
|
||||
|
||||
1. Receives proposed chunks and denial summaries from the sandbox
|
||||
2. Validates each chunk (rejects missing `rule_name` or `proposed_rule`)
|
||||
@@ -106,7 +106,7 @@ CREATE UNIQUE INDEX idx_draft_chunks_endpoint
|
||||
WHERE status IN ('pending', 'approved', 'rejected');
|
||||
```
|
||||
|
||||
Schema lives in `crates/navigator-server/migrations/{sqlite,postgres}/003_create_policy_recommendations.sql`.
|
||||
Schema lives in `crates/openshell-server/migrations/{sqlite,postgres}/003_create_policy_recommendations.sql`.
|
||||
|
||||
### Per-Binary Granularity
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ There is also a gateway-side `ExecSandbox` gRPC RPC that executes commands insid
|
||||
|
||||
### CLI SSH module
|
||||
|
||||
**File**: `crates/navigator-cli/src/ssh.rs`
|
||||
**File**: `crates/openshell-cli/src/ssh.rs`
|
||||
|
||||
Contains the client-side SSH and editor-launch helpers for sandbox connectivity:
|
||||
|
||||
@@ -28,17 +28,17 @@ Contains the client-side SSH and editor-launch helpers for sandbox connectivity:
|
||||
`~/.ssh/config` and maintain generated `Host openshell-<name>` blocks in a
|
||||
separate OpenShell-owned config file for editor workflows
|
||||
|
||||
These are re-exported from `crates/navigator-cli/src/run.rs` for backward compatibility.
|
||||
These are re-exported from `crates/openshell-cli/src/run.rs` for backward compatibility.
|
||||
|
||||
### CLI `ssh-proxy` subcommand
|
||||
|
||||
**File**: `crates/navigator-cli/src/main.rs` (line ~139, `Commands::SshProxy`)
|
||||
**File**: `crates/openshell-cli/src/main.rs` (line ~139, `Commands::SshProxy`)
|
||||
|
||||
A top-level CLI subcommand (`ssh-proxy`) that the SSH `ProxyCommand` invokes. It receives `--gateway`, `--sandbox-id`, and `--token` flags, then delegates to `sandbox_ssh_proxy()`. This process has no TTY of its own -- it pipes stdin/stdout directly to the gateway tunnel.
|
||||
|
||||
### gRPC session bootstrap
|
||||
|
||||
**Files**: `proto/navigator.proto`, `crates/navigator-server/src/grpc.rs`
|
||||
**Files**: `proto/openshell.proto`, `crates/openshell-server/src/grpc.rs`
|
||||
|
||||
Two RPCs manage SSH session tokens:
|
||||
|
||||
@@ -47,7 +47,7 @@ Two RPCs manage SSH session tokens:
|
||||
|
||||
### Gateway tunnel handler
|
||||
|
||||
**File**: `crates/navigator-server/src/ssh_tunnel.rs`
|
||||
**File**: `crates/openshell-server/src/ssh_tunnel.rs`
|
||||
|
||||
An Axum route at `/connect/ssh` on the shared gateway port. Handles HTTP CONNECT requests by:
|
||||
1. Validating the session token and sandbox readiness
|
||||
@@ -58,13 +58,13 @@ An Axum route at `/connect/ssh` on the shared gateway port. Handles HTTP CONNECT
|
||||
|
||||
### Gateway multiplexing
|
||||
|
||||
**File**: `crates/navigator-server/src/multiplex.rs`
|
||||
**File**: `crates/openshell-server/src/multiplex.rs`
|
||||
|
||||
The gateway runs a single listener that multiplexes gRPC and HTTP on the same port. `MultiplexedService` routes based on the `content-type` header: requests with `application/grpc` go to the gRPC router; all others (including HTTP CONNECT) go to the HTTP router. The HTTP router (`crates/navigator-server/src/http.rs`) merges health endpoints with the SSH tunnel router.
|
||||
The gateway runs a single listener that multiplexes gRPC and HTTP on the same port. `MultiplexedService` routes based on the `content-type` header: requests with `application/grpc` go to the gRPC router; all others (including HTTP CONNECT) go to the HTTP router. The HTTP router (`crates/openshell-server/src/http.rs`) merges health endpoints with the SSH tunnel router.
|
||||
|
||||
### Sandbox SSH daemon
|
||||
|
||||
**File**: `crates/navigator-sandbox/src/ssh.rs`
|
||||
**File**: `crates/openshell-sandbox/src/ssh.rs`
|
||||
|
||||
An embedded SSH server built on `russh` that runs inside each sandbox pod. It:
|
||||
- Generates an ephemeral Ed25519 host key on startup (no persistent key material)
|
||||
@@ -75,7 +75,7 @@ An embedded SSH server built on `russh` that runs inside each sandbox pod. It:
|
||||
|
||||
### Gateway-side exec (gRPC)
|
||||
|
||||
**File**: `crates/navigator-server/src/grpc.rs` (functions `stream_exec_over_ssh`, `start_single_use_ssh_proxy`, `run_exec_with_russh`)
|
||||
**File**: `crates/openshell-server/src/grpc.rs` (functions `stream_exec_over_ssh`, `start_single_use_ssh_proxy`, `run_exec_with_russh`)
|
||||
|
||||
The `ExecSandbox` gRPC RPC provides programmatic command execution without requiring an external SSH client. It:
|
||||
1. Spins up a single-use local TCP proxy that performs the NSSH1 handshake
|
||||
@@ -125,8 +125,8 @@ sequenceDiagram
|
||||
|
||||
**Code trace for `sandbox connect`:**
|
||||
|
||||
1. `crates/navigator-cli/src/main.rs` -- `SandboxCommands::Connect { name }` dispatches to `run::sandbox_connect()`
|
||||
2. `crates/navigator-cli/src/ssh.rs` -- `sandbox_connect()` calls `ssh_session_config()`:
|
||||
1. `crates/openshell-cli/src/main.rs` -- `SandboxCommands::Connect { name }` dispatches to `run::sandbox_connect()`
|
||||
2. `crates/openshell-cli/src/ssh.rs` -- `sandbox_connect()` calls `ssh_session_config()`:
|
||||
- Resolves sandbox name to ID via `GetSandbox` gRPC
|
||||
- Creates an SSH session via `CreateSshSession` gRPC
|
||||
- Builds a `ProxyCommand` string: `<openshell-exe> ssh-proxy --gateway <url> --sandbox-id <id> --token <token>`
|
||||
@@ -139,7 +139,7 @@ sequenceDiagram
|
||||
- `sandbox` as the SSH user
|
||||
4. If stdin is a terminal (interactive), the CLI calls `exec()` (Unix) to replace itself with the `ssh` process, giving SSH direct terminal ownership. Otherwise it spawns and waits.
|
||||
5. When SSH starts, it spawns the `ssh-proxy` subprocess as its `ProxyCommand`.
|
||||
6. `crates/navigator-cli/src/ssh.rs` -- `sandbox_ssh_proxy()`:
|
||||
6. `crates/openshell-cli/src/ssh.rs` -- `sandbox_ssh_proxy()`:
|
||||
- Parses the gateway URL, connects via TCP (plain) or TLS (mTLS)
|
||||
- Sends a raw HTTP CONNECT request with `X-Sandbox-Id` and `X-Sandbox-Token` headers
|
||||
- Reads the response status line; proceeds if 200
|
||||
@@ -177,7 +177,7 @@ on the host are forwarded to `127.0.0.1:<port>` inside the sandbox.
|
||||
|
||||
#### TUI
|
||||
|
||||
The TUI (`crates/navigator-tui/`) supports port forwarding through the create sandbox modal. Users
|
||||
The TUI (`crates/openshell-tui/`) supports port forwarding through the create sandbox modal. Users
|
||||
specify comma-separated ports in the **Ports** field. After sandbox creation:
|
||||
|
||||
1. The TUI polls for `Ready` state (up to 30 attempts at 2-second intervals).
|
||||
@@ -193,7 +193,7 @@ request. PID tracking uses the same `~/.config/openshell/forwards/` directory as
|
||||
|
||||
#### Shared forward module
|
||||
|
||||
**File**: `crates/navigator-core/src/forward.rs`
|
||||
**File**: `crates/openshell-core/src/forward.rs`
|
||||
|
||||
Port forwarding PID management and SSH utility functions are shared between the CLI and TUI:
|
||||
|
||||
@@ -207,7 +207,7 @@ Port forwarding PID management and SSH utility functions are shared between the
|
||||
|
||||
#### Supervisor `direct-tcpip` handling
|
||||
|
||||
The sandbox SSH server (`crates/navigator-sandbox/src/ssh.rs`) implements
|
||||
The sandbox SSH server (`crates/openshell-sandbox/src/ssh.rs`) implements
|
||||
`channel_open_direct_tcpip` from the russh `Handler` trait.
|
||||
|
||||
- **Loopback-only**: only `127.0.0.1`, `localhost`, and `::1` destinations are accepted.
|
||||
@@ -281,7 +281,7 @@ If `timeout_seconds > 0`, the exec is wrapped in `tokio::time::timeout`. On time
|
||||
|
||||
File sync uses **tar-over-SSH**: the CLI streams a tar archive through the existing SSH proxy tunnel. No external dependencies (like `rsync`) are required on the client side. The sandbox image provides GNU `tar` for extraction.
|
||||
|
||||
**Files**: `crates/navigator-cli/src/ssh.rs`, `crates/navigator-cli/src/run.rs`
|
||||
**Files**: `crates/openshell-cli/src/ssh.rs`, `crates/openshell-cli/src/run.rs`
|
||||
|
||||
#### `sandbox create --upload`
|
||||
|
||||
@@ -343,7 +343,7 @@ NSSH1 <token> <timestamp> <nonce> <hmac>\n
|
||||
|
||||
### Validation (sandbox side)
|
||||
|
||||
**File**: `crates/navigator-sandbox/src/ssh.rs` -- `verify_preface()`
|
||||
**File**: `crates/openshell-sandbox/src/ssh.rs` -- `verify_preface()`
|
||||
|
||||
1. Split line on whitespace; reject if not exactly 5 fields or magic is not `NSSH1`
|
||||
2. Parse timestamp; compute absolute clock skew `|now - timestamp|`
|
||||
@@ -360,7 +360,7 @@ The SSH server maintains a per-process `NonceCache` (`HashMap<String, Instant>`
|
||||
|
||||
### HMAC computation
|
||||
|
||||
Both the gateway (`crates/navigator-server/src/ssh_tunnel.rs` -- `build_preface()`) and the gRPC exec path (`crates/navigator-server/src/grpc.rs` -- `build_preface()`) use identical logic:
|
||||
Both the gateway (`crates/openshell-server/src/ssh_tunnel.rs` -- `build_preface()`) and the gRPC exec path (`crates/openshell-server/src/grpc.rs` -- `build_preface()`) use identical logic:
|
||||
|
||||
```rust
|
||||
let payload = format!("{token}|{timestamp}|{nonce}");
|
||||
@@ -376,7 +376,7 @@ Both sides cap the preface line at 1024 bytes and stop reading at `\n` or EOF. T
|
||||
|
||||
### Startup
|
||||
|
||||
`run_ssh_server()` in `crates/navigator-sandbox/src/ssh.rs`:
|
||||
`run_ssh_server()` in `crates/openshell-sandbox/src/ssh.rs`:
|
||||
|
||||
1. Generates an ephemeral Ed25519 host key using `OsRng`
|
||||
2. Configures `russh::server::Config` with 1-second auth rejection delay
|
||||
@@ -440,7 +440,7 @@ The reader-done synchronization ensures correct SSH protocol ordering: data -> E
|
||||
|
||||
The gateway and the gRPC exec path both resolve the sandbox's network address using the same logic.
|
||||
|
||||
**File**: `crates/navigator-server/src/ssh_tunnel.rs` (gateway), `crates/navigator-server/src/grpc.rs` (exec)
|
||||
**File**: `crates/openshell-server/src/ssh_tunnel.rs` (gateway), `crates/openshell-server/src/grpc.rs` (exec)
|
||||
|
||||
Resolution order:
|
||||
1. If the sandbox has a `status.agent_pod` field, resolve the pod IP via the Kubernetes API (`agent_pod_ip()`)
|
||||
@@ -456,7 +456,7 @@ The `ConnectTarget` enum in `ssh_tunnel.rs` encodes both cases:
|
||||
|
||||
### CreateSshSession
|
||||
|
||||
**Proto**: `proto/navigator.proto` -- `CreateSshSessionRequest` / `CreateSshSessionResponse`
|
||||
**Proto**: `proto/openshell.proto` -- `CreateSshSessionRequest` / `CreateSshSessionResponse`
|
||||
|
||||
Request:
|
||||
- `sandbox_id` (string) -- the sandbox to connect to
|
||||
@@ -480,7 +480,7 @@ Response:
|
||||
|
||||
### SshSession persistence
|
||||
|
||||
**Proto**: `proto/navigator.proto` -- `SshSession` message
|
||||
**Proto**: `proto/openshell.proto` -- `SshSession` message
|
||||
|
||||
Stored in the gateway's persistence layer (SQLite or Postgres) as object type `"ssh_session"`:
|
||||
|
||||
@@ -495,7 +495,7 @@ Stored in the gateway's persistence layer (SQLite or Postgres) as object type `"
|
||||
|
||||
### ExecSandbox
|
||||
|
||||
**Proto**: `proto/navigator.proto` -- `ExecSandboxRequest` / `ExecSandboxEvent`
|
||||
**Proto**: `proto/openshell.proto` -- `ExecSandboxRequest` / `ExecSandboxEvent`
|
||||
|
||||
Request:
|
||||
- `sandbox_id` (string)
|
||||
@@ -514,13 +514,13 @@ The gateway builds the remote command by shell-escaping arguments, prepending so
|
||||
|
||||
## Gateway Loopback Resolution
|
||||
|
||||
**File**: `crates/navigator-core/src/forward.rs` -- `resolve_ssh_gateway()`
|
||||
**File**: `crates/openshell-core/src/forward.rs` -- `resolve_ssh_gateway()`
|
||||
|
||||
When the gateway returns a loopback address (`127.0.0.1`, `0.0.0.0`, `localhost`, or `::1`), the client overrides it with the host from the cluster endpoint URL. This handles the common case where the gateway defaults to `127.0.0.1` but the cluster is running on a remote machine.
|
||||
|
||||
The override only applies if the cluster endpoint itself is not also a loopback address. If both are loopback, the original address is kept.
|
||||
|
||||
This function is shared between the CLI and TUI via the `navigator-core::forward` module.
|
||||
This function is shared between the CLI and TUI via the `openshell-core::forward` module.
|
||||
|
||||
## Authentication and Security Model
|
||||
|
||||
@@ -550,7 +550,7 @@ The sandbox generates a fresh Ed25519 host key on every startup. The CLI disable
|
||||
|
||||
### Gateway configuration
|
||||
|
||||
**File**: `crates/navigator-core/src/config.rs` -- `Config` struct
|
||||
**File**: `crates/openshell-core/src/config.rs` -- `Config` struct
|
||||
|
||||
| Field | Default | Description |
|
||||
|----------------------------|------------------|-------------|
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sandbox Custom Containers
|
||||
|
||||
Users can run `openshell sandbox create --from <source>` to launch a sandbox with a custom container image while keeping the `navigator-sandbox` process supervisor in control.
|
||||
Users can run `openshell sandbox create --from <source>` to launch a sandbox with a custom container image while keeping the `openshell-sandbox` process supervisor in control.
|
||||
|
||||
## The `--from` Flag
|
||||
|
||||
@@ -34,12 +34,12 @@ When `--from` points to a Dockerfile or directory, the CLI:
|
||||
|
||||
## How It Works
|
||||
|
||||
The supervisor binary (`navigator-sandbox`) is **always side-loaded** from the k3s node filesystem via a read-only `hostPath` volume. It is never baked into sandbox images. This applies to all sandbox pods — whether using the default community base image, a custom image, or a user-built Dockerfile.
|
||||
The supervisor binary (`openshell-sandbox`) is **always side-loaded** from the k3s node filesystem via a read-only `hostPath` volume. It is never baked into sandbox images. This applies to all sandbox pods — whether using the default community base image, a custom image, or a user-built Dockerfile.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph node["K3s Node"]
|
||||
bin["/opt/openshell/bin/navigator-sandbox
|
||||
bin["/opt/openshell/bin/openshell-sandbox
|
||||
(built into cluster image, updatable via docker cp)"]
|
||||
end
|
||||
|
||||
@@ -48,8 +48,8 @@ flowchart TB
|
||||
subgraph pod["Pod"]
|
||||
subgraph agent["Agent Container"]
|
||||
agent_desc["Image: community base or custom image
|
||||
Command: /opt/navigator/bin/navigator-sandbox
|
||||
Volume: /opt/navigator/bin (ro hostPath)
|
||||
Command: /opt/openshell/bin/openshell-sandbox
|
||||
Volume: /opt/openshell/bin (ro hostPath)
|
||||
Env: OPENSHELL_SANDBOX_ID, OPENSHELL_ENDPOINT, ...
|
||||
Caps: SYS_ADMIN, NET_ADMIN, SYS_PTRACE"]
|
||||
end
|
||||
@@ -58,9 +58,9 @@ flowchart TB
|
||||
|
||||
The server applies these transforms to every sandbox pod template (`sandbox/mod.rs`):
|
||||
|
||||
1. Adds a `hostPath` volume named `navigator-supervisor-bin` pointing to `/opt/openshell/bin` on the node.
|
||||
2. Mounts it read-only at `/opt/navigator/bin` in the agent container.
|
||||
3. Overrides the agent container's `command` to `/opt/navigator/bin/navigator-sandbox`.
|
||||
1. Adds a `hostPath` volume named `openshell-supervisor-bin` pointing to `/opt/openshell/bin` on the node.
|
||||
2. Mounts it read-only at `/opt/openshell/bin` in the agent container.
|
||||
3. Overrides the agent container's `command` to `/opt/openshell/bin/openshell-sandbox`.
|
||||
4. Sets `runAsUser: 0` so the supervisor has root privileges for namespace creation, proxy setup, and Landlock/seccomp.
|
||||
|
||||
These transforms apply to both generated templates and user-provided `pod_template` overrides.
|
||||
@@ -90,9 +90,9 @@ openshell sandbox create --from ./my-sandbox/ # directory with Dockerfile
|
||||
|
||||
## Supervisor Behavior in Custom Images
|
||||
|
||||
The `navigator-sandbox` supervisor adapts to arbitrary environments:
|
||||
The `openshell-sandbox` supervisor adapts to arbitrary environments:
|
||||
|
||||
- **Log file fallback**: Attempts to open `/var/log/navigator.log` for append; silently falls back to stdout-only logging if the path is not writable.
|
||||
- **Log file fallback**: Attempts to open `/var/log/openshell.log` for append; silently falls back to stdout-only logging if the path is not writable.
|
||||
- **Command resolution**: Executes the command from CLI args, then the `OPENSHELL_SANDBOX_COMMAND` env var (set to `sleep infinity` by the server), then `/bin/bash` as a last resort.
|
||||
- **Network namespace**: Requires successful namespace creation for proxy isolation; startup fails in proxy mode if required capabilities (`CAP_NET_ADMIN`, `CAP_SYS_ADMIN`) or `iproute2` are unavailable.
|
||||
|
||||
@@ -106,13 +106,13 @@ The `navigator-sandbox` supervisor adapts to arbitrary environments:
|
||||
| `OPENSHELL_COMMUNITY_REGISTRY` env var | Allows organizations to host their own community sandbox registry |
|
||||
| hostPath side-load | Supervisor binary lives on the node filesystem — no init container, no emptyDir, no extra image pull. Faster pod startup. |
|
||||
| Read-only mount in agent | Supervisor binary cannot be tampered with by the workload |
|
||||
| Command override | Ensures `navigator-sandbox` is the entrypoint regardless of the image's default CMD |
|
||||
| Command override | Ensures `openshell-sandbox` is the entrypoint regardless of the image's default CMD |
|
||||
| Clear `run_as_user/group` for custom images | Prevents startup failure when the image lacks the default `sandbox` user |
|
||||
| Non-fatal log file init | `/var/log/navigator.log` may be unwritable in arbitrary images; falls back to stdout |
|
||||
| Non-fatal log file init | `/var/log/openshell.log` may be unwritable in arbitrary images; falls back to stdout |
|
||||
| `docker save` / `ctr import` for push | Avoids requiring a registry for local dev; images land directly in the k3s containerd store |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Distroless / `FROM scratch` images are not supported (the supervisor needs glibc and `/proc`)
|
||||
- Missing `iproute2` (or required capabilities) blocks startup in proxy mode because namespace isolation is mandatory
|
||||
- The supervisor binary must be present on the k3s node at `/opt/openshell/bin/navigator-sandbox` (embedded in the cluster image at build time)
|
||||
- The supervisor binary must be present on the k3s node at `/opt/openshell/bin/openshell-sandbox` (embedded in the cluster image at build time)
|
||||
|
||||
@@ -36,7 +36,7 @@ Provider is defined in `proto/datamodel.proto`:
|
||||
- `credentials`: `map<string, string>` for secret values
|
||||
- `config`: `map<string, string>` for non-secret settings
|
||||
|
||||
The gRPC surface is defined in `proto/navigator.proto`:
|
||||
The gRPC surface is defined in `proto/openshell.proto`:
|
||||
|
||||
- `CreateProvider`
|
||||
- `GetProvider`
|
||||
@@ -46,29 +46,29 @@ The gRPC surface is defined in `proto/navigator.proto`:
|
||||
|
||||
## Components
|
||||
|
||||
- `crates/navigator-providers`
|
||||
- `crates/openshell-providers`
|
||||
- canonical provider type normalization and command detection,
|
||||
- provider registry and per-provider discovery plugins,
|
||||
- shared discovery engine and context abstraction for testability.
|
||||
- `crates/navigator-cli`
|
||||
- `crates/openshell-cli`
|
||||
- `openshell provider ...` command handlers,
|
||||
- sandbox provider requirement resolution in `sandbox create`.
|
||||
- `crates/navigator-server` (gateway)
|
||||
- `crates/openshell-server` (gateway)
|
||||
- provider CRUD gRPC handlers,
|
||||
- `GetSandboxProviderEnvironment` handler resolves credentials at runtime,
|
||||
- persistence using `object_type = "provider"`.
|
||||
- `crates/navigator-sandbox`
|
||||
- `crates/openshell-sandbox`
|
||||
- sandbox supervisor fetches provider credentials via gRPC at startup,
|
||||
- injects placeholder env vars into entrypoint and SSH child processes,
|
||||
- resolves placeholders back to real secrets in the outbound proxy path.
|
||||
|
||||
## Provider Plugins
|
||||
|
||||
Each provider has its own module under `crates/navigator-providers/src/providers/`.
|
||||
Each provider has its own module under `crates/openshell-providers/src/providers/`.
|
||||
|
||||
### Trait Definition
|
||||
|
||||
`ProviderPlugin` (`crates/navigator-providers/src/lib.rs`):
|
||||
`ProviderPlugin` (`crates/openshell-providers/src/lib.rs`):
|
||||
|
||||
```rust
|
||||
pub trait ProviderPlugin: Send + Sync {
|
||||
@@ -181,7 +181,7 @@ Also supported:
|
||||
|
||||
`openshell sandbox create --provider gitlab -- claude`
|
||||
|
||||
Resolution logic (CLI side, `crates/navigator-cli/src/run.rs`):
|
||||
Resolution logic (CLI side, `crates/openshell-cli/src/run.rs`):
|
||||
|
||||
1. `detect_provider_from_command()` infers provider from command token after `--`
|
||||
(for example `claude`),
|
||||
@@ -194,7 +194,7 @@ Resolution logic (CLI side, `crates/navigator-cli/src/run.rs`):
|
||||
6. non-interactive mode fails with a clear missing-provider error,
|
||||
7. set resolved provider **names** in `SandboxSpec.providers`.
|
||||
|
||||
Gateway-side `create_sandbox()` (`crates/navigator-server/src/grpc.rs`):
|
||||
Gateway-side `create_sandbox()` (`crates/openshell-server/src/grpc.rs`):
|
||||
|
||||
1. validates all provider names exist by fetching each from the store (fail fast),
|
||||
2. creates the `Sandbox` object with `spec.providers` set,
|
||||
@@ -217,7 +217,7 @@ them at runtime via the `GetSandboxProviderEnvironment` gRPC call.
|
||||
|
||||
### Gateway-side: `resolve_provider_environment()`
|
||||
|
||||
`resolve_provider_environment()` (`crates/navigator-server/src/grpc.rs`) builds the
|
||||
`resolve_provider_environment()` (`crates/openshell-server/src/grpc.rs`) builds the
|
||||
environment map returned by `GetSandboxProviderEnvironment`:
|
||||
|
||||
1. for each provider name in `spec.providers`, fetch the provider from the store,
|
||||
@@ -235,11 +235,11 @@ Key behaviors:
|
||||
|
||||
### Sandbox Supervisor: Fetching Credentials
|
||||
|
||||
The sandbox pod runs `navigator-sandbox` (`crates/navigator-sandbox/src/main.rs`). On
|
||||
The sandbox pod runs `openshell-sandbox` (`crates/openshell-sandbox/src/main.rs`). On
|
||||
startup it receives `OPENSHELL_SANDBOX_ID` and `OPENSHELL_ENDPOINT` as environment
|
||||
variables (injected into the pod spec by the gateway's Kubernetes sandbox creation code).
|
||||
|
||||
In `run_sandbox()` (`crates/navigator-sandbox/src/lib.rs`):
|
||||
In `run_sandbox()` (`crates/openshell-sandbox/src/lib.rs`):
|
||||
|
||||
1. loads the sandbox policy via gRPC (`GetSandboxPolicy`),
|
||||
2. fetches provider credentials via gRPC (`GetSandboxProviderEnvironment`),
|
||||
@@ -259,7 +259,7 @@ The registry is threaded to the proxy so it can rewrite outbound headers.
|
||||
Provider placeholders are injected into child processes in two places, covering all
|
||||
process spawning paths inside the sandbox:
|
||||
|
||||
**1. Entrypoint process** (`crates/navigator-sandbox/src/process.rs`):
|
||||
**1. Entrypoint process** (`crates/openshell-sandbox/src/process.rs`):
|
||||
|
||||
```rust
|
||||
let mut cmd = Command::new(program);
|
||||
@@ -281,7 +281,7 @@ After provider env vars, proxy env vars (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY
|
||||
are also set when `NetworkMode` is `Proxy`. The child is then launched with namespace
|
||||
isolation, privilege dropping, seccomp, and Landlock restrictions via `pre_exec`.
|
||||
|
||||
**2. SSH shell sessions** (`crates/navigator-sandbox/src/ssh.rs`):
|
||||
**2. SSH shell sessions** (`crates/openshell-sandbox/src/ssh.rs`):
|
||||
|
||||
When a user connects via `openshell sandbox connect`, a PTY shell is spawned:
|
||||
|
||||
@@ -333,7 +333,7 @@ CLI: openshell sandbox create -- claude
|
||||
+-- Persists Sandbox with spec.providers = ["claude"]
|
||||
+-- Creates K8s Sandbox CRD (no credentials in pod spec)
|
||||
|
|
||||
K8s: pod starts navigator-sandbox binary
|
||||
K8s: pod starts openshell-sandbox binary
|
||||
+-- OPENSHELL_SANDBOX_ID and OPENSHELL_ENDPOINT set in pod env
|
||||
|
|
||||
Sandbox supervisor: run_sandbox()
|
||||
@@ -375,10 +375,10 @@ Providers are stored with `object_type = "provider"` in the shared object store.
|
||||
## Test Strategy
|
||||
|
||||
- Per-provider unit tests in each provider module.
|
||||
- Shared normalization/command-detection tests in `crates/navigator-providers/src/lib.rs`.
|
||||
- Shared normalization/command-detection tests in `crates/openshell-providers/src/lib.rs`.
|
||||
- Mocked discovery context tests cover env and path-based behavior.
|
||||
- CLI and gateway integration tests validate end-to-end RPC compatibility.
|
||||
- `resolve_provider_environment` unit tests in `crates/navigator-server/src/grpc.rs`.
|
||||
- `resolve_provider_environment` unit tests in `crates/openshell-server/src/grpc.rs`.
|
||||
- sandbox unit tests validate placeholder generation and header rewriting.
|
||||
- E2E sandbox tests verify placeholders are visible in child env, outbound proxy traffic
|
||||
is rewritten with the real secret, and the SSH handshake secret is absent from exec env.
|
||||
|
||||
+66
-66
@@ -4,7 +4,7 @@ The sandbox binary isolates a user-specified command inside a child process with
|
||||
|
||||
## Source File Index
|
||||
|
||||
All paths are relative to `crates/navigator-sandbox/src/`.
|
||||
All paths are relative to `crates/openshell-sandbox/src/`.
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
@@ -18,7 +18,7 @@ All paths are relative to `crates/navigator-sandbox/src/`.
|
||||
| `ssh.rs` | Embedded SSH server (`russh` crate) with PTY support and handshake verification |
|
||||
| `identity.rs` | `BinaryIdentityCache` -- SHA256 trust-on-first-use binary integrity |
|
||||
| `procfs.rs` | `/proc` filesystem reading for TCP peer identity resolution and ancestor chain walking |
|
||||
| `grpc_client.rs` | gRPC client for fetching policy, provider environment, inference route bundles, policy polling/status reporting, proposal submission, and log push (`CachedNavigatorClient`) |
|
||||
| `grpc_client.rs` | gRPC client for fetching policy, provider environment, inference route bundles, policy polling/status reporting, proposal submission, and log push (`CachedOpenShellClient`) |
|
||||
| `denial_aggregator.rs` | `DenialAggregator` background task -- receives `DenialEvent`s from the proxy, deduplicates by `(host, port, binary)`, drains on flush interval |
|
||||
| `mechanistic_mapper.rs` | Deterministic policy recommendation generator -- converts denial summaries to `PolicyChunk` proposals with confidence scores, rationale, and SSRF/private-IP detection |
|
||||
| `sandbox/mod.rs` | Platform abstraction -- dispatches to Linux or no-op |
|
||||
@@ -35,7 +35,7 @@ All paths are relative to `crates/navigator-sandbox/src/`.
|
||||
|
||||
## Startup and Orchestration
|
||||
|
||||
The `run_sandbox()` function in `crates/navigator-sandbox/src/lib.rs` is the main orchestration entry point. It executes the following steps in order.
|
||||
The `run_sandbox()` function in `crates/openshell-sandbox/src/lib.rs` is the main orchestration entry point. It executes the following steps in order.
|
||||
|
||||
### Orchestration flow
|
||||
|
||||
@@ -76,7 +76,7 @@ flowchart TD
|
||||
|
||||
1. **Policy loading** (`load_policy()`):
|
||||
- Priority 1: `--policy-rules` + `--policy-data` provided -- load OPA engine from local Rego file and YAML data file via `OpaEngine::from_files()`. Query `query_sandbox_config()` for filesystem/landlock/process settings. Network mode forced to `Proxy`.
|
||||
- Priority 2: `--sandbox-id` + `--navigator-endpoint` provided -- fetch typed proto policy via `grpc_client::fetch_policy()`. Create OPA engine via `OpaEngine::from_proto()` using baked-in Rego rules. Convert proto to `SandboxPolicy` via `TryFrom`, which always forces `NetworkMode::Proxy` so that all egress passes through the proxy and the `inference.local` virtual host is always addressable.
|
||||
- Priority 2: `--sandbox-id` + `--openshell-endpoint` provided -- fetch typed proto policy via `grpc_client::fetch_policy()`. Create OPA engine via `OpaEngine::from_proto()` using baked-in Rego rules. Convert proto to `SandboxPolicy` via `TryFrom`, which always forces `NetworkMode::Proxy` so that all egress passes through the proxy and the `inference.local` virtual host is always addressable.
|
||||
- Neither present: return fatal error.
|
||||
- Output: `(SandboxPolicy, Option<Arc<OpaEngine>>)`
|
||||
|
||||
@@ -88,7 +88,7 @@ flowchart TD
|
||||
|
||||
5. **TLS state for L7 inspection** (proxy mode only):
|
||||
- Generate ephemeral CA via `SandboxCa::generate()` using `rcgen`
|
||||
- Write CA cert PEM and combined bundle (system CAs + sandbox CA) to `/etc/navigator-tls/`
|
||||
- Write CA cert PEM and combined bundle (system CAs + sandbox CA) to `/etc/openshell-tls/`
|
||||
- Add the TLS directory to `policy.filesystem.read_only` so Landlock allows the child to read it
|
||||
- Build upstream `ClientConfig` with Mozilla root CAs via `webpki_roots`
|
||||
- Create `Arc<ProxyTlsState>` wrapping a `CertCache` and the upstream config
|
||||
@@ -113,13 +113,13 @@ flowchart TD
|
||||
|
||||
10. **Store entrypoint PID**: `entrypoint_pid.store(pid, Ordering::Release)` so the proxy can resolve TCP peer identity via `/proc`.
|
||||
|
||||
11. **Spawn policy poll task** (gRPC mode only): If `sandbox_id`, `navigator_endpoint`, and an OPA engine are all present, spawn `run_policy_poll_loop()` as a background tokio task. This task polls the gateway for policy updates and hot-reloads the OPA engine when a new version is detected. See [Policy Reload Lifecycle](#policy-reload-lifecycle) for details.
|
||||
11. **Spawn policy poll task** (gRPC mode only): If `sandbox_id`, `openshell_endpoint`, and an OPA engine are all present, spawn `run_policy_poll_loop()` as a background tokio task. This task polls the gateway for policy updates and hot-reloads the OPA engine when a new version is detected. See [Policy Reload Lifecycle](#policy-reload-lifecycle) for details.
|
||||
|
||||
12. **Wait with timeout**: If `--timeout > 0`, wrap `handle.wait()` in `tokio::time::timeout()`. On timeout, kill the process and return exit code 124.
|
||||
|
||||
## Policy Model
|
||||
|
||||
Policy data structures live in `crates/navigator-sandbox/src/policy.rs`.
|
||||
Policy data structures live in `crates/openshell-sandbox/src/policy.rs`.
|
||||
|
||||
```rust
|
||||
pub struct SandboxPolicy {
|
||||
@@ -188,11 +188,11 @@ flowchart LR
|
||||
|
||||
## OPA Policy Engine
|
||||
|
||||
The OPA engine lives in `crates/navigator-sandbox/src/opa.rs` and uses the `regorus` crate -- a pure-Rust Rego evaluator with no external OPA daemon dependency.
|
||||
The OPA engine lives in `crates/openshell-sandbox/src/opa.rs` and uses the `regorus` crate -- a pure-Rust Rego evaluator with no external OPA daemon dependency.
|
||||
|
||||
### Baked-in rules
|
||||
|
||||
The Rego rules are compiled into the binary via `include_str!("../data/sandbox-policy.rego")`. The package is `navigator.sandbox`. Key rules:
|
||||
The Rego rules are compiled into the binary via `include_str!("../data/sandbox-policy.rego")`. The package is `openshell.sandbox`. Key rules:
|
||||
|
||||
| Rule | Type | Purpose |
|
||||
|------|------|---------|
|
||||
@@ -247,15 +247,15 @@ Input JSON shape:
|
||||
```
|
||||
|
||||
Evaluates three Rego rules:
|
||||
1. `data.navigator.sandbox.allow_network` -> bool
|
||||
2. `data.navigator.sandbox.deny_reason` -> string
|
||||
3. `data.navigator.sandbox.matched_network_policy` -> string (or `Undefined`)
|
||||
1. `data.openshell.sandbox.allow_network` -> bool
|
||||
2. `data.openshell.sandbox.deny_reason` -> string
|
||||
3. `data.openshell.sandbox.matched_network_policy` -> string (or `Undefined`)
|
||||
|
||||
Returns `PolicyDecision { allowed, reason, matched_policy }`.
|
||||
|
||||
#### `evaluate_network_action(input: &NetworkInput) -> Result<NetworkAction>`
|
||||
|
||||
Uses the same input JSON shape as `evaluate_network()`. Evaluates the `data.navigator.sandbox.network_action` Rego rule, which returns one of two string values:
|
||||
Uses the same input JSON shape as `evaluate_network()`. Evaluates the `data.openshell.sandbox.network_action` Rego rule, which returns one of two string values:
|
||||
|
||||
- `"allow"` -- endpoint + binary explicitly matched in a network policy
|
||||
- `"deny"` -- network connections not allowed by policy
|
||||
@@ -277,7 +277,7 @@ The proxy calls `evaluate_network_action()` (not `evaluate_network()`) as its ma
|
||||
|
||||
### L7 endpoint config query
|
||||
|
||||
After L4 allows a connection, `query_endpoint_config(input)` evaluates `data.navigator.sandbox.matched_endpoint_config` to get the full endpoint object. If the endpoint has a `protocol` field, `l7::parse_l7_config()` extracts the L7 config for protocol-aware inspection.
|
||||
After L4 allows a connection, `query_endpoint_config(input)` evaluates `data.openshell.sandbox.matched_endpoint_config` to get the full endpoint object. If the endpoint has a `protocol` field, `l7::parse_l7_config()` extracts the L7 config for protocol-aware inspection.
|
||||
|
||||
### Engine cloning for L7
|
||||
|
||||
@@ -294,7 +294,7 @@ Both methods hold the `Mutex` only for the final swap (`*engine = new_engine`),
|
||||
|
||||
## Policy Reload Lifecycle
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/lib.rs` (`run_policy_poll_loop()`)
|
||||
**File:** `crates/openshell-sandbox/src/lib.rs` (`run_policy_poll_loop()`)
|
||||
|
||||
In gRPC mode, the sandbox can receive policy updates at runtime without restarting. A background task polls the gateway for new policy versions and hot-reloads the OPA engine when changes are detected. Only **dynamic** policy domains (network rules) can change at runtime; **static** domains (filesystem, Landlock, process) are applied once in the pre-exec closure and cannot be modified after the child process spawns.
|
||||
|
||||
@@ -340,24 +340,24 @@ sequenceDiagram
|
||||
end
|
||||
```
|
||||
|
||||
The `run_policy_poll_loop()` function in `crates/navigator-sandbox/src/lib.rs` implements this loop:
|
||||
The `run_policy_poll_loop()` function in `crates/openshell-sandbox/src/lib.rs` implements this loop:
|
||||
|
||||
1. **Connect once**: Create a `CachedNavigatorClient` that holds a persistent mTLS channel to the gateway. This avoids TLS renegotiation on every poll.
|
||||
1. **Connect once**: Create a `CachedOpenShellClient` that holds a persistent mTLS channel to the gateway. This avoids TLS renegotiation on every poll.
|
||||
2. **Fetch initial version**: Call `poll_policy(sandbox_id)` to establish the baseline `current_version`. On failure, log a warning and retry on the next interval.
|
||||
3. **Poll loop**: Sleep for the configured interval, then call `poll_policy()` again.
|
||||
4. **Version comparison**: If `result.version <= current_version`, skip. The version is a monotonically increasing `u32` per sandbox.
|
||||
5. **Reload attempt**: Call `opa_engine.reload_from_proto(&result.policy)`. This runs the full `from_proto()` pipeline on the new policy, then atomically swaps the inner engine.
|
||||
6. **Status reporting**: On success, report `PolicyStatus::Loaded` to the gateway via `ReportPolicyStatus` RPC. On failure, report `PolicyStatus::Failed` with the error message. Status report failures are logged but do not affect the poll loop.
|
||||
|
||||
### `CachedNavigatorClient`
|
||||
### `CachedOpenShellClient`
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/grpc_client.rs`
|
||||
**File:** `crates/openshell-sandbox/src/grpc_client.rs`
|
||||
|
||||
`CachedNavigatorClient` is a persistent gRPC client for the `Navigator` service. It wraps a `NavigatorClient<Channel>` connected once at construction and reused for all subsequent calls.
|
||||
`CachedOpenShellClient` is a persistent gRPC client for the `OpenShell` service. It wraps a `OpenShellClient<Channel>` connected once at construction and reused for all subsequent calls.
|
||||
|
||||
```rust
|
||||
pub struct CachedNavigatorClient {
|
||||
client: NavigatorClient<Channel>,
|
||||
pub struct CachedOpenShellClient {
|
||||
client: OpenShellClient<Channel>,
|
||||
}
|
||||
|
||||
pub struct PolicyPollResult {
|
||||
@@ -371,7 +371,7 @@ Methods:
|
||||
- **`connect(endpoint)`**: Establish an mTLS channel and return a new client.
|
||||
- **`poll_policy(sandbox_id)`**: Call `GetSandboxPolicy` RPC and return a `PolicyPollResult` containing the policy, version, and hash.
|
||||
- **`report_policy_status(sandbox_id, version, loaded, error_msg)`**: Call `ReportPolicyStatus` RPC with the appropriate `PolicyStatus` enum value (`Loaded` or `Failed`).
|
||||
- **`raw_client()`**: Return a clone of the underlying `NavigatorClient<Channel>` for direct RPC calls (used by the log push task).
|
||||
- **`raw_client()`**: Return a clone of the underlying `OpenShellClient<Channel>` for direct RPC calls (used by the log push task).
|
||||
|
||||
### Server-side policy versioning
|
||||
|
||||
@@ -379,9 +379,9 @@ The gateway assigns a monotonically increasing version number to each policy rev
|
||||
|
||||
Proto messages involved:
|
||||
- `GetSandboxPolicyResponse` (`proto/sandbox.proto`): `policy`, `version`, `policy_hash`
|
||||
- `ReportPolicyStatusRequest` (`proto/navigator.proto`): `sandbox_id`, `version`, `status` (enum), `load_error`
|
||||
- `ReportPolicyStatusRequest` (`proto/openshell.proto`): `sandbox_id`, `version`, `status` (enum), `load_error`
|
||||
- `PolicyStatus` enum: `PENDING`, `LOADED`, `FAILED`, `SUPERSEDED`
|
||||
- `SandboxPolicyRevision` (`proto/navigator.proto`): Full revision metadata including `created_at_ms`, `loaded_at_ms`
|
||||
- `SandboxPolicyRevision` (`proto/openshell.proto`): Full revision metadata including `created_at_ms`, `loaded_at_ms`
|
||||
|
||||
### Failure modes
|
||||
|
||||
@@ -399,7 +399,7 @@ All enforcement code runs in the child process's pre-exec closure -- after `fork
|
||||
|
||||
### Landlock filesystem isolation
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/sandbox/linux/landlock.rs`
|
||||
**File:** `crates/openshell-sandbox/src/sandbox/linux/landlock.rs`
|
||||
|
||||
Landlock restricts the child process's filesystem access to an explicit allowlist.
|
||||
|
||||
@@ -417,7 +417,7 @@ Error behavior depends on `LandlockCompatibility`:
|
||||
|
||||
### Seccomp syscall filtering
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/sandbox/linux/seccomp.rs`
|
||||
**File:** `crates/openshell-sandbox/src/sandbox/linux/seccomp.rs`
|
||||
|
||||
Seccomp blocks socket creation for specific address families. The filter targets a single syscall (`SYS_socket`) and inspects argument 0 (the domain).
|
||||
|
||||
@@ -437,7 +437,7 @@ In `Proxy` mode, `AF_INET`/`AF_INET6` are allowed because the sandboxed process
|
||||
|
||||
### Network namespace isolation
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/sandbox/linux/netns.rs`
|
||||
**File:** `crates/openshell-sandbox/src/sandbox/linux/netns.rs`
|
||||
|
||||
The network namespace creates an isolated network stack where the sandboxed process can only communicate through the proxy.
|
||||
|
||||
@@ -489,7 +489,7 @@ If namespace creation fails (e.g., missing capabilities), startup fails in `Prox
|
||||
|
||||
## HTTP CONNECT Proxy
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/proxy.rs`
|
||||
**File:** `crates/openshell-sandbox/src/proxy.rs`
|
||||
|
||||
The proxy is an async TCP listener that accepts HTTP CONNECT requests. Each connection spawns a handler task. The proxy evaluates every CONNECT request against OPA policy with full process-identity binding, except for connections to the `inference.local` virtual host which bypass OPA and are handled by the inference interception path.
|
||||
|
||||
@@ -620,7 +620,7 @@ After OPA allows a connection, the proxy resolves DNS and rejects any host that
|
||||
|
||||
### Inference interception
|
||||
|
||||
When a CONNECT target is `inference.local`, the proxy TLS-terminates the client side and inspects the HTTP traffic to detect inference API calls. Matched requests are executed locally via the `navigator-router` crate. The function `handle_inference_interception()` implements this path and returns an `InferenceOutcome`:
|
||||
When a CONNECT target is `inference.local`, the proxy TLS-terminates the client side and inspects the HTTP traffic to detect inference API calls. Matched requests are executed locally via the `openshell-router` crate. The function `handle_inference_interception()` implements this path and returns an `InferenceOutcome`:
|
||||
|
||||
```rust
|
||||
enum InferenceOutcome {
|
||||
@@ -678,15 +678,15 @@ When `Router::proxy_with_candidates()` returns an error, `router_error_to_http()
|
||||
|
||||
### Inference routing context
|
||||
|
||||
**Files:** `crates/navigator-sandbox/src/lib.rs` (`build_inference_context`, `bundle_to_resolved_routes`, `spawn_route_refresh`), `crates/navigator-sandbox/src/proxy.rs` (`InferenceContext`)
|
||||
**Files:** `crates/openshell-sandbox/src/lib.rs` (`build_inference_context`, `bundle_to_resolved_routes`, `spawn_route_refresh`), `crates/openshell-sandbox/src/proxy.rs` (`InferenceContext`)
|
||||
|
||||
The sandbox executes inference requests locally using the `navigator-router` crate. `InferenceContext` holds the router, API patterns, and a cached set of resolved routes:
|
||||
The sandbox executes inference requests locally using the `openshell-router` crate. `InferenceContext` holds the router, API patterns, and a cached set of resolved routes:
|
||||
|
||||
```rust
|
||||
pub struct InferenceContext {
|
||||
pub patterns: Vec<InferenceApiPattern>,
|
||||
router: navigator_router::Router,
|
||||
routes: Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>>,
|
||||
router: openshell_router::Router,
|
||||
routes: Arc<tokio::sync::RwLock<Vec<openshell_router::config::ResolvedRoute>>>,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -700,7 +700,7 @@ The sandbox is designed to operate both as part of a cluster and as a standalone
|
||||
|
||||
1. **Route file (standalone mode)**: `--inference-routes` / `OPENSHELL_INFERENCE_ROUTES` points to a YAML file parsed by `RouterConfig::load_from_file()`. Routes are resolved via `config.resolve_routes()`. File loading or parsing errors are fatal (fail-fast), but an empty route list gracefully disables inference routing (returns `None`). The route file always takes precedence -- if both a route file and cluster credentials are present, the route file wins and the cluster bundle is not fetched.
|
||||
|
||||
2. **Cluster bundle (cluster mode)**: When `navigator_endpoint` is available (and no route file is configured), routes are fetched from the gateway via `grpc_client::fetch_inference_bundle()`, which calls the `GetInferenceBundle` gRPC RPC on the `Inference` service. The RPC takes no arguments (the bundle is cluster-scoped, not per-sandbox). The gateway returns a `GetInferenceBundleResponse` containing resolved `ResolvedRoute` entries for the managed cluster route. These proto messages are converted to router `ResolvedRoute` structs by `bundle_to_resolved_routes()`, which maps provider types to auth headers and default headers via `navigator_core::inference::auth_for_provider_type()`.
|
||||
2. **Cluster bundle (cluster mode)**: When `openshell_endpoint` is available (and no route file is configured), routes are fetched from the gateway via `grpc_client::fetch_inference_bundle()`, which calls the `GetInferenceBundle` gRPC RPC on the `Inference` service. The RPC takes no arguments (the bundle is cluster-scoped, not per-sandbox). The gateway returns a `GetInferenceBundleResponse` containing resolved `ResolvedRoute` entries for the managed cluster route. These proto messages are converted to router `ResolvedRoute` structs by `bundle_to_resolved_routes()`, which maps provider types to auth headers and default headers via `openshell_core::inference::auth_for_provider_type()`.
|
||||
|
||||
3. **No source**: If neither route file nor cluster credentials are configured, `build_inference_context()` returns `None` and inference routing is disabled.
|
||||
|
||||
@@ -741,7 +741,7 @@ flowchart TD
|
||||
|
||||
#### API key security
|
||||
|
||||
`ResolvedRoute` has a custom `Debug` implementation in `crates/navigator-router/src/config.rs` that redacts the `api_key` field, printing `[REDACTED]` instead of the actual value. This prevents key leakage in log output and debug traces.
|
||||
`ResolvedRoute` has a custom `Debug` implementation in `crates/openshell-router/src/config.rs` that redacts the `api_key` field, printing `[REDACTED]` instead of the actual value. This prevents key leakage in log output and debug traces.
|
||||
|
||||
### Post-decision: L7 dispatch or raw tunnel (`Allow` path)
|
||||
|
||||
@@ -760,7 +760,7 @@ After a CONNECT is allowed, the SSRF check passes, and the upstream TCP connecti
|
||||
|
||||
## L7 Protocol-Aware Inspection
|
||||
|
||||
**Files:** `crates/navigator-sandbox/src/l7/`
|
||||
**Files:** `crates/openshell-sandbox/src/l7/`
|
||||
|
||||
The L7 subsystem inspects application-layer traffic within CONNECT tunnels. Instead of raw `copy_bidirectional`, each request is parsed, evaluated against OPA rules, and either forwarded or blocked.
|
||||
|
||||
@@ -820,13 +820,13 @@ Expansion happens in `expand_access_presets()` before the Rego engine loads the
|
||||
|
||||
### TLS termination
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/l7/tls.rs`
|
||||
**File:** `crates/openshell-sandbox/src/l7/tls.rs`
|
||||
|
||||
TLS termination enables the proxy to inspect HTTPS traffic by performing MITM decryption.
|
||||
|
||||
**Ephemeral CA lifecycle:**
|
||||
1. At sandbox startup, `SandboxCa::generate()` creates a self-signed CA (CN: "Navigator Sandbox CA") using `rcgen`
|
||||
2. The CA cert PEM and a combined bundle (system CAs + sandbox CA) are written to `/etc/navigator-tls/`
|
||||
1. At sandbox startup, `SandboxCa::generate()` creates a self-signed CA (CN: "OpenShell Sandbox CA") using `rcgen`
|
||||
2. The CA cert PEM and a combined bundle (system CAs + sandbox CA) are written to `/etc/openshell-tls/`
|
||||
3. The sandbox CA cert path is set as `NODE_EXTRA_CA_CERTS` (additive for Node.js)
|
||||
4. The combined bundle is set as `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` (replaces defaults for OpenSSL, Python requests, curl)
|
||||
|
||||
@@ -845,7 +845,7 @@ System CA bundles are searched at well-known paths: `/etc/ssl/certs/ca-certifica
|
||||
|
||||
### REST protocol provider
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/l7/rest.rs`
|
||||
**File:** `crates/openshell-sandbox/src/l7/rest.rs`
|
||||
|
||||
Implements `L7Provider` for HTTP/1.1:
|
||||
|
||||
@@ -853,17 +853,17 @@ Implements `L7Provider` for HTTP/1.1:
|
||||
|
||||
- **`relay()`**: Forwards request headers and body to upstream (handling Content-Length, chunked, and no-body cases), then reads and relays the full response back to the client.
|
||||
|
||||
- **`deny()`**: Sends an HTTP `403 Forbidden` JSON response with `Content-Type: application/json`, including the policy name, matched rule, and deny reason. Sets `Connection: close` and includes an `X-Navigator-Policy` header.
|
||||
- **`deny()`**: Sends an HTTP `403 Forbidden` JSON response with `Content-Type: application/json`, including the policy name, matched rule, and deny reason. Sets `Connection: close` and includes an `X-OpenShell-Policy` header.
|
||||
|
||||
- **`looks_like_http()`**: Protocol detection via first-byte peek -- checks for standard HTTP method prefixes (GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, CONNECT, TRACE).
|
||||
|
||||
### Per-request L7 evaluation
|
||||
|
||||
`relay_with_inspection()` in `crates/navigator-sandbox/src/l7/relay.rs` is the main relay loop:
|
||||
`relay_with_inspection()` in `crates/openshell-sandbox/src/l7/relay.rs` is the main relay loop:
|
||||
|
||||
1. Parse one HTTP request from client via the provider
|
||||
2. Build L7 input JSON with `request.method`, `request.path`, plus the CONNECT-level context (host, port, binary, ancestors, cmdline)
|
||||
3. Evaluate `data.navigator.sandbox.allow_request` and `data.navigator.sandbox.request_deny_reason`
|
||||
3. Evaluate `data.openshell.sandbox.allow_request` and `data.openshell.sandbox.request_deny_reason`
|
||||
4. Log the L7 decision (tagged `L7_REQUEST`)
|
||||
5. If allowed (or audit mode): relay request to upstream and response back to client, then loop
|
||||
6. If denied in enforce mode: send 403 and close the connection
|
||||
@@ -872,7 +872,7 @@ Implements `L7Provider` for HTTP/1.1:
|
||||
|
||||
### SHA256 TOFU (Trust-On-First-Use)
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/identity.rs`
|
||||
**File:** `crates/openshell-sandbox/src/identity.rs`
|
||||
|
||||
`BinaryIdentityCache` wraps a `Mutex<HashMap<PathBuf, CachedBinary>>`, where
|
||||
each cached entry stores:
|
||||
@@ -892,7 +892,7 @@ The TOFU model means:
|
||||
|
||||
### /proc-based identity resolution
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/procfs.rs`
|
||||
**File:** `crates/openshell-sandbox/src/procfs.rs`
|
||||
|
||||
The proxy resolves which binary is making each network request by inspecting `/proc`.
|
||||
|
||||
@@ -920,7 +920,7 @@ Both IPv4 (`/proc/{pid}/net/tcp`) and IPv6 (`/proc/{pid}/net/tcp6`) tables are c
|
||||
|
||||
## Process Management
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/process.rs`
|
||||
**File:** `crates/openshell-sandbox/src/process.rs`
|
||||
|
||||
### `ProcessHandle`
|
||||
|
||||
@@ -962,7 +962,7 @@ Exit code is `code` if the process exited normally, or `128 + signal` if killed
|
||||
|
||||
## SSH Server
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/ssh.rs`
|
||||
**File:** `crates/openshell-sandbox/src/ssh.rs`
|
||||
|
||||
The embedded SSH server provides remote shell access to the sandbox. It uses the `russh` crate and allocates PTYs for interactive sessions.
|
||||
|
||||
@@ -1017,11 +1017,11 @@ The `SshHandler` implements `russh::server::Handler`:
|
||||
|
||||
## Zombie Reaping (PID 1 Init Duties)
|
||||
|
||||
`navigator-sandbox` runs as PID 1 inside the container. In Linux, when a process exits, its parent must call `waitpid()` to collect the exit status; otherwise the process remains as a zombie. Orphaned processes (whose parent exits first) are reparented to PID 1, which becomes responsible for reaping them.
|
||||
`openshell-sandbox` runs as PID 1 inside the container. In Linux, when a process exits, its parent must call `waitpid()` to collect the exit status; otherwise the process remains as a zombie. Orphaned processes (whose parent exits first) are reparented to PID 1, which becomes responsible for reaping them.
|
||||
|
||||
Coding agents running inside the sandbox (OpenClaw, Claude, Codex) frequently spawn background daemons and child processes. When these grandchildren are orphaned, they become PID 1's responsibility. Without reaping, they accumulate as zombies for the lifetime of the container.
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/lib.rs`
|
||||
**File:** `crates/openshell-sandbox/src/lib.rs`
|
||||
|
||||
The sandbox supervisor registers a `SIGCHLD` handler at startup and spawns a background reaper task. The reaper also runs on a 5-second interval timer as a fallback in case signals are coalesced or missed. On each wake, it loops calling `waitid(Id::All, WEXITED | WNOHANG | WNOWAIT)` to inspect exited children without consuming their status. For each exited child:
|
||||
|
||||
@@ -1039,7 +1039,7 @@ This two-phase approach (peek with `WNOWAIT`, then selectively reap) avoids `ECH
|
||||
|----------|----------|---------|---------|
|
||||
| `OPENSHELL_SANDBOX_COMMAND` | (trailing args) | `/bin/bash` | Command to execute inside sandbox |
|
||||
| `OPENSHELL_SANDBOX_ID` | `--sandbox-id` | | Sandbox ID for gRPC policy fetch |
|
||||
| `OPENSHELL_ENDPOINT` | `--navigator-endpoint` | | Gateway gRPC endpoint |
|
||||
| `OPENSHELL_ENDPOINT` | `--openshell-endpoint` | | Gateway gRPC endpoint |
|
||||
| `OPENSHELL_POLICY_RULES` | `--policy-rules` | | Path to Rego policy file |
|
||||
| `OPENSHELL_POLICY_DATA` | `--policy-data` | | Path to YAML data file |
|
||||
| `OPENSHELL_LOG_LEVEL` | `--log-level` | `warn` | Log level (trace/debug/info/warn/error) |
|
||||
@@ -1121,7 +1121,7 @@ The sandbox uses `miette` for error reporting and `thiserror` for typed errors.
|
||||
|
||||
Dual-output logging is configured in `main.rs`:
|
||||
- **stdout**: Filtered by `--log-level` (default `warn`), uses ANSI colors
|
||||
- **`/var/log/navigator.log`**: Fixed at `info` level, no ANSI, non-blocking writer
|
||||
- **`/var/log/openshell.log`**: Fixed at `info` level, no ANSI, non-blocking writer
|
||||
|
||||
Key structured log events:
|
||||
- `CONNECT`: One per proxy CONNECT request (for non-`inference.local` targets) with full identity context. Inference interception failures produce a separate `info!()` log with `action=deny` and the denial reason.
|
||||
@@ -1161,7 +1161,7 @@ Two log sources feed the same `TracingLogBus`:
|
||||
|
||||
### LogPushLayer
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/log_push.rs`
|
||||
**File:** `crates/openshell-sandbox/src/log_push.rs`
|
||||
|
||||
`LogPushLayer` is a `tracing_subscriber::Layer` that intercepts tracing events in the sandbox supervisor and forwards them to the gateway.
|
||||
|
||||
@@ -1181,9 +1181,9 @@ Key behaviors:
|
||||
|
||||
### Initialization
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/main.rs`
|
||||
**File:** `crates/openshell-sandbox/src/main.rs`
|
||||
|
||||
The log push layer is set up in `main()` before calling `run_sandbox()`, only in gRPC mode (when both `--sandbox-id` and `--navigator-endpoint` are present):
|
||||
The log push layer is set up in `main()` before calling `run_sandbox()`, only in gRPC mode (when both `--sandbox-id` and `--openshell-endpoint` are present):
|
||||
|
||||
1. `spawn_log_push_task(endpoint, sandbox_id)` creates the mpsc channel and background task, returning the sender half and a `JoinHandle`.
|
||||
2. `LogPushLayer::new(sandbox_id, tx)` wraps the sender in a tracing layer.
|
||||
@@ -1193,12 +1193,12 @@ This means the push layer captures all tracing events the sandbox supervisor gen
|
||||
|
||||
### Background push task
|
||||
|
||||
**File:** `crates/navigator-sandbox/src/log_push.rs` (`spawn_log_push_task()`, `run_push_loop()`)
|
||||
**File:** `crates/openshell-sandbox/src/log_push.rs` (`spawn_log_push_task()`, `run_push_loop()`)
|
||||
|
||||
The background task batches log lines and streams them to the gateway:
|
||||
|
||||
1. **Channel setup**: Creates a bounded `mpsc::channel::<SandboxLogLine>(1024)`. The sender goes to the `LogPushLayer`; the receiver feeds the push loop.
|
||||
2. **gRPC connection**: Connects a `CachedNavigatorClient` to the gateway. On connection failure, the task prints to stderr (cannot use tracing to avoid recursion) and exits.
|
||||
2. **gRPC connection**: Connects a `CachedOpenShellClient` to the gateway. On connection failure, the task prints to stderr (cannot use tracing to avoid recursion) and exits.
|
||||
3. **Client-streaming RPC**: Opens a `PushSandboxLogs` client-streaming call via a secondary `mpsc::channel::<PushSandboxLogsRequest>(32)` wrapped in `tokio_stream::wrappers::ReceiverStream`. A separate spawned task drives the gRPC call.
|
||||
4. **Batch-and-flush loop**: Accumulates lines in a `Vec` (capacity 50). Flushes when:
|
||||
- The batch reaches 50 lines, OR
|
||||
@@ -1207,7 +1207,7 @@ The background task batches log lines and streams them to the gateway:
|
||||
|
||||
### Server-side ingestion
|
||||
|
||||
**File:** `crates/navigator-server/src/grpc.rs` (`push_sandbox_logs`)
|
||||
**File:** `crates/openshell-server/src/grpc.rs` (`push_sandbox_logs`)
|
||||
|
||||
The `PushSandboxLogs` RPC handler processes each batch:
|
||||
1. Validates `sandbox_id` is non-empty (skips empty batches).
|
||||
@@ -1218,7 +1218,7 @@ The `PushSandboxLogs` RPC handler processes each batch:
|
||||
|
||||
### TracingLogBus integration
|
||||
|
||||
**File:** `crates/navigator-server/src/tracing_bus.rs`
|
||||
**File:** `crates/openshell-server/src/tracing_bus.rs`
|
||||
|
||||
`publish_external()` wraps the `SandboxLogLine` in a `SandboxStreamEvent` and calls the internal `publish()` method, which:
|
||||
1. Sends the event to the per-sandbox `broadcast::Sender` (capacity 1024). Subscribers (active `WatchSandbox` streams) receive the event immediately.
|
||||
@@ -1252,7 +1252,7 @@ Gateway-sourced logs do not currently populate the `fields` map (it remains empt
|
||||
|
||||
### CLI filtering
|
||||
|
||||
**File:** `crates/navigator-cli/src/main.rs` (command definition), `crates/navigator-cli/src/run.rs` (`sandbox_logs()`)
|
||||
**File:** `crates/openshell-cli/src/main.rs` (command definition), `crates/openshell-cli/src/run.rs` (`sandbox_logs()`)
|
||||
|
||||
The `nav logs` command supports filtering by source and level:
|
||||
|
||||
@@ -1292,7 +1292,7 @@ Filtering is implemented server-side. For `WatchSandbox`, filters apply to both
|
||||
|
||||
### CLI output format
|
||||
|
||||
`print_log_line()` in `crates/navigator-cli/src/run.rs` formats each log line:
|
||||
`print_log_line()` in `crates/openshell-cli/src/run.rs` formats each log line:
|
||||
|
||||
```
|
||||
[timestamp] [source ] [level] [target] message key=value key=value
|
||||
@@ -1300,15 +1300,15 @@ Filtering is implemented server-side. For `WatchSandbox`, filters apply to both
|
||||
|
||||
Example output:
|
||||
```
|
||||
[1708891234.567] [sandbox] [INFO ] [navigator_sandbox::proxy] CONNECT api.example.com:443 dst_host=api.example.com action=allow
|
||||
[1708891234.890] [gateway] [INFO ] [navigator_server::grpc] ReportPolicyStatus: sandbox reported policy load result
|
||||
[1708891234.567] [sandbox] [INFO ] [openshell_sandbox::proxy] CONNECT api.example.com:443 dst_host=api.example.com action=allow
|
||||
[1708891234.890] [gateway] [INFO ] [openshell_server::grpc] ReportPolicyStatus: sandbox reported policy load result
|
||||
```
|
||||
|
||||
When the `fields` map is non-empty, entries are sorted by key and appended as `key=value` pairs.
|
||||
|
||||
### Create-watch filter
|
||||
|
||||
**File:** `crates/navigator-cli/src/run.rs`
|
||||
**File:** `crates/openshell-cli/src/run.rs`
|
||||
|
||||
During `sandbox create`, the CLI opens a `WatchSandbox` stream with `stop_on_terminal: true` to wait until the sandbox reaches `Ready` phase. This stream uses `log_sources: ["gateway"]` to filter out sandbox-pushed logs. Without this filter, continuous sandbox supervisor logs (e.g., proxy CONNECT events) would keep the stream active and prevent `stop_on_terminal` from detecting that provisioning has completed and the stream should close.
|
||||
|
||||
@@ -1352,7 +1352,7 @@ sequenceDiagram
|
||||
|
||||
## Platform Support
|
||||
|
||||
Platform-specific code is abstracted through `crates/navigator-sandbox/src/sandbox/mod.rs`.
|
||||
Platform-specific code is abstracted through `crates/openshell-sandbox/src/sandbox/mod.rs`.
|
||||
|
||||
| Feature | Linux | Other platforms |
|
||||
|---------|-------|-----------------|
|
||||
@@ -1374,4 +1374,4 @@ On non-Linux platforms, the sandbox can still run commands with proxy-based netw
|
||||
- [Sandbox Connect](sandbox-connect.md) -- SSH tunnel from gateway to sandbox
|
||||
- [Providers](sandbox-providers.md) -- Provider credential injection
|
||||
- [Policy Language](security-policy.md) -- Rego policy syntax and rules
|
||||
- [Inference Routing](inference-routing.md) -- Inference interception, route management, and the `navigator-router` crate
|
||||
- [Inference Routing](inference-routing.md) -- Inference interception, route management, and the `openshell-router` crate
|
||||
|
||||
@@ -16,7 +16,7 @@ The sandbox supervisor loads policy through one of two paths, selected at startu
|
||||
Provide a Rego rules file and a YAML data file via CLI flags or environment variables:
|
||||
|
||||
```bash
|
||||
navigator-sandbox \
|
||||
openshell-sandbox \
|
||||
--policy-rules sandbox-policy.rego \
|
||||
--policy-data dev-sandbox-policy.yaml \
|
||||
-- /bin/bash
|
||||
@@ -27,25 +27,25 @@ navigator-sandbox \
|
||||
| `--policy-rules` | `OPENSHELL_POLICY_RULES` | Path to `.rego` file containing evaluation rules |
|
||||
| `--policy-data` | `OPENSHELL_POLICY_DATA` | Path to YAML file containing policy data |
|
||||
|
||||
The YAML data file is preprocessed before loading into the OPA engine: L7 policies are validated, and `access` presets are expanded into explicit `rules` arrays. See `crates/navigator-sandbox/src/opa.rs` -- `preprocess_yaml_data()`.
|
||||
The YAML data file is preprocessed before loading into the OPA engine: L7 policies are validated, and `access` presets are expanded into explicit `rules` arrays. See `crates/openshell-sandbox/src/opa.rs` -- `preprocess_yaml_data()`.
|
||||
|
||||
### gRPC Mode (Production)
|
||||
|
||||
When the sandbox runs inside a managed cluster, it fetches its typed protobuf policy from the gateway:
|
||||
|
||||
```bash
|
||||
navigator-sandbox \
|
||||
openshell-sandbox \
|
||||
--sandbox-id abc123 \
|
||||
--navigator-endpoint https://navigator:8080 \
|
||||
--openshell-endpoint https://openshell:8080 \
|
||||
-- /bin/bash
|
||||
```
|
||||
|
||||
| Flag | Environment Variable | Description |
|
||||
| ------------------------ | ---------------------- | ---------------------------- |
|
||||
| `--sandbox-id` | `OPENSHELL_SANDBOX_ID` | Sandbox ID for policy lookup |
|
||||
| `--navigator-endpoint` | `OPENSHELL_ENDPOINT` | Gateway gRPC endpoint |
|
||||
| `--openshell-endpoint` | `OPENSHELL_ENDPOINT` | Gateway gRPC endpoint |
|
||||
|
||||
The gateway returns a `SandboxPolicy` protobuf message (defined in `proto/sandbox.proto`). The sandbox supervisor converts this proto into JSON, validates L7 config, expands presets, and loads it into the OPA engine using baked-in Rego rules (`sandbox-policy.rego` compiled via `include_str!`). See `crates/navigator-sandbox/src/opa.rs` -- `OpaEngine::from_proto()`.
|
||||
The gateway returns a `SandboxPolicy` protobuf message (defined in `proto/sandbox.proto`). The sandbox supervisor converts this proto into JSON, validates L7 config, expands presets, and loads it into the OPA engine using baked-in Rego rules (`sandbox-policy.rego` compiled via `include_str!`). See `crates/openshell-sandbox/src/opa.rs` -- `OpaEngine::from_proto()`.
|
||||
|
||||
### Policy Loading Sequence
|
||||
|
||||
@@ -53,7 +53,7 @@ The gateway returns a `SandboxPolicy` protobuf message (defined in `proto/sandbo
|
||||
flowchart TD
|
||||
START[Sandbox Startup] --> CHECK{File mode?<br/>--policy-rules +<br/>--policy-data}
|
||||
CHECK -->|Yes| FILE[Read .rego + .yaml from disk]
|
||||
CHECK -->|No| OPENSHELL{gRPC mode?<br/>--sandbox-id +<br/>--navigator-endpoint}
|
||||
CHECK -->|No| OPENSHELL{gRPC mode?<br/>--sandbox-id +<br/>--openshell-endpoint}
|
||||
OPENSHELL -->|Yes| FETCH[Fetch SandboxPolicy proto via gRPC]
|
||||
OPENSHELL -->|No| ERR[Error: no policy source]
|
||||
|
||||
@@ -69,7 +69,7 @@ flowchart TD
|
||||
|
||||
### Priority
|
||||
|
||||
File mode takes precedence. If both `--policy-rules`/`--policy-data` and `--sandbox-id`/`--navigator-endpoint` are provided, file mode is used. See `crates/navigator-sandbox/src/lib.rs` -- `load_policy()`.
|
||||
File mode takes precedence. If both `--policy-rules`/`--policy-data` and `--sandbox-id`/`--openshell-endpoint` are provided, file mode is used. See `crates/openshell-sandbox/src/lib.rs` -- `load_policy()`.
|
||||
|
||||
## Live Policy Updates
|
||||
|
||||
@@ -86,7 +86,7 @@ Policy fields fall into two categories based on when they are enforced:
|
||||
| **Static** | `filesystem_policy`, `landlock`, `process` | Applied once in the child process `pre_exec` (after `fork()`, before `exec()`). Kernel-level Landlock rulesets and UID/GID changes cannot be reversed. | No -- immutable after sandbox creation |
|
||||
| **Dynamic** | `network_policies`, `inference` | Evaluated at runtime by the OPA engine on every proxy CONNECT request and L7 rule check. The OPA engine can be atomically replaced. | Yes -- via `openshell policy set` |
|
||||
|
||||
Attempting to change a static field in an update request returns an `INVALID_ARGUMENT` error with a message indicating which field cannot be modified. See `crates/navigator-server/src/grpc.rs` -- `validate_static_fields_unchanged()`.
|
||||
Attempting to change a static field in an update request returns an `INVALID_ARGUMENT` error with a message indicating which field cannot be modified. See `crates/openshell-server/src/grpc.rs` -- `validate_static_fields_unchanged()`.
|
||||
|
||||
### Network Mode Immutability
|
||||
|
||||
@@ -95,7 +95,7 @@ The network mode (Block vs. Proxy) cannot change after sandbox creation. This is
|
||||
- **Block to Proxy**: Requires creating a network namespace, veth pair, and starting the CONNECT proxy -- none of which exist if the sandbox started in Block mode.
|
||||
- **Proxy to Block**: Requires removing the proxy, veth pair, and network namespace, and applying a stricter seccomp filter that blocks `AF_INET`/`AF_INET6` -- not possible on a running process.
|
||||
|
||||
An update that adds `network_policies` to a sandbox created without them (or removes all `network_policies` from a sandbox created with them) is rejected. See `crates/navigator-server/src/grpc.rs` -- `validate_network_mode_unchanged()`.
|
||||
An update that adds `network_policies` to a sandbox created without them (or removes all `network_policies` from a sandbox created with them) is rejected. See `crates/openshell-server/src/grpc.rs` -- `validate_network_mode_unchanged()`.
|
||||
|
||||
### Update Flow
|
||||
|
||||
@@ -104,9 +104,9 @@ The update mechanism uses a poll-based model with versioned policy revisions and
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as nav policy set
|
||||
participant GW as Gateway (navigator-server)
|
||||
participant GW as Gateway (openshell-server)
|
||||
participant DB as Persistence (SQLite/Postgres)
|
||||
participant SB as Sandbox (navigator-sandbox)
|
||||
participant SB as Sandbox (openshell-sandbox)
|
||||
|
||||
CLI->>GW: UpdateSandboxPolicy(name, new_policy)
|
||||
GW->>GW: Validate static fields unchanged
|
||||
@@ -144,16 +144,16 @@ sequenceDiagram
|
||||
|
||||
Each sandbox maintains an independent, monotonically increasing version counter for its policy revisions:
|
||||
|
||||
- **Version 1** is the policy from the sandbox's `spec.policy` at creation time. It is backfilled lazily on the first `GetSandboxPolicy` call if no explicit revision exists in the policy history table. See `crates/navigator-server/src/grpc.rs` -- `get_sandbox_policy()`.
|
||||
- **Version 1** is the policy from the sandbox's `spec.policy` at creation time. It is backfilled lazily on the first `GetSandboxPolicy` call if no explicit revision exists in the policy history table. See `crates/openshell-server/src/grpc.rs` -- `get_sandbox_policy()`.
|
||||
- Each `UpdateSandboxPolicy` call computes the next version as `latest_version + 1` and persists a new `PolicyRecord` with status `"pending"`.
|
||||
- When a new version is persisted, all older revisions still in `"pending"` status are marked `"superseded"` via `supersede_pending_policies()`. This handles rapid successive updates where the sandbox has not yet picked up an intermediate version.
|
||||
- The `Sandbox` protobuf object carries a `current_policy_version` field (see `proto/datamodel.proto`) that is updated when the sandbox reports a successful load.
|
||||
|
||||
Each revision is stored as a `PolicyRecord` containing the full serialized protobuf payload, a SHA-256 hash of that payload, a status string, and timestamps. See `crates/navigator-server/src/persistence/mod.rs` -- `PolicyRecord`.
|
||||
Each revision is stored as a `PolicyRecord` containing the full serialized protobuf payload, a SHA-256 hash of that payload, a status string, and timestamps. See `crates/openshell-server/src/persistence/mod.rs` -- `PolicyRecord`.
|
||||
|
||||
### Deterministic Policy Hashing
|
||||
|
||||
Policy hashes use a deterministic function that avoids the non-determinism of protobuf's `encode_to_vec()` on `map` fields. Protobuf `map` fields are backed by `HashMap`, whose iteration order is randomized, so encoding the same logical policy twice can produce different byte sequences. The `deterministic_policy_hash()` function avoids this by hashing each top-level field individually and sorting `network_policies` map entries by key before hashing. See `crates/navigator-server/src/grpc.rs` -- `deterministic_policy_hash()`.
|
||||
Policy hashes use a deterministic function that avoids the non-determinism of protobuf's `encode_to_vec()` on `map` fields. Protobuf `map` fields are backed by `HashMap`, whose iteration order is randomized, so encoding the same logical policy twice can produce different byte sequences. The `deterministic_policy_hash()` function avoids this by hashing each top-level field individually and sorting `network_policies` map entries by key before hashing. See `crates/openshell-server/src/grpc.rs` -- `deterministic_policy_hash()`.
|
||||
|
||||
The hash is computed as follows:
|
||||
|
||||
@@ -177,7 +177,7 @@ This guarantees that the same logical policy always produces the same hash regar
|
||||
|
||||
### Sandbox Poll Loop
|
||||
|
||||
In gRPC mode, the sandbox spawns a background task that periodically polls the gateway for policy updates. See `crates/navigator-sandbox/src/lib.rs` -- `run_policy_poll_loop()`.
|
||||
In gRPC mode, the sandbox spawns a background task that periodically polls the gateway for policy updates. See `crates/openshell-sandbox/src/lib.rs` -- `run_policy_poll_loop()`.
|
||||
|
||||
| Parameter | Default | Override |
|
||||
|-----------|---------|----------|
|
||||
@@ -185,20 +185,20 @@ In gRPC mode, the sandbox spawns a background task that periodically polls the g
|
||||
|
||||
The poll loop:
|
||||
|
||||
1. Connects a reusable gRPC client (`CachedNavigatorClient`) to avoid per-poll TLS handshake overhead.
|
||||
1. Connects a reusable gRPC client (`CachedOpenShellClient`) to avoid per-poll TLS handshake overhead.
|
||||
2. Fetches the current policy via `GetSandboxPolicy`, which returns the latest version, its policy payload, and a SHA-256 hash.
|
||||
3. Compares the returned version against the locally tracked `current_version`. If the server version is not greater, the loop sleeps and retries.
|
||||
4. On a new version, calls `OpaEngine::reload_from_proto()` which builds a complete new `regorus::Engine` through the same validated pipeline as the initial load (proto-to-JSON conversion, L7 validation, access preset expansion).
|
||||
5. If the new engine builds successfully, it atomically replaces the inner `Mutex<regorus::Engine>`. If it fails, the previous engine is untouched.
|
||||
6. Reports success or failure back to the server via `ReportPolicyStatus`.
|
||||
|
||||
See `crates/navigator-sandbox/src/grpc_client.rs` -- `CachedNavigatorClient`.
|
||||
See `crates/openshell-sandbox/src/grpc_client.rs` -- `CachedOpenShellClient`.
|
||||
|
||||
### Last-Known-Good (LKG) Behavior
|
||||
|
||||
When a new policy version fails validation during reload, the sandbox keeps the previous policy active. This provides safe rollback semantics:
|
||||
|
||||
- `OpaEngine::reload_from_proto()` constructs a complete new engine via `OpaEngine::from_proto()` before touching the existing one. If `from_proto()` returns an error (L7 validation failures, preset expansion errors, malformed proto data), the existing engine's `Mutex<regorus::Engine>` is never locked for replacement. See `crates/navigator-sandbox/src/opa.rs` -- `reload_from_proto()`.
|
||||
- `OpaEngine::reload_from_proto()` constructs a complete new engine via `OpaEngine::from_proto()` before touching the existing one. If `from_proto()` returns an error (L7 validation failures, preset expansion errors, malformed proto data), the existing engine's `Mutex<regorus::Engine>` is never locked for replacement. See `crates/openshell-sandbox/src/opa.rs` -- `reload_from_proto()`.
|
||||
- The failure error message is reported back to the server via `ReportPolicyStatus` with `PolicyStatus::FAILED` and stored in the `PolicyRecord.load_error` field.
|
||||
- The CLI's `--wait` flag polls `GetSandboxPolicyStatus` and surfaces the error to the operator.
|
||||
|
||||
@@ -245,9 +245,9 @@ nav policy list <sandbox-name> --limit 20
|
||||
| `--rev N` | `0` (latest) | Retrieve a specific policy revision by version number instead of the latest. Maps to the `version` field of `GetSandboxPolicyStatusRequest` -- version `0` resolves to the latest revision server-side. |
|
||||
| `--full` | off | Print the complete policy as YAML after the metadata summary. The YAML output uses the same schema as the `--policy` input file, so it round-trips: you can save it to a file and pass it back to `nav policy set --policy`. |
|
||||
|
||||
When `--full` is specified, the server includes the deserialized `SandboxPolicy` protobuf in the `SandboxPolicyRevision.policy` field (see `crates/navigator-server/src/grpc.rs` -- `policy_record_to_revision()` with `include_policy: true`). The CLI converts this proto back to YAML via `policy_to_yaml()`, which uses a `BTreeMap` for `network_policies` to produce deterministic key ordering. See `crates/navigator-cli/src/run.rs` -- `policy_to_yaml()`, `policy_get()`.
|
||||
When `--full` is specified, the server includes the deserialized `SandboxPolicy` protobuf in the `SandboxPolicyRevision.policy` field (see `crates/openshell-server/src/grpc.rs` -- `policy_record_to_revision()` with `include_policy: true`). The CLI converts this proto back to YAML via `policy_to_yaml()`, which uses a `BTreeMap` for `network_policies` to produce deterministic key ordering. See `crates/openshell-cli/src/run.rs` -- `policy_to_yaml()`, `policy_get()`.
|
||||
|
||||
See `crates/navigator-cli/src/main.rs` -- `PolicyCommands` enum, `crates/navigator-cli/src/run.rs` -- `policy_set()`, `policy_get()`, `policy_list()`.
|
||||
See `crates/openshell-cli/src/main.rs` -- `PolicyCommands` enum, `crates/openshell-cli/src/run.rs` -- `policy_set()`, `policy_get()`, `policy_list()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -299,11 +299,11 @@ Controls which filesystem paths the sandboxed process can access. Enforced via L
|
||||
|
||||
**Enforcement mapping**: Each path becomes a Landlock `PathBeneath` rule. Read-only paths receive `AccessFs::from_read(ABI::V1)` permissions. Read-write paths receive `AccessFs::from_all(ABI::V1)` permissions (read, write, execute, create, delete, rename). All other paths are denied by the Landlock ruleset.
|
||||
|
||||
**Filesystem preparation**: Before the child process spawns, the supervisor creates any `read_write` directories that do not exist and sets their ownership to `process.run_as_user`:`process.run_as_group` via `chown()`. See `crates/navigator-sandbox/src/lib.rs` -- `prepare_filesystem()`.
|
||||
**Filesystem preparation**: Before the child process spawns, the supervisor creates any `read_write` directories that do not exist and sets their ownership to `process.run_as_user`:`process.run_as_group` via `chown()`. See `crates/openshell-sandbox/src/lib.rs` -- `prepare_filesystem()`.
|
||||
|
||||
**Working directory**: When `include_workdir` is `true` and a `--workdir` is specified, the working directory path is appended to `read_write` if not already present. See `crates/navigator-sandbox/src/sandbox/linux/landlock.rs` -- `apply()`.
|
||||
**Working directory**: When `include_workdir` is `true` and a `--workdir` is specified, the working directory path is appended to `read_write` if not already present. See `crates/openshell-sandbox/src/sandbox/linux/landlock.rs` -- `apply()`.
|
||||
|
||||
**TLS directory**: When network proxy mode is active with TLS termination enabled, the directory `/etc/navigator-tls` is automatically appended to `read_only` so sandbox processes can read the ephemeral CA certificate files.
|
||||
**TLS directory**: When network proxy mode is active with TLS termination enabled, the directory `/etc/openshell-tls` is automatically appended to `read_only` so sandbox processes can read the ephemeral CA certificate files.
|
||||
|
||||
```yaml
|
||||
filesystem_policy:
|
||||
@@ -338,7 +338,7 @@ Controls Landlock LSM compatibility behavior. **Static field** -- immutable afte
|
||||
| `best_effort` | If Landlock is unavailable (older kernel, unprivileged container), log a warning and continue without filesystem sandboxing |
|
||||
| `hard_requirement` | If Landlock is unavailable, abort sandbox startup with an error |
|
||||
|
||||
See `crates/navigator-sandbox/src/sandbox/linux/landlock.rs` -- `compat_level()`.
|
||||
See `crates/openshell-sandbox/src/sandbox/linux/landlock.rs` -- `compat_level()`.
|
||||
|
||||
```yaml
|
||||
landlock:
|
||||
@@ -365,7 +365,7 @@ Controls privilege dropping for the sandboxed process. **Static field** -- immut
|
||||
5. Verify `geteuid()` matches the target UID
|
||||
6. Verify `setuid(0)` fails -- confirms root cannot be re-acquired
|
||||
|
||||
This happens before Landlock and seccomp are applied because `initgroups` needs access to `/etc/group` and `/etc/passwd`, which Landlock may subsequently block. The post-condition checks (steps 3, 5, 6) are async-signal-safe and add negligible overhead while guarding against hypothetical kernel-level defects. See `crates/navigator-sandbox/src/process.rs` -- `drop_privileges()`.
|
||||
This happens before Landlock and seccomp are applied because `initgroups` needs access to `/etc/group` and `/etc/passwd`, which Landlock may subsequently block. The post-condition checks (steps 3, 5, 6) are async-signal-safe and add negligible overhead while guarding against hypothetical kernel-level defects. See `crates/openshell-sandbox/src/process.rs` -- `drop_privileges()`.
|
||||
|
||||
```yaml
|
||||
process:
|
||||
@@ -461,15 +461,15 @@ The `access` field provides shorthand for common rule sets. During preprocessing
|
||||
| `read-write` | `GET/**`, `HEAD/**`, `OPTIONS/**`, `POST/**`, `PUT/**`, `PATCH/**` | Read and write but not delete |
|
||||
| `full` | `*/**` | All methods, all paths |
|
||||
|
||||
See `crates/navigator-sandbox/src/l7/mod.rs` -- `expand_access_presets()`.
|
||||
See `crates/openshell-sandbox/src/l7/mod.rs` -- `expand_access_presets()`.
|
||||
|
||||
---
|
||||
|
||||
### Inference Routing
|
||||
|
||||
Inference routing to `inference.local` is handled by the proxy's `InferenceContext`, not by the OPA policy engine or an `inference` block in the policy YAML. The proxy intercepts HTTPS CONNECT requests to `inference.local` and routes matching inference API requests (e.g., `POST /v1/chat/completions`, `POST /v1/messages`) through the sandbox-local `navigator-router`. See [Inference Routing](inference-routing.md) for details on route configuration and the router architecture.
|
||||
Inference routing to `inference.local` is handled by the proxy's `InferenceContext`, not by the OPA policy engine or an `inference` block in the policy YAML. The proxy intercepts HTTPS CONNECT requests to `inference.local` and routes matching inference API requests (e.g., `POST /v1/chat/completions`, `POST /v1/messages`) through the sandbox-local `openshell-router`. See [Inference Routing](inference-routing.md) for details on route configuration and the router architecture.
|
||||
|
||||
The proxy always runs in proxy mode so that `inference.local` is addressable from within the sandbox's network namespace. Inference route sources are configured separately from policy: via `--inference-routes` (file mode) or fetched from the gateway's inference bundle (cluster mode). See `crates/navigator-sandbox/src/proxy.rs` -- `InferenceContext`, `crates/navigator-sandbox/src/l7/inference.rs`.
|
||||
The proxy always runs in proxy mode so that `inference.local` is addressable from within the sandbox's network namespace. Inference route sources are configured separately from policy: via `--inference-routes` (file mode) or fetched from the gateway's inference bundle (cluster mode). See `crates/openshell-sandbox/src/proxy.rs` -- `InferenceContext`, `crates/openshell-sandbox/src/l7/inference.rs`.
|
||||
|
||||
---
|
||||
|
||||
@@ -479,7 +479,7 @@ Several policy fields trigger fundamentally different enforcement behavior. Unde
|
||||
|
||||
### Network Mode: Always Proxy
|
||||
|
||||
The sandbox always runs in **proxy mode**. Both file mode and gRPC mode set `NetworkMode::Proxy` unconditionally. This ensures all egress is evaluated by OPA and the virtual hostname `inference.local` is always addressable for inference routing. See `crates/navigator-sandbox/src/lib.rs` -- `load_policy()`, `crates/navigator-sandbox/src/policy.rs` -- `TryFrom<ProtoSandboxPolicy>`.
|
||||
The sandbox always runs in **proxy mode**. Both file mode and gRPC mode set `NetworkMode::Proxy` unconditionally. This ensures all egress is evaluated by OPA and the virtual hostname `inference.local` is always addressable for inference routing. See `crates/openshell-sandbox/src/lib.rs` -- `load_policy()`, `crates/openshell-sandbox/src/policy.rs` -- `TryFrom<ProtoSandboxPolicy>`.
|
||||
|
||||
In proxy mode:
|
||||
|
||||
@@ -490,7 +490,7 @@ In proxy mode:
|
||||
|
||||
When `network_policies` is empty, the OPA engine denies all outbound connections (except `inference.local` which is handled separately by the proxy before OPA evaluation).
|
||||
|
||||
**Gateway-side validation**: The `validate_network_mode_unchanged()` function on the server still rejects live policy updates that would add `network_policies` to a sandbox created without them or remove all `network_policies` from a sandbox created with them. This prevents unexpected behavioral changes in the OPA allow/deny logic. See `crates/navigator-server/src/grpc.rs` -- `validate_network_mode_unchanged()`.
|
||||
**Gateway-side validation**: The `validate_network_mode_unchanged()` function on the server still rejects live policy updates that would add `network_policies` to a sandbox created without them or remove all `network_policies` from a sandbox created with them. This prevents unexpected behavioral changes in the OPA allow/deny logic. See `crates/openshell-server/src/grpc.rs` -- `validate_network_mode_unchanged()`.
|
||||
|
||||
**Proxy sub-modes**: In proxy mode, the proxy handles two distinct request types:
|
||||
|
||||
@@ -527,9 +527,9 @@ flowchart LR
|
||||
|
||||
This is the single most important behavioral trigger in the policy language. An endpoint with no `protocol` field passes traffic opaquely after the L4 (CONNECT) check. Adding `protocol: rest` activates per-request HTTP parsing and policy evaluation inside the proxy.
|
||||
|
||||
**Implementation path**: After L4 CONNECT is allowed, the proxy calls `query_l7_config()` which evaluates the Rego rule `data.navigator.sandbox.matched_endpoint_config`. This rule only matches endpoints that have a `protocol` field set (see `sandbox-policy.rego` line `ep.protocol`). If a config is returned, the proxy enters `relay_with_inspection()` instead of `copy_bidirectional()`. See `crates/navigator-sandbox/src/proxy.rs` -- `handle_tcp_connection()`.
|
||||
**Implementation path**: After L4 CONNECT is allowed, the proxy calls `query_l7_config()` which evaluates the Rego rule `data.openshell.sandbox.matched_endpoint_config`. This rule only matches endpoints that have a `protocol` field set (see `sandbox-policy.rego` line `ep.protocol`). If a config is returned, the proxy enters `relay_with_inspection()` instead of `copy_bidirectional()`. See `crates/openshell-sandbox/src/proxy.rs` -- `handle_tcp_connection()`.
|
||||
|
||||
**Validation requirement**: When `protocol` is set, either `rules` or `access` must also be present. An endpoint with `protocol` but no rules/access is rejected at validation time because it would deny all traffic (no allow rules means nothing matches). See `crates/navigator-sandbox/src/l7/mod.rs` -- `validate_l7_policies()`.
|
||||
**Validation requirement**: When `protocol` is set, either `rules` or `access` must also be present. An endpoint with `protocol` but no rules/access is rejected at validation time because it would deny all traffic (no allow rules means nothing matches). See `crates/openshell-sandbox/src/l7/mod.rs` -- `validate_l7_policies()`.
|
||||
|
||||
### Behavioral Trigger: Forward Proxy Mode
|
||||
|
||||
@@ -561,12 +561,12 @@ If any condition fails, the proxy returns `403 Forbidden`.
|
||||
4. Requires `allowed_ips` on the matched endpoint
|
||||
5. Resolves DNS and validates all IPs are private and within `allowed_ips`
|
||||
6. Connects to upstream
|
||||
7. Rewrites the request: absolute-form → origin-form (`GET /path HTTP/1.1`), strips hop-by-hop headers, adds `Via: 1.1 navigator-sandbox` and `Connection: close`
|
||||
7. Rewrites the request: absolute-form → origin-form (`GET /path HTTP/1.1`), strips hop-by-hop headers, adds `Via: 1.1 openshell-sandbox` and `Connection: close`
|
||||
8. Forwards the rewritten request, then relays bidirectionally using `tokio::io::copy_bidirectional` (supports chunked transfer, SSE streams, and other long-lived responses with no idle timeout)
|
||||
|
||||
**V1 simplifications**: Forward proxy v1 injects `Connection: close` (no keep-alive) and does not perform L7 inspection on the forwarded traffic. Every forward proxy connection handles exactly one request-response exchange.
|
||||
|
||||
**Implementation**: See `crates/navigator-sandbox/src/proxy.rs` -- `handle_forward_proxy()`, `parse_proxy_uri()`, `rewrite_forward_request()`.
|
||||
**Implementation**: See `crates/openshell-sandbox/src/proxy.rs` -- `handle_forward_proxy()`, `parse_proxy_uri()`, `rewrite_forward_request()`.
|
||||
|
||||
**Logging**: Forward proxy requests are logged distinctly from CONNECT:
|
||||
|
||||
@@ -631,11 +631,11 @@ resp = httpx.get("http://10.86.8.223:8000/screenshot/",
|
||||
**Prerequisites for TLS termination**:
|
||||
|
||||
- The `protocol` field must also be set. `tls: terminate` without `protocol` is rejected at validation time.
|
||||
- The sandbox supervisor generates an ephemeral CA at startup (`SandboxCa::generate()`) and writes it to `/etc/navigator-tls/`.
|
||||
- The sandbox supervisor generates an ephemeral CA at startup (`SandboxCa::generate()`) and writes it to `/etc/openshell-tls/`.
|
||||
- Trust store environment variables are set on the child process: `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`.
|
||||
- A combined CA bundle (system CAs + sandbox CA) is written to `/etc/navigator-tls/ca-bundle.pem` so `SSL_CERT_FILE` replaces the default trust store while still trusting real CAs.
|
||||
- A combined CA bundle (system CAs + sandbox CA) is written to `/etc/openshell-tls/ca-bundle.pem` so `SSL_CERT_FILE` replaces the default trust store while still trusting real CAs.
|
||||
|
||||
**Certificate caching**: Per-hostname leaf certificates are cached (up to 256 entries, then the entire cache is cleared). See `crates/navigator-sandbox/src/l7/tls.rs` -- `CertCache`.
|
||||
**Certificate caching**: Per-hostname leaf certificates are cached (up to 256 entries, then the entire cache is cleared). See `crates/openshell-sandbox/src/l7/tls.rs` -- `CertCache`.
|
||||
|
||||
**Validation warning**: When `protocol: rest` is set on port 443 without `tls: terminate`, the validator emits a warning: "L7 rules won't be evaluated on encrypted traffic without `tls: terminate`".
|
||||
|
||||
@@ -659,7 +659,7 @@ resp = httpx.get("http://10.86.8.223:8000/screenshot/",
|
||||
}
|
||||
```
|
||||
|
||||
The response includes an `X-Navigator-Policy` header and `Connection: close`. See `crates/navigator-sandbox/src/l7/rest.rs` -- `send_deny_response()`.
|
||||
The response includes an `X-OpenShell-Policy` header and `Connection: close`. See `crates/openshell-sandbox/src/l7/rest.rs` -- `send_deny_response()`.
|
||||
|
||||
**SQL restriction**: `protocol: sql` + `enforcement: enforce` is rejected at validation time because full SQL parsing is not available in v1. SQL endpoints must use `enforcement: audit`.
|
||||
|
||||
@@ -690,7 +690,7 @@ Regardless of network mode, certain socket domains are always blocked:
|
||||
|
||||
In proxy mode (which is always active), `AF_INET` (2) and `AF_INET6` (10) are allowed so the sandbox process can reach the proxy.
|
||||
|
||||
The seccomp filter uses a default-allow policy (`SeccompAction::Allow`) with specific `socket()` syscall rules that return `EPERM` when the first argument (domain) matches a blocked value. See `crates/navigator-sandbox/src/sandbox/linux/seccomp.rs`.
|
||||
The seccomp filter uses a default-allow policy (`SeccompAction::Allow`) with specific `socket()` syscall rules that return `EPERM` when the first argument (domain) matches a blocked value. See `crates/openshell-sandbox/src/sandbox/linux/seccomp.rs`.
|
||||
|
||||
---
|
||||
|
||||
@@ -706,7 +706,7 @@ When proxy mode is active (on Linux), the sandbox creates an isolated network na
|
||||
| Default route | via `10.200.0.1` | All sandbox traffic goes through the host veth |
|
||||
| Proxy port | `3128` (default) | Configurable |
|
||||
|
||||
The child process enters the namespace via `setns(fd, CLONE_NEWNET)` in `pre_exec`. This provides hard network isolation -- even if a process ignores proxy environment variables, it can only reach the host veth IP, where the proxy listens. See `crates/navigator-sandbox/src/sandbox/linux/netns.rs`.
|
||||
The child process enters the namespace via `setns(fd, CLONE_NEWNET)` in `pre_exec`. This provides hard network isolation -- even if a process ignores proxy environment variables, it can only reach the host veth IP, where the proxy listens. See `crates/openshell-sandbox/src/sandbox/linux/netns.rs`.
|
||||
|
||||
---
|
||||
|
||||
@@ -720,7 +720,7 @@ The proxy identifies which binary initiated each CONNECT request using Linux `/p
|
||||
4. **Cmdline extraction**: `/proc/{pid}/cmdline` is parsed for absolute paths to capture script names (e.g., when `node` runs `/usr/local/bin/claude`).
|
||||
5. **TOFU verification**: SHA256 hash of each binary is computed on first use and cached. Subsequent requests from the same binary path must match the cached hash. A mismatch (binary replaced mid-sandbox) triggers an immediate deny.
|
||||
|
||||
See `crates/navigator-sandbox/src/procfs.rs`, `crates/navigator-sandbox/src/identity.rs`.
|
||||
See `crates/openshell-sandbox/src/procfs.rs`, `crates/openshell-sandbox/src/identity.rs`.
|
||||
|
||||
---
|
||||
|
||||
@@ -797,7 +797,7 @@ The following validation rules are enforced during policy loading (both file mod
|
||||
|
||||
### Errors (Live Update Rejection)
|
||||
|
||||
These errors are returned by the gateway's `UpdateSandboxPolicy` handler and reject the update before it is persisted. See `crates/navigator-server/src/grpc.rs`.
|
||||
These errors are returned by the gateway's `UpdateSandboxPolicy` handler and reject the update before it is persisted. See `crates/openshell-server/src/grpc.rs`.
|
||||
|
||||
| Condition | Error Message |
|
||||
|-----------|---------------|
|
||||
@@ -814,7 +814,7 @@ These errors are returned by the gateway's `UpdateSandboxPolicy` handler and rej
|
||||
| `protocol: rest` on port 443 without `tls: terminate` | `L7 rules won't be evaluated on encrypted traffic without tls: terminate` |
|
||||
| Unknown HTTP method in rules (not GET/HEAD/POST/PUT/DELETE/PATCH/OPTIONS/\*) | `Unknown HTTP method '{method}'. Standard methods: GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS.` |
|
||||
|
||||
See `crates/navigator-sandbox/src/l7/mod.rs` -- `validate_l7_policies()`.
|
||||
See `crates/openshell-sandbox/src/l7/mod.rs` -- `validate_l7_policies()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -849,7 +849,7 @@ These ranges are blocked by default but can be selectively allowed via the `allo
|
||||
|
||||
### Implementation
|
||||
|
||||
Functions in `crates/navigator-sandbox/src/proxy.rs` implement the SSRF checks:
|
||||
Functions in `crates/openshell-sandbox/src/proxy.rs` implement the SSRF checks:
|
||||
|
||||
- **`is_internal_ip(ip: IpAddr) -> bool`**: Classifies an IP address as internal or public. Checks loopback, link-local, and RFC 1918 ranges. For IPv6, unwraps IPv4-mapped addresses (`::ffff:x.x.x.x`) via `to_ipv4_mapped()` and applies IPv4 checks. Used in the default (no `allowed_ips`) code path.
|
||||
|
||||
@@ -1122,7 +1122,7 @@ When the gateway delivers policy via gRPC, the protobuf `SandboxPolicy` message
|
||||
| `L7Rule` | `allow` | `rules[].allow` |
|
||||
| `L7Allow` | `method`, `path`, `command` | `rules[].allow.method`, `.path`, `.command` |
|
||||
|
||||
The conversion is performed in `crates/navigator-sandbox/src/opa.rs` -- `proto_to_opa_data_json()`.
|
||||
The conversion is performed in `crates/openshell-sandbox/src/opa.rs` -- `proto_to_opa_data_json()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -1135,7 +1135,7 @@ The sandbox supervisor applies enforcement mechanisms in a specific order during
|
||||
3. **Landlock** -- Filesystem access rules are applied
|
||||
4. **Seccomp** -- Socket domain restrictions are applied
|
||||
|
||||
This ordering is intentional: privilege dropping needs `/etc/group` and `/etc/passwd` access, which Landlock may subsequently restrict. Network namespace entry must happen before any network operations. See `crates/navigator-sandbox/src/process.rs` -- `spawn_impl()`.
|
||||
This ordering is intentional: privilege dropping needs `/etc/group` and `/etc/passwd` access, which Landlock may subsequently restrict. Network namespace entry must happen before any network operations. See `crates/openshell-sandbox/src/process.rs` -- `spawn_impl()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -1190,7 +1190,7 @@ nav logs my-sandbox --source sandbox
|
||||
nav logs my-sandbox --source gateway
|
||||
```
|
||||
|
||||
The filter applies to both one-shot mode (`GetSandboxLogs` RPC) and streaming mode (`--tail`, via `WatchSandbox` RPC). In both cases, the server evaluates `source_matches()` before sending each log line to the client. See `crates/navigator-server/src/grpc.rs` -- `source_matches()`, `get_sandbox_logs()`.
|
||||
The filter applies to both one-shot mode (`GetSandboxLogs` RPC) and streaming mode (`--tail`, via `WatchSandbox` RPC). In both cases, the server evaluates `source_matches()` before sending each log line to the client. See `crates/openshell-server/src/grpc.rs` -- `source_matches()`, `get_sandbox_logs()`.
|
||||
|
||||
### Level Filter (`--level`)
|
||||
|
||||
@@ -1214,7 +1214,7 @@ nav logs my-sandbox --level warn
|
||||
nav logs my-sandbox --source sandbox --level error
|
||||
```
|
||||
|
||||
The filter is applied server-side via `level_matches()` in both one-shot and streaming modes. See `crates/navigator-server/src/grpc.rs` -- `level_matches()`.
|
||||
The filter is applied server-side via `level_matches()` in both one-shot and streaming modes. See `crates/openshell-server/src/grpc.rs` -- `level_matches()`.
|
||||
|
||||
### Proto Messages
|
||||
|
||||
@@ -1225,7 +1225,7 @@ The source and level filters are carried in both log-related RPC messages:
|
||||
| `GetSandboxLogs` | `GetSandboxLogsRequest` | `repeated string sources` | `string min_level` |
|
||||
| `WatchSandbox` | `WatchSandboxRequest` | `repeated string log_sources` | `string log_min_level` |
|
||||
|
||||
An empty `sources`/`log_sources` list means no source filtering (all sources pass). An empty `min_level`/`log_min_level` string means no level filtering (all levels pass). See `proto/navigator.proto`.
|
||||
An empty `sources`/`log_sources` list means no source filtering (all sources pass). An empty `min_level`/`log_min_level` string means no level filtering (all levels pass). See `proto/openshell.proto`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ graph TB
|
||||
LocalPathProv["local-path-provisioner"]
|
||||
end
|
||||
|
||||
subgraph NSNamespace["navigator namespace"]
|
||||
subgraph NSNamespace["openshell namespace"]
|
||||
|
||||
subgraph GatewayPod["Gateway StatefulSet"]
|
||||
Gateway["navigator-server<br/>:8080<br/>(gRPC + HTTP, mTLS)"]
|
||||
SQLite[("SQLite DB<br/>/var/navigator/<br/>navigator.db")]
|
||||
Gateway["openshell-server<br/>:8080<br/>(gRPC + HTTP, mTLS)"]
|
||||
SQLite[("SQLite DB<br/>/var/openshell/<br/>openshell.db")]
|
||||
SandboxWatcher["Sandbox Watcher"]
|
||||
KubeEventTailer["Kube Event Tailer"]
|
||||
WatchBus["SandboxWatchBus<br/>(in-memory broadcast)"]
|
||||
@@ -40,7 +40,7 @@ graph TB
|
||||
SSHServer["Embedded SSH<br/>Server (russh)<br/>:2222"]
|
||||
Proxy["HTTP CONNECT<br/>Proxy<br/>10.200.0.1:3128"]
|
||||
OPA["OPA Policy Engine<br/>(regorus, in-process)"]
|
||||
InferenceRouter["Inference Router<br/>(navigator-router)"]
|
||||
InferenceRouter["Inference Router<br/>(openshell-router)"]
|
||||
CertCache["TLS MITM<br/>Cert Cache"]
|
||||
end
|
||||
|
||||
@@ -178,7 +178,7 @@ graph TB
|
||||
| Color | Category | Examples |
|
||||
|-------|----------|---------|
|
||||
| Blue | User-side components | OpenShell CLI, OpenShell TUI, Python SDK |
|
||||
| Orange | Gateway / Control plane | navigator-server, watch bus, log bus |
|
||||
| Orange | Gateway / Control plane | openshell-server, watch bus, log bus |
|
||||
| Green | Sandbox supervisor | SSH server, HTTP CONNECT proxy, OPA engine, inference router |
|
||||
| Purple | Agent process & isolation | AI agent, Landlock, Seccomp, network namespace |
|
||||
| Indigo | Data stores | SQLite database |
|
||||
@@ -195,6 +195,6 @@ graph TB
|
||||
|
||||
4. **Sandbox to External**: All agent outbound traffic is forced through the HTTP CONNECT proxy (10.200.0.1:3128) via a network namespace veth pair. OPA/Rego policies evaluate every connection. Optional TLS MITM enables L7 inspection.
|
||||
|
||||
5. **Inference Routing**: Inference requests are handled inside the sandbox by the navigator-router (not through the gateway). The gateway provides route configuration and credentials via gRPC; the sandbox executes HTTP requests directly to inference backends.
|
||||
5. **Inference Routing**: Inference requests are handled inside the sandbox by the openshell-router (not through the gateway). The gateway provides route configuration and credentials via gRPC; the sandbox executes HTTP requests directly to inference backends.
|
||||
|
||||
6. **Sandbox to Gateway**: The sandbox supervisor uses gRPC (mTLS) to fetch policies, provider credentials, inference bundles, and to push logs back to the gateway.
|
||||
|
||||
+3
-3
@@ -139,7 +139,7 @@ Port forwarding lifecycle:
|
||||
- **On delete**: Any active forwards for the sandbox are automatically stopped before deletion.
|
||||
- **PID tracking**: Forward PIDs are stored in `~/.config/openshell/forwards/<name>-<port>.pid`, shared with the CLI.
|
||||
|
||||
The forwarding implementation lives in `navigator-core::forward`, shared between the CLI and TUI.
|
||||
The forwarding implementation lives in `openshell-core::forward`, shared between the CLI and TUI.
|
||||
|
||||
## What is Not Yet Available
|
||||
|
||||
@@ -152,6 +152,6 @@ The TUI is in its initial phase. The following features are planned but not yet
|
||||
|
||||
## Crate Structure
|
||||
|
||||
The TUI lives in `crates/navigator-tui/`, a separate workspace crate. The CLI crate (`crates/navigator-cli/`) depends on it and launches it via the `Term` command variant in the `Commands` enum. This keeps TUI-specific dependencies (ratatui, crossterm) out of the CLI when not in use.
|
||||
The TUI lives in `crates/openshell-tui/`, a separate workspace crate. The CLI crate (`crates/openshell-cli/`) depends on it and launches it via the `Term` command variant in the `Commands` enum. This keeps TUI-specific dependencies (ratatui, crossterm) out of the CLI when not in use.
|
||||
|
||||
The `navigator-tui` crate depends on `navigator-core` for protobuf types, the gRPC client, and shared utilities (e.g., `navigator_core::forward` for port forwarding PID management) — it communicates with the gateway over the same gRPC channel the CLI uses.
|
||||
The `openshell-tui` crate depends on `openshell-core` for protobuf types, the gRPC client, and shared utilities (e.g., `openshell_core::forward` for port forwarding PID management) — it communicates with the gateway over the same gRPC channel the CLI uses.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "navigator-bootstrap"
|
||||
name = "openshell-bootstrap"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "navigator-cli"
|
||||
name = "openshell-cli"
|
||||
description = "OpenShell CLI tool"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -15,11 +15,11 @@ name = "openshell"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
navigator-bootstrap = { path = "../navigator-bootstrap" }
|
||||
navigator-core = { path = "../navigator-core" }
|
||||
navigator-policy = { path = "../navigator-policy" }
|
||||
navigator-providers = { path = "../navigator-providers" }
|
||||
navigator-tui = { path = "../navigator-tui" }
|
||||
openshell-bootstrap = { path = "../openshell-bootstrap" }
|
||||
openshell-core = { path = "../openshell-core" }
|
||||
openshell-policy = { path = "../openshell-policy" }
|
||||
openshell-providers = { path = "../openshell-providers" }
|
||||
openshell-tui = { path = "../openshell-tui" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
@@ -143,9 +143,9 @@ pub async fn run_bootstrap(
|
||||
);
|
||||
eprintln!();
|
||||
|
||||
let mut options = navigator_bootstrap::DeployOptions::new(&gateway_name);
|
||||
let mut options = openshell_bootstrap::DeployOptions::new(&gateway_name);
|
||||
if let Some(dest) = remote {
|
||||
let mut remote_opts = navigator_bootstrap::RemoteOptions::new(dest);
|
||||
let mut remote_opts = openshell_bootstrap::RemoteOptions::new(dest);
|
||||
if let Some(key) = ssh_key {
|
||||
remote_opts = remote_opts.with_ssh_key(key);
|
||||
}
|
||||
@@ -166,7 +166,7 @@ pub async fn run_bootstrap(
|
||||
print_deploy_summary(&gateway_name, &handle);
|
||||
|
||||
// Auto-activate the bootstrapped gateway.
|
||||
if let Err(err) = navigator_bootstrap::save_active_gateway(&gateway_name) {
|
||||
if let Err(err) = openshell_bootstrap::save_active_gateway(&gateway_name) {
|
||||
tracing::debug!("failed to set active gateway after bootstrap: {err}");
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap_complete::engine::CompletionCandidate;
|
||||
use navigator_bootstrap::{list_gateways, load_active_gateway, load_gateway_metadata};
|
||||
use navigator_core::proto::navigator_client::NavigatorClient;
|
||||
use navigator_core::proto::{ListProvidersRequest, ListSandboxesRequest};
|
||||
use openshell_bootstrap::{list_gateways, load_active_gateway, load_gateway_metadata};
|
||||
use openshell_core::proto::open_shell_client::OpenShellClient;
|
||||
use openshell_core::proto::{ListProvidersRequest, ListSandboxesRequest};
|
||||
use tonic::transport::{Channel, Endpoint};
|
||||
|
||||
use crate::tls::{TlsOptions, build_tonic_tls_config, require_tls_materials};
|
||||
@@ -82,7 +82,7 @@ fn resolve_active_gateway() -> Option<(String, String)> {
|
||||
async fn completion_grpc_client(
|
||||
server: &str,
|
||||
gateway_name: &str,
|
||||
) -> Option<NavigatorClient<Channel>> {
|
||||
) -> Option<OpenShellClient<Channel>> {
|
||||
let tls_opts = TlsOptions::default().with_gateway_name(gateway_name);
|
||||
let materials = require_tls_materials(server, &tls_opts).ok()?;
|
||||
let tls_config = build_tonic_tls_config(&materials);
|
||||
@@ -92,7 +92,7 @@ async fn completion_grpc_client(
|
||||
.tls_config(tls_config)
|
||||
.ok()?;
|
||||
let channel = endpoint.connect().await.ok()?;
|
||||
Some(NavigatorClient::new(channel))
|
||||
Some(OpenShellClient::new(channel))
|
||||
}
|
||||
|
||||
/// Run an async future on a dedicated thread to avoid nested tokio runtime panics.
|
||||
@@ -121,7 +121,7 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::TEST_ENV_LOCK;
|
||||
use navigator_bootstrap::{GatewayMetadata, store_gateway_metadata};
|
||||
use openshell_bootstrap::{GatewayMetadata, store_gateway_metadata};
|
||||
use temp_env::with_vars;
|
||||
|
||||
fn with_isolated_cli_env<F: FnOnce()>(tmp: &std::path::Path, f: F) {
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
<!-- Derived from .agents/skills/debug-navigator-cluster/SKILL.md -->
|
||||
<!-- Derived from .agents/skills/debug-openshell-cluster/SKILL.md -->
|
||||
<!-- Keep in sync when updating cluster debug procedures -->
|
||||
|
||||
# Debug OpenShell Gateway
|
||||
@@ -197,7 +197,7 @@ If `registries.yaml` is missing or has wrong values, verify env wiring (`OPENSHE
|
||||
|
||||
### Step 6: Check mTLS / PKI
|
||||
|
||||
TLS certificates are generated by the `navigator-bootstrap` crate (using `rcgen`) and stored as K8s secrets before the Helm release installs. There is no PKI job or cert-manager — certificates are applied directly via `kubectl apply`.
|
||||
TLS certificates are generated by the `openshell-bootstrap` crate (using `rcgen`) and stored as K8s secrets before the Helm release installs. There is no PKI job or cert-manager — certificates are applied directly via `kubectl apply`.
|
||||
|
||||
```bash
|
||||
# Check if the three TLS secrets exist
|
||||
@@ -10,13 +10,13 @@ use miette::Result;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::io::Write;
|
||||
|
||||
use navigator_bootstrap::{
|
||||
use openshell_bootstrap::{
|
||||
edge_token::load_edge_token, get_gateway_metadata, list_gateways, load_active_gateway,
|
||||
load_gateway_metadata, load_last_sandbox, save_last_sandbox,
|
||||
};
|
||||
use navigator_cli::completers;
|
||||
use navigator_cli::run;
|
||||
use navigator_cli::tls::TlsOptions;
|
||||
use openshell_cli::completers;
|
||||
use openshell_cli::run;
|
||||
use openshell_cli::tls::TlsOptions;
|
||||
|
||||
/// Resolved gateway context: name + gateway endpoint.
|
||||
struct GatewayContext {
|
||||
@@ -453,7 +453,7 @@ enum Commands {
|
||||
Term {
|
||||
/// Color theme for the TUI: auto, dark, or light.
|
||||
#[arg(long, default_value = "auto", env = "OPENSHELL_THEME")]
|
||||
theme: navigator_tui::ThemeMode,
|
||||
theme: openshell_tui::ThemeMode,
|
||||
},
|
||||
|
||||
/// Generate shell completions.
|
||||
@@ -591,7 +591,7 @@ enum CliEditor {
|
||||
Cursor,
|
||||
}
|
||||
|
||||
impl From<CliEditor> for navigator_cli::ssh::Editor {
|
||||
impl From<CliEditor> for openshell_cli::ssh::Editor {
|
||||
fn from(value: CliEditor) -> Self {
|
||||
match value {
|
||||
CliEditor::Vscode => Self::Vscode,
|
||||
@@ -726,7 +726,7 @@ enum GatewayCommands {
|
||||
ssh_key: Option<String>,
|
||||
|
||||
/// Host port to map to the gateway (default: 8080).
|
||||
#[arg(long, default_value_t = navigator_bootstrap::DEFAULT_GATEWAY_PORT)]
|
||||
#[arg(long, default_value_t = openshell_bootstrap::DEFAULT_GATEWAY_PORT)]
|
||||
port: u16,
|
||||
|
||||
/// Override the gateway host written into cluster metadata.
|
||||
@@ -2065,8 +2065,8 @@ async fn main() -> Result<()> {
|
||||
let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
|
||||
let mut tls = tls.with_gateway_name(&ctx.name);
|
||||
apply_edge_auth(&mut tls, &ctx.name);
|
||||
let channel = navigator_cli::tls::build_channel(&ctx.endpoint, &tls).await?;
|
||||
navigator_tui::run(channel, &ctx.name, &ctx.endpoint, theme).await?;
|
||||
let channel = openshell_cli::tls::build_channel(&ctx.endpoint, &tls).await?;
|
||||
openshell_tui::run(channel, &ctx.name, &ctx.endpoint, theme).await?;
|
||||
}
|
||||
Some(Commands::Completions { shell }) => {
|
||||
let exe = std::env::current_exe()
|
||||
@@ -2208,7 +2208,7 @@ fn parse_upload_spec(spec: &str) -> (String, Option<String>) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use navigator_bootstrap::{
|
||||
use openshell_bootstrap::{
|
||||
GatewayMetadata, edge_token::store_edge_token, store_gateway_metadata,
|
||||
};
|
||||
use std::ffi::OsString;
|
||||
@@ -15,13 +15,13 @@ use hyper_rustls::HttpsConnectorBuilder;
|
||||
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use navigator_bootstrap::{
|
||||
use openshell_bootstrap::{
|
||||
DeployOptions, GatewayMetadata, RemoteOptions, clear_active_gateway, container_name,
|
||||
extract_host_from_ssh_destination, get_gateway_metadata, list_gateways, load_active_gateway,
|
||||
remove_gateway_metadata, resolve_ssh_hostname, save_active_gateway, save_last_sandbox,
|
||||
store_gateway_metadata,
|
||||
};
|
||||
use navigator_core::proto::{
|
||||
use openshell_core::proto::{
|
||||
ApproveAllDraftChunksRequest, ApproveDraftChunkRequest, ClearDraftChunksRequest,
|
||||
CreateProviderRequest, CreateSandboxRequest, DeleteProviderRequest, DeleteSandboxRequest,
|
||||
GetClusterInferenceRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, GetProviderRequest,
|
||||
@@ -31,7 +31,7 @@ use navigator_core::proto::{
|
||||
SetClusterInferenceRequest, UpdateProviderRequest, UpdateSandboxPolicyRequest,
|
||||
WatchSandboxRequest,
|
||||
};
|
||||
use navigator_providers::{
|
||||
use openshell_providers::{
|
||||
ProviderRegistry, detect_provider_from_command, normalize_provider_type,
|
||||
};
|
||||
use owo_colors::OwoColorize;
|
||||
@@ -48,7 +48,7 @@ pub use crate::ssh::{
|
||||
sandbox_connect, sandbox_connect_editor, sandbox_exec, sandbox_forward, sandbox_ssh_proxy,
|
||||
sandbox_ssh_proxy_by_name, sandbox_sync_down, sandbox_sync_up, sandbox_sync_up_files,
|
||||
};
|
||||
pub use navigator_core::forward::{list_forwards, stop_forward, stop_forwards_for_sandbox};
|
||||
pub use openshell_core::forward::{list_forwards, stop_forward, stop_forwards_for_sandbox};
|
||||
|
||||
/// Convert a sandbox phase integer to a human-readable string.
|
||||
fn phase_name(phase: i32) -> &'static str {
|
||||
@@ -961,7 +961,7 @@ pub async fn gateway_add(
|
||||
// the same Docker host.
|
||||
let endpoint_port = url::Url::parse(&endpoint).ok().and_then(|u| u.port());
|
||||
eprintln!("• Extracting TLS certificates from gateway container...");
|
||||
navigator_bootstrap::extract_and_store_pki(name, remote_opts.as_ref(), endpoint_port)
|
||||
openshell_bootstrap::extract_and_store_pki(name, remote_opts.as_ref(), endpoint_port)
|
||||
.await?;
|
||||
|
||||
let (remote_host, resolved_host) = if let Some(dest) = remote {
|
||||
@@ -1027,7 +1027,7 @@ pub async fn gateway_add(
|
||||
|
||||
match crate::auth::browser_auth_flow(&endpoint).await {
|
||||
Ok(token) => {
|
||||
navigator_bootstrap::edge_token::store_edge_token(name, &token)?;
|
||||
openshell_bootstrap::edge_token::store_edge_token(name, &token)?;
|
||||
eprintln!("{} Authenticated successfully", "✓".green().bold());
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1047,7 +1047,7 @@ pub async fn gateway_add(
|
||||
///
|
||||
/// Opens a browser for edge proxy login and stores the updated token.
|
||||
pub async fn gateway_login(name: &str) -> Result<()> {
|
||||
let metadata = navigator_bootstrap::load_gateway_metadata(name).map_err(|_| {
|
||||
let metadata = openshell_bootstrap::load_gateway_metadata(name).map_err(|_| {
|
||||
miette::miette!(
|
||||
"Unknown gateway '{name}'.\n\
|
||||
List available gateways: openshell gateway select"
|
||||
@@ -1062,7 +1062,7 @@ pub async fn gateway_login(name: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
let token = crate::auth::browser_auth_flow(&metadata.gateway_endpoint).await?;
|
||||
navigator_bootstrap::edge_token::store_edge_token(name, &token)?;
|
||||
openshell_bootstrap::edge_token::store_edge_token(name, &token)?;
|
||||
|
||||
eprintln!("{} Authenticated to gateway '{name}'", "✓".green().bold(),);
|
||||
|
||||
@@ -1174,7 +1174,7 @@ async fn http_health_check(server: &str, tls: &TlsOptions) -> Result<Option<Stat
|
||||
/// Returns `true` to recreate (destroy and start fresh), `false` to reuse.
|
||||
fn prompt_existing_gateway(
|
||||
name: &str,
|
||||
info: &navigator_bootstrap::ExistingGatewayInfo,
|
||||
info: &openshell_bootstrap::ExistingGatewayInfo,
|
||||
) -> Result<bool> {
|
||||
let status = if info.container_running {
|
||||
"running"
|
||||
@@ -1212,7 +1212,7 @@ pub(crate) async fn deploy_gateway_with_panel(
|
||||
options: DeployOptions,
|
||||
name: &str,
|
||||
location: &str,
|
||||
) -> Result<navigator_bootstrap::GatewayHandle> {
|
||||
) -> Result<openshell_bootstrap::GatewayHandle> {
|
||||
let interactive = std::io::stderr().is_terminal();
|
||||
|
||||
if interactive {
|
||||
@@ -1220,7 +1220,7 @@ pub(crate) async fn deploy_gateway_with_panel(
|
||||
name, location,
|
||||
)));
|
||||
let panel_clone = std::sync::Arc::clone(&panel);
|
||||
let result = navigator_bootstrap::deploy_gateway_with_logs(options, move |line| {
|
||||
let result = openshell_bootstrap::deploy_gateway_with_logs(options, move |line| {
|
||||
if let Ok(mut p) = panel_clone.lock() {
|
||||
p.push_log(line);
|
||||
}
|
||||
@@ -1247,7 +1247,7 @@ pub(crate) async fn deploy_gateway_with_panel(
|
||||
// Try to diagnose the failure and provide guidance
|
||||
let err_str = format!("{err:?}");
|
||||
if let Some(diagnosis) =
|
||||
navigator_bootstrap::errors::diagnose_failure(name, &err_str, None)
|
||||
openshell_bootstrap::errors::diagnose_failure(name, &err_str, None)
|
||||
{
|
||||
print_failure_diagnosis(&diagnosis);
|
||||
}
|
||||
@@ -1256,7 +1256,7 @@ pub(crate) async fn deploy_gateway_with_panel(
|
||||
}
|
||||
} else {
|
||||
eprintln!("Deploying {location} gateway {name}...");
|
||||
let handle = navigator_bootstrap::deploy_gateway_with_logs(options, |line| {
|
||||
let handle = openshell_bootstrap::deploy_gateway_with_logs(options, |line| {
|
||||
if let Some(status) = line.strip_prefix("[status] ") {
|
||||
eprintln!(" {status}");
|
||||
} else if line.strip_prefix("[progress] ").is_some() {
|
||||
@@ -1272,7 +1272,7 @@ pub(crate) async fn deploy_gateway_with_panel(
|
||||
}
|
||||
|
||||
/// Print post-deploy summary showing the gateway name and endpoint.
|
||||
pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::GatewayHandle) {
|
||||
pub(crate) fn print_deploy_summary(name: &str, handle: &openshell_bootstrap::GatewayHandle) {
|
||||
eprintln!();
|
||||
eprintln!("{} Gateway ready", "✓".green().bold());
|
||||
eprintln!();
|
||||
@@ -1282,7 +1282,7 @@ pub(crate) fn print_deploy_summary(name: &str, handle: &navigator_bootstrap::Gat
|
||||
}
|
||||
|
||||
/// Print a user-friendly failure diagnosis with recovery steps.
|
||||
fn print_failure_diagnosis(diagnosis: &navigator_bootstrap::errors::GatewayFailureDiagnosis) {
|
||||
fn print_failure_diagnosis(diagnosis: &openshell_bootstrap::errors::GatewayFailureDiagnosis) {
|
||||
eprintln!();
|
||||
eprintln!("{}", diagnosis.summary.yellow().bold());
|
||||
eprintln!();
|
||||
@@ -1350,7 +1350,7 @@ pub async fn gateway_admin_deploy(
|
||||
opts
|
||||
});
|
||||
if let Some(info) =
|
||||
navigator_bootstrap::check_existing_deployment(name, remote_opts.as_ref()).await?
|
||||
openshell_bootstrap::check_existing_deployment(name, remote_opts.as_ref()).await?
|
||||
{
|
||||
let should_recreate = if recreate {
|
||||
true
|
||||
@@ -1363,7 +1363,7 @@ pub async fn gateway_admin_deploy(
|
||||
if should_recreate {
|
||||
eprintln!("• Destroying existing gateway...");
|
||||
let handle =
|
||||
navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
|
||||
openshell_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
|
||||
handle.destroy().await?;
|
||||
eprintln!("{} Gateway destroyed, starting fresh.", "✓".green().bold());
|
||||
eprintln!();
|
||||
@@ -1439,7 +1439,7 @@ fn gateway_control_target_options(
|
||||
}
|
||||
|
||||
fn remove_gateway_registration(name: &str) {
|
||||
if let Err(err) = navigator_bootstrap::edge_token::remove_edge_token(name) {
|
||||
if let Err(err) = openshell_bootstrap::edge_token::remove_edge_token(name) {
|
||||
tracing::debug!("failed to remove edge token: {err}");
|
||||
}
|
||||
if let Err(err) = remove_gateway_metadata(name) {
|
||||
@@ -1453,7 +1453,7 @@ fn remove_gateway_registration(name: &str) {
|
||||
}
|
||||
|
||||
fn cleanup_gateway_metadata(name: &str) {
|
||||
if let Err(err) = navigator_bootstrap::edge_token::remove_edge_token(name) {
|
||||
if let Err(err) = openshell_bootstrap::edge_token::remove_edge_token(name) {
|
||||
tracing::debug!("failed to remove edge token: {err}");
|
||||
}
|
||||
if let Err(err) = remove_gateway_metadata(name) {
|
||||
@@ -1475,7 +1475,7 @@ pub async fn gateway_admin_stop(
|
||||
let remote_opts = gateway_control_target_options(name, remote, ssh_key)?;
|
||||
|
||||
eprintln!("• Stopping gateway {name}...");
|
||||
let handle = navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
|
||||
let handle = openshell_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
|
||||
handle.stop().await?;
|
||||
eprintln!("{} Gateway {name} stopped.", "✓".green().bold());
|
||||
Ok(())
|
||||
@@ -1501,7 +1501,7 @@ pub async fn gateway_admin_destroy(
|
||||
let remote_opts = gateway_control_target_options(name, remote, ssh_key)?;
|
||||
|
||||
eprintln!("• Destroying gateway {name}...");
|
||||
let handle = navigator_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
|
||||
let handle = openshell_bootstrap::gateway_handle(name, remote_opts.as_ref()).await?;
|
||||
handle.destroy().await?;
|
||||
|
||||
cleanup_gateway_metadata(name);
|
||||
@@ -1576,7 +1576,7 @@ pub async fn doctor_logs(
|
||||
};
|
||||
|
||||
let stdout = std::io::stdout().lock();
|
||||
navigator_bootstrap::gateway_container_logs(remote_opts.as_ref(), name, lines, tail, stdout)
|
||||
openshell_bootstrap::gateway_container_logs(remote_opts.as_ref(), name, lines, tail, stdout)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1970,7 +1970,7 @@ pub async fn sandbox_create(
|
||||
|
||||
let evt = item.into_diagnostic()?;
|
||||
match evt.payload {
|
||||
Some(navigator_core::proto::sandbox_stream_event::Payload::Sandbox(s)) => {
|
||||
Some(openshell_core::proto::sandbox_stream_event::Payload::Sandbox(s)) => {
|
||||
let phase = SandboxPhase::try_from(s.phase).unwrap_or(SandboxPhase::Unknown);
|
||||
last_phase = s.phase;
|
||||
|
||||
@@ -2002,13 +2002,13 @@ pub async fn sandbox_create(
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(navigator_core::proto::sandbox_stream_event::Payload::Log(line)) => {
|
||||
Some(openshell_core::proto::sandbox_stream_event::Payload::Log(line)) => {
|
||||
// Detect gateway readiness from log messages.
|
||||
if !saw_gateway_ready && line.message.contains("listening") {
|
||||
saw_gateway_ready = true;
|
||||
}
|
||||
}
|
||||
Some(navigator_core::proto::sandbox_stream_event::Payload::Event(ev)) => {
|
||||
Some(openshell_core::proto::sandbox_stream_event::Payload::Event(ev)) => {
|
||||
// Map Kubernetes events to provisioning steps.
|
||||
// We simplify the display to: Sandbox allocated -> Pulling image -> Ready
|
||||
if let Some(reason) = parse_kube_event_reason(&ev.reason) {
|
||||
@@ -2085,7 +2085,7 @@ pub async fn sandbox_create(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(navigator_core::proto::sandbox_stream_event::Payload::Warning(w)) => {
|
||||
Some(openshell_core::proto::sandbox_stream_event::Payload::Warning(w)) => {
|
||||
if let Some(d) = display.as_mut() {
|
||||
d.println(&format!(" {} {}", "!".yellow().bold(), w.message.yellow()));
|
||||
} else {
|
||||
@@ -2093,7 +2093,7 @@ pub async fn sandbox_create(
|
||||
eprintln!(" {} {} {}", ts.dimmed(), "WARN".yellow(), w.message);
|
||||
}
|
||||
}
|
||||
Some(navigator_core::proto::sandbox_stream_event::Payload::DraftPolicyUpdate(_)) => {
|
||||
Some(openshell_core::proto::sandbox_stream_event::Payload::DraftPolicyUpdate(_)) => {
|
||||
// Draft policy updates are handled in the draft panel, not during provisioning.
|
||||
}
|
||||
None => {}
|
||||
@@ -2371,7 +2371,7 @@ async fn build_from_dockerfile(
|
||||
eprintln!(" {msg}");
|
||||
};
|
||||
|
||||
navigator_bootstrap::build::build_and_push_image(
|
||||
openshell_bootstrap::build::build_and_push_image(
|
||||
dockerfile,
|
||||
&tag,
|
||||
context,
|
||||
@@ -2398,7 +2398,7 @@ async fn build_from_dockerfile(
|
||||
/// Returns `None` when no policy source is configured, allowing the server
|
||||
/// to apply its own default.
|
||||
fn load_sandbox_policy(cli_path: Option<&str>) -> Result<Option<SandboxPolicy>> {
|
||||
navigator_policy::load_sandbox_policy(cli_path)
|
||||
openshell_policy::load_sandbox_policy(cli_path)
|
||||
}
|
||||
|
||||
/// Sync files to or from a sandbox.
|
||||
@@ -2523,7 +2523,7 @@ fn print_yaml_line(line: &str) {
|
||||
fn print_sandbox_policy(policy: &SandboxPolicy) {
|
||||
println!("{}", "Policy:".cyan().bold());
|
||||
println!();
|
||||
if let Ok(yaml_str) = navigator_policy::serialize_sandbox_policy(policy) {
|
||||
if let Ok(yaml_str) = openshell_policy::serialize_sandbox_policy(policy) {
|
||||
// Indent the YAML output and skip the initial "---" line
|
||||
for line in yaml_str.lines() {
|
||||
if line == "---" {
|
||||
@@ -3716,7 +3716,7 @@ pub async fn sandbox_policy_get(
|
||||
if full {
|
||||
if let Some(ref policy) = rev.policy {
|
||||
println!("---");
|
||||
let yaml_str = navigator_policy::serialize_sandbox_policy(policy)
|
||||
let yaml_str = openshell_policy::serialize_sandbox_policy(policy)
|
||||
.wrap_err("failed to serialize policy to YAML")?;
|
||||
print!("{yaml_str}");
|
||||
} else {
|
||||
@@ -3851,7 +3851,7 @@ pub async fn sandbox_logs(
|
||||
|
||||
while let Some(event) = stream.next().await {
|
||||
let event = event.into_diagnostic()?;
|
||||
if let Some(navigator_core::proto::sandbox_stream_event::Payload::Log(log)) =
|
||||
if let Some(openshell_core::proto::sandbox_stream_event::Payload::Log(log)) =
|
||||
event.payload
|
||||
{
|
||||
print_log_line(&log);
|
||||
@@ -3887,7 +3887,7 @@ pub async fn sandbox_logs(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_log_line(log: &navigator_core::proto::SandboxLogLine) {
|
||||
fn print_log_line(log: &openshell_core::proto::SandboxLogLine) {
|
||||
let source = if log.source.is_empty() {
|
||||
"gateway"
|
||||
} else {
|
||||
@@ -4152,7 +4152,7 @@ pub async fn sandbox_draft_history(server: &str, name: &str, tls: &TlsOptions) -
|
||||
}
|
||||
|
||||
/// Format a `NetworkPolicyRule`'s endpoints as a compact string.
|
||||
fn format_endpoints(rule: &navigator_core::proto::NetworkPolicyRule) -> String {
|
||||
fn format_endpoints(rule: &openshell_core::proto::NetworkPolicyRule) -> String {
|
||||
rule.endpoints
|
||||
.iter()
|
||||
.map(|e| {
|
||||
@@ -4192,7 +4192,7 @@ mod tests {
|
||||
};
|
||||
use crate::TEST_ENV_LOCK;
|
||||
use hyper::StatusCode;
|
||||
use navigator_bootstrap::{load_active_gateway, store_gateway_metadata};
|
||||
use openshell_bootstrap::{load_active_gateway, store_gateway_metadata};
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
@@ -4200,7 +4200,7 @@ mod tests {
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
|
||||
use navigator_bootstrap::GatewayMetadata;
|
||||
use openshell_bootstrap::GatewayMetadata;
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
use crate::tls::{TlsOptions, build_rustls_config, grpc_client, require_tls_materials};
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use navigator_core::forward::{
|
||||
find_ssh_forward_pid, resolve_ssh_gateway, shell_escape, write_forward_pid,
|
||||
};
|
||||
use navigator_core::proto::{CreateSshSessionRequest, GetSandboxRequest};
|
||||
#[cfg(unix)]
|
||||
use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
|
||||
use openshell_core::forward::{
|
||||
find_ssh_forward_pid, resolve_ssh_gateway, shell_escape, write_forward_pid,
|
||||
};
|
||||
use openshell_core::proto::{CreateSshSessionRequest, GetSandboxRequest};
|
||||
use owo_colors::OwoColorize;
|
||||
use rustls::pki_types::ServerName;
|
||||
use std::fs;
|
||||
@@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use navigator_core::proto::inference_client::InferenceClient;
|
||||
use navigator_core::proto::navigator_client::NavigatorClient;
|
||||
use openshell_core::proto::inference_client::InferenceClient;
|
||||
use openshell_core::proto::open_shell_client::OpenShellClient;
|
||||
use rustls::{
|
||||
RootCertStore,
|
||||
pki_types::{CertificateDer, PrivateKeyDer},
|
||||
@@ -20,7 +20,7 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity
|
||||
use tracing::debug;
|
||||
|
||||
/// Concrete gRPC client type used by all commands.
|
||||
pub type GrpcClient = NavigatorClient<InterceptedService<Channel, EdgeAuthInterceptor>>;
|
||||
pub type GrpcClient = OpenShellClient<InterceptedService<Channel, EdgeAuthInterceptor>>;
|
||||
/// Concrete inference client type.
|
||||
pub type GrpcInferenceClient = InferenceClient<InterceptedService<Channel, EdgeAuthInterceptor>>;
|
||||
|
||||
@@ -294,7 +294,7 @@ pub async fn build_channel(server: &str, tls: &TlsOptions) -> Result<Channel> {
|
||||
endpoint.connect().await.into_diagnostic()
|
||||
}
|
||||
|
||||
/// Build a gRPC [`NavigatorClient`].
|
||||
/// Build a gRPC [`OpenShellClient`].
|
||||
///
|
||||
/// When `tls.edge_token` is set, the returned client is wrapped with an
|
||||
/// interceptor that injects authentication headers on every request.
|
||||
@@ -302,7 +302,7 @@ pub async fn build_channel(server: &str, tls: &TlsOptions) -> Result<Channel> {
|
||||
pub async fn grpc_client(server: &str, tls: &TlsOptions) -> Result<GrpcClient> {
|
||||
let channel = build_channel(server, tls).await?;
|
||||
let interceptor = EdgeAuthInterceptor::maybe_from(tls)?;
|
||||
Ok(NavigatorClient::with_interceptor(channel, interceptor))
|
||||
Ok(OpenShellClient::with_interceptor(channel, interceptor))
|
||||
}
|
||||
|
||||
/// Interceptor that injects edge authentication headers into every outgoing
|
||||
+57
-57
@@ -5,10 +5,10 @@
|
||||
//! `--provider` names are auto-created when they match a known provider type,
|
||||
//! pass through when they already exist, and error for unrecognised names.
|
||||
|
||||
use navigator_cli::run;
|
||||
use navigator_cli::tls::TlsOptions;
|
||||
use navigator_core::proto::navigator_server::{Navigator, NavigatorServer};
|
||||
use navigator_core::proto::{
|
||||
use openshell_cli::run;
|
||||
use openshell_cli::tls::TlsOptions;
|
||||
use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer};
|
||||
use openshell_core::proto::{
|
||||
CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse,
|
||||
DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse,
|
||||
ExecSandboxEvent, ExecSandboxRequest, GetProviderRequest, GetSandboxPolicyRequest,
|
||||
@@ -84,7 +84,7 @@ impl Drop for EnvVarGuard {
|
||||
}
|
||||
}
|
||||
|
||||
// ── mock Navigator server ─────────────────────────────────────────────
|
||||
// ── mock OpenShell server ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct ProviderState {
|
||||
@@ -92,11 +92,11 @@ struct ProviderState {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct TestNavigator {
|
||||
struct TestOpenShell {
|
||||
state: ProviderState,
|
||||
}
|
||||
|
||||
impl TestNavigator {
|
||||
impl TestOpenShell {
|
||||
/// Seed the mock with an existing provider.
|
||||
async fn seed_provider(&self, name: &str, provider_type: &str) {
|
||||
let mut providers = self.state.providers.lock().await;
|
||||
@@ -114,7 +114,7 @@ impl TestNavigator {
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Navigator for TestNavigator {
|
||||
impl OpenShell for TestOpenShell {
|
||||
async fn health(
|
||||
&self,
|
||||
_request: tonic::Request<HealthRequest>,
|
||||
@@ -313,106 +313,106 @@ impl Navigator for TestNavigator {
|
||||
|
||||
async fn update_sandbox_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn list_sandbox_policies(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn report_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn push_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<tonic::Streaming<navigator_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<navigator_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<tonic::Streaming<openshell_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<openshell_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn submit_policy_analysis(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<navigator_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<openshell_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn reject_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_all_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn edit_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::EditDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::EditDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn undo_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn clear_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_history(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
}
|
||||
@@ -452,7 +452,7 @@ fn build_client_cert(ca: &Certificate, ca_key: &KeyPair) -> (String, String) {
|
||||
struct TestServer {
|
||||
endpoint: String,
|
||||
tls: TlsOptions,
|
||||
navigator: TestNavigator,
|
||||
openshell: TestOpenShell,
|
||||
_dir: TempDir,
|
||||
}
|
||||
|
||||
@@ -474,14 +474,14 @@ async fn run_server() -> TestServer {
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let incoming = TcpListenerStream::new(listener);
|
||||
|
||||
let navigator = TestNavigator::default();
|
||||
let svc_navigator = navigator.clone();
|
||||
let openshell = TestOpenShell::default();
|
||||
let svc_openshell = openshell.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.tls_config(tls_config)
|
||||
.unwrap()
|
||||
.add_service(NavigatorServer::new(svc_navigator))
|
||||
.add_service(OpenShellServer::new(svc_openshell))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -501,7 +501,7 @@ async fn run_server() -> TestServer {
|
||||
TestServer {
|
||||
endpoint,
|
||||
tls,
|
||||
navigator,
|
||||
openshell,
|
||||
_dir: dir,
|
||||
}
|
||||
}
|
||||
@@ -514,9 +514,9 @@ async fn run_server() -> TestServer {
|
||||
#[tokio::test]
|
||||
async fn explicit_provider_name_passes_through_when_it_exists() {
|
||||
let ts = run_server().await;
|
||||
ts.navigator.seed_provider("nvidia", "nvidia").await;
|
||||
ts.openshell.seed_provider("nvidia", "nvidia").await;
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client");
|
||||
|
||||
@@ -532,7 +532,7 @@ async fn explicit_provider_name_passes_through_when_it_exists() {
|
||||
assert_eq!(result, vec!["nvidia".to_string()]);
|
||||
|
||||
// Verify no extra providers were created.
|
||||
let providers = ts.navigator.state.providers.lock().await;
|
||||
let providers = ts.openshell.state.providers.lock().await;
|
||||
assert_eq!(providers.len(), 1, "no new providers should be created");
|
||||
}
|
||||
|
||||
@@ -544,7 +544,7 @@ async fn explicit_provider_name_auto_creates_when_valid_type() {
|
||||
let ts = run_server().await;
|
||||
let _guard = EnvVarGuard::set(&[("NVIDIA_API_KEY", "nvapi-test-key")]);
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client");
|
||||
|
||||
@@ -560,7 +560,7 @@ async fn explicit_provider_name_auto_creates_when_valid_type() {
|
||||
assert_eq!(result, vec!["nvidia".to_string()]);
|
||||
|
||||
// Verify the provider was created on the server with the right type.
|
||||
let providers = ts.navigator.state.providers.lock().await;
|
||||
let providers = ts.openshell.state.providers.lock().await;
|
||||
let provider = providers
|
||||
.get("nvidia")
|
||||
.expect("nvidia provider should exist");
|
||||
@@ -577,7 +577,7 @@ async fn explicit_provider_name_auto_creates_when_valid_type() {
|
||||
async fn explicit_provider_name_errors_for_unrecognised_name() {
|
||||
let ts = run_server().await;
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client");
|
||||
|
||||
@@ -608,7 +608,7 @@ async fn inferred_type_auto_creates_provider() {
|
||||
let ts = run_server().await;
|
||||
let _guard = EnvVarGuard::set(&[("ANTHROPIC_API_KEY", "sk-ant-test")]);
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client");
|
||||
|
||||
@@ -623,7 +623,7 @@ async fn inferred_type_auto_creates_provider() {
|
||||
|
||||
assert_eq!(result, vec!["claude".to_string()]);
|
||||
|
||||
let providers = ts.navigator.state.providers.lock().await;
|
||||
let providers = ts.openshell.state.providers.lock().await;
|
||||
let provider = providers
|
||||
.get("claude")
|
||||
.expect("claude provider should exist");
|
||||
@@ -637,7 +637,7 @@ async fn no_auto_providers_skips_missing_explicit_provider() {
|
||||
let ts = run_server().await;
|
||||
let _guard = EnvVarGuard::set(&[("NVIDIA_API_KEY", "nvapi-skip-test")]);
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client");
|
||||
|
||||
@@ -655,7 +655,7 @@ async fn no_auto_providers_skips_missing_explicit_provider() {
|
||||
"skipped providers should not appear in the result"
|
||||
);
|
||||
|
||||
let providers = ts.navigator.state.providers.lock().await;
|
||||
let providers = ts.openshell.state.providers.lock().await;
|
||||
assert!(
|
||||
providers.is_empty(),
|
||||
"no providers should be created when --no-auto-providers is set"
|
||||
@@ -672,7 +672,7 @@ async fn explicit_and_inferred_providers_combined() {
|
||||
("ANTHROPIC_API_KEY", "sk-ant-combo"),
|
||||
]);
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client");
|
||||
|
||||
@@ -689,7 +689,7 @@ async fn explicit_and_inferred_providers_combined() {
|
||||
assert!(result.contains(&"nvidia".to_string()));
|
||||
assert!(result.contains(&"claude".to_string()));
|
||||
|
||||
let providers = ts.navigator.state.providers.lock().await;
|
||||
let providers = ts.openshell.state.providers.lock().await;
|
||||
assert_eq!(providers.len(), 2);
|
||||
assert!(providers.contains_key("nvidia"));
|
||||
assert!(providers.contains_key("claude"));
|
||||
@@ -702,7 +702,7 @@ async fn explicit_and_inferred_deduplicates() {
|
||||
let ts = run_server().await;
|
||||
let _guard = EnvVarGuard::set(&[("NVIDIA_API_KEY", "nvapi-dedup")]);
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client");
|
||||
|
||||
@@ -722,7 +722,7 @@ async fn explicit_and_inferred_deduplicates() {
|
||||
"nvidia should appear exactly once"
|
||||
);
|
||||
|
||||
let providers = ts.navigator.state.providers.lock().await;
|
||||
let providers = ts.openshell.state.providers.lock().await;
|
||||
assert_eq!(
|
||||
providers.len(),
|
||||
1,
|
||||
+56
-56
@@ -1,14 +1,14 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use navigator_cli::tls::{TlsOptions, grpc_client};
|
||||
use navigator_core::proto::{
|
||||
use openshell_cli::tls::{TlsOptions, grpc_client};
|
||||
use openshell_core::proto::{
|
||||
CreateProviderRequest, CreateSshSessionRequest, CreateSshSessionResponse,
|
||||
DeleteProviderRequest, DeleteProviderResponse, ExecSandboxEvent, ExecSandboxRequest,
|
||||
GetProviderRequest, HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse,
|
||||
ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, ServiceStatus,
|
||||
UpdateProviderRequest,
|
||||
navigator_server::{Navigator, NavigatorServer},
|
||||
open_shell_server::{OpenShell, OpenShellServer},
|
||||
};
|
||||
use rcgen::{
|
||||
BasicConstraints, Certificate, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair,
|
||||
@@ -58,10 +58,10 @@ fn install_rustls_provider() {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct TestNavigator;
|
||||
struct TestOpenShell;
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Navigator for TestNavigator {
|
||||
impl OpenShell for TestOpenShell {
|
||||
async fn health(
|
||||
&self,
|
||||
_request: tonic::Request<HealthRequest>,
|
||||
@@ -74,56 +74,56 @@ impl Navigator for TestNavigator {
|
||||
|
||||
async fn create_sandbox(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::CreateSandboxRequest>,
|
||||
) -> Result<Response<navigator_core::proto::SandboxResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::CreateSandboxRequest>,
|
||||
) -> Result<Response<openshell_core::proto::SandboxResponse>, Status> {
|
||||
Ok(Response::new(
|
||||
navigator_core::proto::SandboxResponse::default(),
|
||||
openshell_core::proto::SandboxResponse::default(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_sandbox(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxRequest>,
|
||||
) -> Result<Response<navigator_core::proto::SandboxResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxRequest>,
|
||||
) -> Result<Response<openshell_core::proto::SandboxResponse>, Status> {
|
||||
Ok(Response::new(
|
||||
navigator_core::proto::SandboxResponse::default(),
|
||||
openshell_core::proto::SandboxResponse::default(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn list_sandboxes(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ListSandboxesRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ListSandboxesResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ListSandboxesRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ListSandboxesResponse>, Status> {
|
||||
Ok(Response::new(
|
||||
navigator_core::proto::ListSandboxesResponse::default(),
|
||||
openshell_core::proto::ListSandboxesResponse::default(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_sandbox(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::DeleteSandboxRequest>,
|
||||
) -> Result<Response<navigator_core::proto::DeleteSandboxResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::DeleteSandboxRequest>,
|
||||
) -> Result<Response<openshell_core::proto::DeleteSandboxResponse>, Status> {
|
||||
Ok(Response::new(
|
||||
navigator_core::proto::DeleteSandboxResponse { deleted: true },
|
||||
openshell_core::proto::DeleteSandboxResponse { deleted: true },
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_sandbox_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxPolicyResponse>, Status> {
|
||||
Ok(Response::new(
|
||||
navigator_core::proto::GetSandboxPolicyResponse::default(),
|
||||
openshell_core::proto::GetSandboxPolicyResponse::default(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_sandbox_provider_environment(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxProviderEnvironmentRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxProviderEnvironmentResponse>, Status>
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxProviderEnvironmentRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxProviderEnvironmentResponse>, Status>
|
||||
{
|
||||
Ok(Response::new(
|
||||
navigator_core::proto::GetSandboxProviderEnvironmentResponse::default(),
|
||||
openshell_core::proto::GetSandboxProviderEnvironmentResponse::default(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -187,14 +187,14 @@ impl Navigator for TestNavigator {
|
||||
}
|
||||
|
||||
type WatchSandboxStream = tokio_stream::wrappers::ReceiverStream<
|
||||
Result<navigator_core::proto::SandboxStreamEvent, Status>,
|
||||
Result<openshell_core::proto::SandboxStreamEvent, Status>,
|
||||
>;
|
||||
type ExecSandboxStream =
|
||||
tokio_stream::wrappers::ReceiverStream<Result<ExecSandboxEvent, Status>>;
|
||||
|
||||
async fn watch_sandbox(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::WatchSandboxRequest>,
|
||||
_request: tonic::Request<openshell_core::proto::WatchSandboxRequest>,
|
||||
) -> Result<Response<Self::WatchSandboxStream>, Status> {
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(
|
||||
@@ -214,106 +214,106 @@ impl Navigator for TestNavigator {
|
||||
|
||||
async fn update_sandbox_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn list_sandbox_policies(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn report_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn push_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<tonic::Streaming<navigator_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<navigator_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<tonic::Streaming<openshell_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<openshell_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn submit_policy_analysis(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<navigator_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<openshell_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn reject_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_all_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn edit_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::EditDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::EditDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn undo_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn clear_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_history(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
}
|
||||
@@ -364,7 +364,7 @@ async fn run_server(
|
||||
Server::builder()
|
||||
.tls_config(tls)
|
||||
.unwrap()
|
||||
.add_service(NavigatorServer::new(TestNavigator))
|
||||
.add_service(OpenShellServer::new(TestOpenShell))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.unwrap();
|
||||
+39
-39
@@ -1,10 +1,10 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use navigator_cli::run;
|
||||
use navigator_cli::tls::TlsOptions;
|
||||
use navigator_core::proto::navigator_server::{Navigator, NavigatorServer};
|
||||
use navigator_core::proto::{
|
||||
use openshell_cli::run;
|
||||
use openshell_cli::tls::TlsOptions;
|
||||
use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer};
|
||||
use openshell_core::proto::{
|
||||
CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse,
|
||||
DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse,
|
||||
ExecSandboxEvent, ExecSandboxRequest, GetProviderRequest, GetSandboxPolicyRequest,
|
||||
@@ -63,12 +63,12 @@ struct ProviderState {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct TestNavigator {
|
||||
struct TestOpenShell {
|
||||
state: ProviderState,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Navigator for TestNavigator {
|
||||
impl OpenShell for TestOpenShell {
|
||||
async fn health(
|
||||
&self,
|
||||
_request: tonic::Request<HealthRequest>,
|
||||
@@ -267,106 +267,106 @@ impl Navigator for TestNavigator {
|
||||
|
||||
async fn update_sandbox_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn list_sandbox_policies(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn report_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn push_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<tonic::Streaming<navigator_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<navigator_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<tonic::Streaming<openshell_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<openshell_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn submit_policy_analysis(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<navigator_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<openshell_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn reject_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_all_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn edit_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::EditDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::EditDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn undo_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn clear_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_history(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
}
|
||||
@@ -427,7 +427,7 @@ async fn run_server() -> TestServer {
|
||||
Server::builder()
|
||||
.tls_config(tls_config)
|
||||
.unwrap()
|
||||
.add_service(NavigatorServer::new(TestNavigator::default()))
|
||||
.add_service(OpenShellServer::new(TestOpenShell::default()))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -530,7 +530,7 @@ async fn provider_create_supports_generic_type_and_env_lookup_credentials() {
|
||||
.await
|
||||
.expect("provider create");
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client should connect");
|
||||
let response = client
|
||||
@@ -612,7 +612,7 @@ async fn provider_create_supports_nvidia_type_with_nvidia_api_key() {
|
||||
.await
|
||||
.expect("provider create");
|
||||
|
||||
let mut client = navigator_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
let mut client = openshell_cli::tls::grpc_client(&ts.endpoint, &ts.tls)
|
||||
.await
|
||||
.expect("grpc client should connect");
|
||||
let response = client
|
||||
+43
-43
@@ -1,11 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use navigator_bootstrap::load_last_sandbox;
|
||||
use navigator_cli::run;
|
||||
use navigator_cli::tls::TlsOptions;
|
||||
use navigator_core::proto::navigator_server::{Navigator, NavigatorServer};
|
||||
use navigator_core::proto::{
|
||||
use openshell_bootstrap::load_last_sandbox;
|
||||
use openshell_cli::run;
|
||||
use openshell_cli::tls::TlsOptions;
|
||||
use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer};
|
||||
use openshell_core::proto::{
|
||||
CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse,
|
||||
DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse,
|
||||
ExecSandboxEvent, ExecSandboxRequest, GetProviderRequest, GetSandboxPolicyRequest,
|
||||
@@ -82,12 +82,12 @@ struct SandboxState {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct TestNavigator {
|
||||
struct TestOpenShell {
|
||||
state: SandboxState,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Navigator for TestNavigator {
|
||||
impl OpenShell for TestOpenShell {
|
||||
async fn health(
|
||||
&self,
|
||||
_request: tonic::Request<HealthRequest>,
|
||||
@@ -293,106 +293,106 @@ impl Navigator for TestNavigator {
|
||||
|
||||
async fn update_sandbox_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn list_sandbox_policies(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn report_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn push_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<tonic::Streaming<navigator_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<navigator_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<tonic::Streaming<openshell_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<openshell_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn submit_policy_analysis(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<navigator_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<openshell_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn reject_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_all_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn edit_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::EditDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::EditDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn undo_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn clear_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_history(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,7 @@ fn build_client_cert(ca: &Certificate, ca_key: &KeyPair) -> (String, String) {
|
||||
struct TestServer {
|
||||
endpoint: String,
|
||||
tls: TlsOptions,
|
||||
navigator: TestNavigator,
|
||||
openshell: TestOpenShell,
|
||||
_dir: TempDir,
|
||||
}
|
||||
|
||||
@@ -450,14 +450,14 @@ async fn run_server() -> TestServer {
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let incoming = TcpListenerStream::new(listener);
|
||||
|
||||
let navigator = TestNavigator::default();
|
||||
let svc_navigator = navigator.clone();
|
||||
let openshell = TestOpenShell::default();
|
||||
let svc_openshell = openshell.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.tls_config(tls_config)
|
||||
.unwrap()
|
||||
.add_service(NavigatorServer::new(svc_navigator))
|
||||
.add_service(OpenShellServer::new(svc_openshell))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -477,7 +477,7 @@ async fn run_server() -> TestServer {
|
||||
TestServer {
|
||||
endpoint,
|
||||
tls,
|
||||
navigator,
|
||||
openshell,
|
||||
_dir: dir,
|
||||
}
|
||||
}
|
||||
@@ -509,7 +509,7 @@ fn test_env(fake_ssh_dir: &TempDir, xdg_dir: &TempDir) -> EnvVarGuard {
|
||||
}
|
||||
|
||||
async fn deleted_names(server: &TestServer) -> Vec<Vec<String>> {
|
||||
server.navigator.state.deleted_names.lock().await.clone()
|
||||
server.openshell.state.deleted_names.lock().await.clone()
|
||||
}
|
||||
|
||||
fn test_tls(server: &TestServer) -> TlsOptions {
|
||||
+49
-49
@@ -1,11 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use navigator_bootstrap::{load_last_sandbox, save_last_sandbox};
|
||||
use navigator_cli::run;
|
||||
use navigator_cli::tls::TlsOptions;
|
||||
use navigator_core::proto::navigator_server::{Navigator, NavigatorServer};
|
||||
use navigator_core::proto::{
|
||||
use openshell_bootstrap::{load_last_sandbox, save_last_sandbox};
|
||||
use openshell_cli::run;
|
||||
use openshell_cli::tls::TlsOptions;
|
||||
use openshell_core::proto::open_shell_server::{OpenShell, OpenShellServer};
|
||||
use openshell_core::proto::{
|
||||
CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse,
|
||||
DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse,
|
||||
ExecSandboxEvent, ExecSandboxRequest, GetProviderRequest, GetSandboxPolicyRequest,
|
||||
@@ -70,7 +70,7 @@ impl Drop for EnvVarGuard {
|
||||
}
|
||||
}
|
||||
|
||||
// ── mock Navigator server ─────────────────────────────────────────────
|
||||
// ── mock OpenShell server ─────────────────────────────────────────────
|
||||
|
||||
/// Records which sandbox name was requested via `get_sandbox`.
|
||||
#[derive(Clone, Default)]
|
||||
@@ -79,12 +79,12 @@ struct SandboxState {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct TestNavigator {
|
||||
struct TestOpenShell {
|
||||
state: SandboxState,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Navigator for TestNavigator {
|
||||
impl OpenShell for TestOpenShell {
|
||||
async fn health(
|
||||
&self,
|
||||
_request: tonic::Request<HealthRequest>,
|
||||
@@ -157,10 +157,10 @@ impl Navigator for TestNavigator {
|
||||
|
||||
async fn revoke_ssh_session(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::RevokeSshSessionRequest>,
|
||||
) -> Result<Response<navigator_core::proto::RevokeSshSessionResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::RevokeSshSessionRequest>,
|
||||
) -> Result<Response<openshell_core::proto::RevokeSshSessionResponse>, Status> {
|
||||
Ok(Response::new(
|
||||
navigator_core::proto::RevokeSshSessionResponse::default(),
|
||||
openshell_core::proto::RevokeSshSessionResponse::default(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -226,106 +226,106 @@ impl Navigator for TestNavigator {
|
||||
|
||||
async fn update_sandbox_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UpdateSandboxPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UpdateSandboxPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn list_sandbox_policies(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ListSandboxPoliciesRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ListSandboxPoliciesResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn report_policy_status(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ReportPolicyStatusRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ReportPolicyStatusResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetSandboxLogsRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn push_sandbox_logs(
|
||||
&self,
|
||||
_request: tonic::Request<tonic::Streaming<navigator_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<navigator_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
_request: tonic::Request<tonic::Streaming<openshell_core::proto::PushSandboxLogsRequest>>,
|
||||
) -> Result<Response<openshell_core::proto::PushSandboxLogsResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn submit_policy_analysis(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<navigator_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::SubmitPolicyAnalysisRequest>,
|
||||
) -> Result<Response<openshell_core::proto::SubmitPolicyAnalysisResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_policy(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftPolicyRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftPolicyResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn reject_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::RejectDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::RejectDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn approve_all_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ApproveAllDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ApproveAllDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn edit_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::EditDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::EditDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::EditDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn undo_draft_chunk(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<navigator_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::UndoDraftChunkRequest>,
|
||||
) -> Result<Response<openshell_core::proto::UndoDraftChunkResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn clear_draft_chunks(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<navigator_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::ClearDraftChunksRequest>,
|
||||
) -> Result<Response<openshell_core::proto::ClearDraftChunksResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
|
||||
async fn get_draft_history(
|
||||
&self,
|
||||
_request: tonic::Request<navigator_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<navigator_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
_request: tonic::Request<openshell_core::proto::GetDraftHistoryRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetDraftHistoryResponse>, Status> {
|
||||
Err(Status::unimplemented("not implemented in test"))
|
||||
}
|
||||
}
|
||||
@@ -363,7 +363,7 @@ fn build_client_cert(ca: &Certificate, ca_key: &KeyPair) -> (String, String) {
|
||||
struct TestServer {
|
||||
endpoint: String,
|
||||
tls: TlsOptions,
|
||||
navigator: TestNavigator,
|
||||
openshell: TestOpenShell,
|
||||
_dir: TempDir,
|
||||
}
|
||||
|
||||
@@ -385,14 +385,14 @@ async fn run_server() -> TestServer {
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let incoming = TcpListenerStream::new(listener);
|
||||
|
||||
let navigator = TestNavigator::default();
|
||||
let svc_navigator = navigator.clone();
|
||||
let openshell = TestOpenShell::default();
|
||||
let svc_openshell = openshell.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.tls_config(tls_config)
|
||||
.unwrap()
|
||||
.add_service(NavigatorServer::new(svc_navigator))
|
||||
.add_service(OpenShellServer::new(svc_openshell))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -412,7 +412,7 @@ async fn run_server() -> TestServer {
|
||||
TestServer {
|
||||
endpoint,
|
||||
tls,
|
||||
navigator,
|
||||
openshell,
|
||||
_dir: dir,
|
||||
}
|
||||
}
|
||||
@@ -429,7 +429,7 @@ async fn sandbox_get_sends_correct_name() {
|
||||
.await
|
||||
.expect("sandbox_get should succeed");
|
||||
|
||||
let recorded = ts.navigator.state.last_get_name.lock().await.clone();
|
||||
let recorded = ts.openshell.state.last_get_name.lock().await.clone();
|
||||
assert_eq!(
|
||||
recorded.as_deref(),
|
||||
Some("my-sandbox"),
|
||||
@@ -459,7 +459,7 @@ async fn sandbox_get_with_persisted_last_sandbox() {
|
||||
.await
|
||||
.expect("sandbox_get should succeed");
|
||||
|
||||
let recorded = ts.navigator.state.last_get_name.lock().await.clone();
|
||||
let recorded = ts.openshell.state.last_get_name.lock().await.clone();
|
||||
assert_eq!(
|
||||
recorded.as_deref(),
|
||||
Some("persisted-sb"),
|
||||
@@ -481,7 +481,7 @@ async fn explicit_name_takes_precedence_over_persisted() {
|
||||
.await
|
||||
.expect("sandbox_get should succeed");
|
||||
|
||||
let recorded = ts.navigator.state.last_get_name.lock().await.clone();
|
||||
let recorded = ts.openshell.state.last_get_name.lock().await.clone();
|
||||
assert_eq!(
|
||||
recorded.as_deref(),
|
||||
Some("explicit-sandbox"),
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "navigator-core"
|
||||
name = "openshell-core"
|
||||
description = "Shared library for OpenShell - proto definitions, config, and errors"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -13,7 +13,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
let proto_files = [
|
||||
"../../proto/navigator.proto",
|
||||
"../../proto/openshell.proto",
|
||||
"../../proto/datamodel.proto",
|
||||
"../../proto/sandbox.proto",
|
||||
"../../proto/inference.proto",
|
||||
@@ -14,7 +14,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub enum Error {
|
||||
/// Configuration error.
|
||||
#[error("configuration error: {message}")]
|
||||
#[diagnostic(code(navigator::config))]
|
||||
#[diagnostic(code(openshell::config))]
|
||||
Config {
|
||||
/// Error message.
|
||||
message: String,
|
||||
@@ -22,7 +22,7 @@ pub enum Error {
|
||||
|
||||
/// I/O error.
|
||||
#[error("I/O error: {source}")]
|
||||
#[diagnostic(code(navigator::io))]
|
||||
#[diagnostic(code(openshell::io))]
|
||||
Io {
|
||||
/// Underlying I/O error.
|
||||
#[from]
|
||||
@@ -31,7 +31,7 @@ pub enum Error {
|
||||
|
||||
/// TLS error.
|
||||
#[error("TLS error: {message}")]
|
||||
#[diagnostic(code(navigator::tls))]
|
||||
#[diagnostic(code(openshell::tls))]
|
||||
Tls {
|
||||
/// Error message.
|
||||
message: String,
|
||||
@@ -39,7 +39,7 @@ pub enum Error {
|
||||
|
||||
/// gRPC transport error.
|
||||
#[error("transport error: {message}")]
|
||||
#[diagnostic(code(navigator::transport))]
|
||||
#[diagnostic(code(openshell::transport))]
|
||||
Transport {
|
||||
/// Error message.
|
||||
message: String,
|
||||
@@ -47,7 +47,7 @@ pub enum Error {
|
||||
|
||||
/// Execution error.
|
||||
#[error("execution error: {message}")]
|
||||
#[diagnostic(code(navigator::execution))]
|
||||
#[diagnostic(code(openshell::execution))]
|
||||
Execution {
|
||||
/// Error message.
|
||||
message: String,
|
||||
@@ -55,7 +55,7 @@ pub enum Error {
|
||||
|
||||
/// Process error.
|
||||
#[error("process error: {message}")]
|
||||
#[diagnostic(code(navigator::process))]
|
||||
#[diagnostic(code(openshell::process))]
|
||||
Process {
|
||||
/// Error message.
|
||||
message: String,
|
||||
@@ -63,7 +63,7 @@ pub enum Error {
|
||||
|
||||
/// Timeout error.
|
||||
#[error("operation timed out")]
|
||||
#[diagnostic(code(navigator::timeout))]
|
||||
#[diagnostic(code(openshell::timeout))]
|
||||
Timeout,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//! Shared port-forward PID file management and SSH utility functions.
|
||||
//!
|
||||
//! Used by both the CLI (`navigator-cli`) and the TUI (`navigator-tui`) to
|
||||
//! Used by both the CLI (`openshell-cli`) and the TUI (`openshell-tui`) to
|
||||
//! start, stop, list, and track background SSH port forwards.
|
||||
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
@@ -9,8 +9,8 @@ use std::collections::HashSet;
|
||||
|
||||
/// How to inject an API key on outgoing inference requests.
|
||||
///
|
||||
/// Defined in `navigator-core` so both `navigator-router` (which applies it)
|
||||
/// and `navigator-server` / `navigator-sandbox` (which resolve it from
|
||||
/// Defined in `openshell-core` so both `openshell-router` (which applies it)
|
||||
/// and `openshell-server` / `openshell-sandbox` (which resolve it from
|
||||
/// provider metadata) can share the same type.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AuthHeader {
|
||||
@@ -30,7 +30,7 @@ pub enum AuthHeader {
|
||||
/// default endpoint, supported protocols, credential key lookup order, auth
|
||||
/// header style, and default headers.
|
||||
///
|
||||
/// This is separate from [`navigator_providers::ProviderPlugin`] which handles
|
||||
/// This is separate from [`openshell_providers::ProviderPlugin`] which handles
|
||||
/// credential *discovery* (scanning env vars). `InferenceProviderProfile` handles
|
||||
/// how to *use* discovered credentials to make inference API calls.
|
||||
pub struct InferenceProviderProfile {
|
||||
@@ -12,8 +12,8 @@
|
||||
unused_qualifications,
|
||||
rust_2018_idioms
|
||||
)]
|
||||
pub mod navigator {
|
||||
include!(concat!(env!("OUT_DIR"), "/navigator.v1.rs"));
|
||||
pub mod openshell {
|
||||
include!(concat!(env!("OUT_DIR"), "/openshell.v1.rs"));
|
||||
}
|
||||
|
||||
#[allow(
|
||||
@@ -25,7 +25,7 @@ pub mod navigator {
|
||||
)]
|
||||
pub mod datamodel {
|
||||
pub mod v1 {
|
||||
include!(concat!(env!("OUT_DIR"), "/navigator.datamodel.v1.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/openshell.datamodel.v1.rs"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub mod datamodel {
|
||||
)]
|
||||
pub mod sandbox {
|
||||
pub mod v1 {
|
||||
include!(concat!(env!("OUT_DIR"), "/navigator.sandbox.v1.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/openshell.sandbox.v1.rs"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub mod sandbox {
|
||||
rust_2018_idioms
|
||||
)]
|
||||
pub mod test {
|
||||
include!(concat!(env!("OUT_DIR"), "/navigator.test.v1.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/openshell.test.v1.rs"));
|
||||
}
|
||||
|
||||
#[allow(
|
||||
@@ -62,12 +62,12 @@ pub mod test {
|
||||
)]
|
||||
pub mod inference {
|
||||
pub mod v1 {
|
||||
include!(concat!(env!("OUT_DIR"), "/navigator.inference.v1.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/openshell.inference.v1.rs"));
|
||||
}
|
||||
}
|
||||
|
||||
pub use datamodel::v1::*;
|
||||
pub use inference::v1::*;
|
||||
pub use navigator::*;
|
||||
pub use openshell::*;
|
||||
pub use sandbox::v1::*;
|
||||
pub use test::ObjectForTest;
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// This file is @generated by prost-build.
|
||||
/// Sandbox model stored by Navigator.
|
||||
/// Sandbox model stored by OpenShell.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Sandbox {
|
||||
#[prost(string, tag = "1")]
|
||||
@@ -18,7 +18,7 @@ pub struct Sandbox {
|
||||
#[prost(enumeration = "SandboxPhase", tag = "6")]
|
||||
pub phase: i32,
|
||||
}
|
||||
/// Navigator-level sandbox spec.
|
||||
/// OpenShell-level sandbox spec.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SandboxSpec {
|
||||
#[prost(string, tag = "1")]
|
||||
@@ -88,7 +88,7 @@ pub struct SandboxCondition {
|
||||
#[prost(string, tag = "5")]
|
||||
pub last_transition_time: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Provider model stored by Navigator.
|
||||
/// Provider model stored by OpenShell.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Provider {
|
||||
#[prost(string, tag = "1")]
|
||||
+76
-76
@@ -136,7 +136,7 @@ pub struct WatchSandboxRequest {
|
||||
/// Stream sandbox status snapshots.
|
||||
#[prost(bool, tag = "2")]
|
||||
pub follow_status: bool,
|
||||
/// Stream navigator-server process logs correlated to this sandbox.
|
||||
/// Stream openshell-server process logs correlated to this sandbox.
|
||||
#[prost(bool, tag = "3")]
|
||||
pub follow_logs: bool,
|
||||
/// Stream platform events correlated to this sandbox.
|
||||
@@ -176,7 +176,7 @@ pub mod sandbox_stream_event {
|
||||
Warning(super::SandboxStreamWarning),
|
||||
}
|
||||
}
|
||||
/// Navigator server process log line correlated to a sandbox.
|
||||
/// OpenShell server process log line correlated to a sandbox.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SandboxLogLine {
|
||||
#[prost(string, tag = "1")]
|
||||
@@ -254,7 +254,7 @@ impl ServiceStatus {
|
||||
}
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod navigator_client {
|
||||
pub mod open_shell_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
@@ -264,12 +264,12 @@ pub mod navigator_client {
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
use tonic::codegen::http::Uri;
|
||||
/// Navigator service provides agent execution and management capabilities.
|
||||
/// OpenShell service provides agent execution and management capabilities.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NavigatorClient<T> {
|
||||
pub struct OpenShellClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl NavigatorClient<tonic::transport::Channel> {
|
||||
impl OpenShellClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
@@ -280,7 +280,7 @@ pub mod navigator_client {
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> NavigatorClient<T>
|
||||
impl<T> OpenShellClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||
T::Error: Into<StdError>,
|
||||
@@ -298,7 +298,7 @@ pub mod navigator_client {
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> NavigatorClient<InterceptedService<T, F>>
|
||||
) -> OpenShellClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
@@ -312,7 +312,7 @@ pub mod navigator_client {
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
NavigatorClient::new(InterceptedService::new(inner, interceptor))
|
||||
OpenShellClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
@@ -360,11 +360,11 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/Health",
|
||||
"/openshell.v1.OpenShell/Health",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "Health"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "Health"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Create a new sandbox.
|
||||
@@ -385,11 +385,11 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/CreateSandbox",
|
||||
"/openshell.v1.OpenShell/CreateSandbox",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "CreateSandbox"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "CreateSandbox"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Fetch a sandbox by id.
|
||||
@@ -410,11 +410,11 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/GetSandbox",
|
||||
"/openshell.v1.OpenShell/GetSandbox",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "GetSandbox"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "GetSandbox"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// List sandboxes.
|
||||
@@ -435,11 +435,11 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/ListSandboxes",
|
||||
"/openshell.v1.OpenShell/ListSandboxes",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "ListSandboxes"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "ListSandboxes"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Delete a sandbox by id.
|
||||
@@ -460,11 +460,11 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/DeleteSandbox",
|
||||
"/openshell.v1.OpenShell/DeleteSandbox",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "DeleteSandbox"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "DeleteSandbox"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Create a short-lived SSH session for a sandbox.
|
||||
@@ -485,11 +485,11 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/CreateSshSession",
|
||||
"/openshell.v1.OpenShell/CreateSshSession",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "CreateSshSession"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "CreateSshSession"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Revoke a previously issued SSH session.
|
||||
@@ -510,11 +510,11 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/RevokeSshSession",
|
||||
"/openshell.v1.OpenShell/RevokeSshSession",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "RevokeSshSession"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "RevokeSshSession"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Get sandbox policy by id (called by sandbox entrypoint at startup).
|
||||
@@ -537,18 +537,18 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/GetSandboxPolicy",
|
||||
"/openshell.v1.OpenShell/GetSandboxPolicy",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "GetSandboxPolicy"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "GetSandboxPolicy"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
/// Watch a sandbox and stream updates.
|
||||
///
|
||||
/// This stream can include:
|
||||
/// - Sandbox status snapshots (phase/status)
|
||||
/// - Navigator server process logs correlated by sandbox_id
|
||||
/// - OpenShell server process logs correlated by sandbox_id
|
||||
/// - Platform events correlated to the sandbox
|
||||
pub async fn watch_sandbox(
|
||||
&mut self,
|
||||
@@ -567,17 +567,17 @@ pub mod navigator_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/navigator.v1.Navigator/WatchSandbox",
|
||||
"/openshell.v1.OpenShell/WatchSandbox",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("navigator.v1.Navigator", "WatchSandbox"));
|
||||
.insert(GrpcMethod::new("openshell.v1.OpenShell", "WatchSandbox"));
|
||||
self.inner.server_streaming(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod navigator_server {
|
||||
pub mod open_shell_server {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
@@ -586,9 +586,9 @@ pub mod navigator_server {
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with NavigatorServer.
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with OpenShellServer.
|
||||
#[async_trait]
|
||||
pub trait Navigator: std::marker::Send + std::marker::Sync + 'static {
|
||||
pub trait OpenShell: std::marker::Send + std::marker::Sync + 'static {
|
||||
/// Check the health of the service.
|
||||
async fn health(
|
||||
&self,
|
||||
@@ -654,7 +654,7 @@ pub mod navigator_server {
|
||||
///
|
||||
/// This stream can include:
|
||||
/// - Sandbox status snapshots (phase/status)
|
||||
/// - Navigator server process logs correlated by sandbox_id
|
||||
/// - OpenShell server process logs correlated by sandbox_id
|
||||
/// - Platform events correlated to the sandbox
|
||||
async fn watch_sandbox(
|
||||
&self,
|
||||
@@ -664,16 +664,16 @@ pub mod navigator_server {
|
||||
tonic::Status,
|
||||
>;
|
||||
}
|
||||
/// Navigator service provides agent execution and management capabilities.
|
||||
/// OpenShell service provides agent execution and management capabilities.
|
||||
#[derive(Debug)]
|
||||
pub struct NavigatorServer<T> {
|
||||
pub struct OpenShellServer<T> {
|
||||
inner: Arc<T>,
|
||||
accept_compression_encodings: EnabledCompressionEncodings,
|
||||
send_compression_encodings: EnabledCompressionEncodings,
|
||||
max_decoding_message_size: Option<usize>,
|
||||
max_encoding_message_size: Option<usize>,
|
||||
}
|
||||
impl<T> NavigatorServer<T> {
|
||||
impl<T> OpenShellServer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self::from_arc(Arc::new(inner))
|
||||
}
|
||||
@@ -724,9 +724,9 @@ pub mod navigator_server {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for NavigatorServer<T>
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for OpenShellServer<T>
|
||||
where
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
@@ -741,10 +741,10 @@ pub mod navigator_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/navigator.v1.Navigator/Health" => {
|
||||
"/openshell.v1.OpenShell/Health" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct HealthSvc<T: Navigator>(pub Arc<T>);
|
||||
impl<T: Navigator> tonic::server::UnaryService<super::HealthRequest>
|
||||
struct HealthSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<T: OpenShell> tonic::server::UnaryService<super::HealthRequest>
|
||||
for HealthSvc<T> {
|
||||
type Response = super::HealthResponse;
|
||||
type Future = BoxFuture<
|
||||
@@ -757,7 +757,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::health(&inner, request).await
|
||||
<T as OpenShell>::health(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -784,11 +784,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/CreateSandbox" => {
|
||||
"/openshell.v1.OpenShell/CreateSandbox" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct CreateSandboxSvc<T: Navigator>(pub Arc<T>);
|
||||
struct CreateSandboxSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::UnaryService<super::CreateSandboxRequest>
|
||||
for CreateSandboxSvc<T> {
|
||||
type Response = super::SandboxResponse;
|
||||
@@ -802,7 +802,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::create_sandbox(&inner, request).await
|
||||
<T as OpenShell>::create_sandbox(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -829,11 +829,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/GetSandbox" => {
|
||||
"/openshell.v1.OpenShell/GetSandbox" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetSandboxSvc<T: Navigator>(pub Arc<T>);
|
||||
struct GetSandboxSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::UnaryService<super::GetSandboxRequest>
|
||||
for GetSandboxSvc<T> {
|
||||
type Response = super::SandboxResponse;
|
||||
@@ -847,7 +847,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::get_sandbox(&inner, request).await
|
||||
<T as OpenShell>::get_sandbox(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -874,11 +874,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/ListSandboxes" => {
|
||||
"/openshell.v1.OpenShell/ListSandboxes" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ListSandboxesSvc<T: Navigator>(pub Arc<T>);
|
||||
struct ListSandboxesSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::UnaryService<super::ListSandboxesRequest>
|
||||
for ListSandboxesSvc<T> {
|
||||
type Response = super::ListSandboxesResponse;
|
||||
@@ -892,7 +892,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::list_sandboxes(&inner, request).await
|
||||
<T as OpenShell>::list_sandboxes(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -919,11 +919,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/DeleteSandbox" => {
|
||||
"/openshell.v1.OpenShell/DeleteSandbox" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct DeleteSandboxSvc<T: Navigator>(pub Arc<T>);
|
||||
struct DeleteSandboxSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::UnaryService<super::DeleteSandboxRequest>
|
||||
for DeleteSandboxSvc<T> {
|
||||
type Response = super::DeleteSandboxResponse;
|
||||
@@ -937,7 +937,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::delete_sandbox(&inner, request).await
|
||||
<T as OpenShell>::delete_sandbox(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -964,11 +964,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/CreateSshSession" => {
|
||||
"/openshell.v1.OpenShell/CreateSshSession" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct CreateSshSessionSvc<T: Navigator>(pub Arc<T>);
|
||||
struct CreateSshSessionSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::UnaryService<super::CreateSshSessionRequest>
|
||||
for CreateSshSessionSvc<T> {
|
||||
type Response = super::CreateSshSessionResponse;
|
||||
@@ -982,7 +982,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::create_ssh_session(&inner, request).await
|
||||
<T as OpenShell>::create_ssh_session(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -1009,11 +1009,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/RevokeSshSession" => {
|
||||
"/openshell.v1.OpenShell/RevokeSshSession" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct RevokeSshSessionSvc<T: Navigator>(pub Arc<T>);
|
||||
struct RevokeSshSessionSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::UnaryService<super::RevokeSshSessionRequest>
|
||||
for RevokeSshSessionSvc<T> {
|
||||
type Response = super::RevokeSshSessionResponse;
|
||||
@@ -1027,7 +1027,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::revoke_ssh_session(&inner, request).await
|
||||
<T as OpenShell>::revoke_ssh_session(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -1054,11 +1054,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/GetSandboxPolicy" => {
|
||||
"/openshell.v1.OpenShell/GetSandboxPolicy" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetSandboxPolicySvc<T: Navigator>(pub Arc<T>);
|
||||
struct GetSandboxPolicySvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::UnaryService<
|
||||
super::super::sandbox::v1::GetSandboxPolicyRequest,
|
||||
> for GetSandboxPolicySvc<T> {
|
||||
@@ -1075,7 +1075,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::get_sandbox_policy(&inner, request).await
|
||||
<T as OpenShell>::get_sandbox_policy(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -1102,11 +1102,11 @@ pub mod navigator_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/navigator.v1.Navigator/WatchSandbox" => {
|
||||
"/openshell.v1.OpenShell/WatchSandbox" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct WatchSandboxSvc<T: Navigator>(pub Arc<T>);
|
||||
struct WatchSandboxSvc<T: OpenShell>(pub Arc<T>);
|
||||
impl<
|
||||
T: Navigator,
|
||||
T: OpenShell,
|
||||
> tonic::server::ServerStreamingService<super::WatchSandboxRequest>
|
||||
for WatchSandboxSvc<T> {
|
||||
type Response = super::SandboxStreamEvent;
|
||||
@@ -1121,7 +1121,7 @@ pub mod navigator_server {
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Navigator>::watch_sandbox(&inner, request).await
|
||||
<T as OpenShell>::watch_sandbox(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
@@ -1168,7 +1168,7 @@ pub mod navigator_server {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for NavigatorServer<T> {
|
||||
impl<T> Clone for OpenShellServer<T> {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = self.inner.clone();
|
||||
Self {
|
||||
@@ -1181,8 +1181,8 @@ pub mod navigator_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "navigator.v1.Navigator";
|
||||
impl<T> tonic::server::NamedService for NavigatorServer<T> {
|
||||
pub const SERVICE_NAME: &str = "openshell.v1.OpenShell";
|
||||
impl<T> tonic::server::NamedService for OpenShellServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "navigator-policy"
|
||||
name = "openshell-policy"
|
||||
description = "Shared sandbox policy parsing and defaults for OpenShell"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -11,7 +11,7 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
navigator-core = { path = "../navigator-core" }
|
||||
openshell-core = { path = "../openshell-core" }
|
||||
serde = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
miette = { workspace = true }
|
||||
@@ -14,7 +14,7 @@ use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use navigator_core::proto::{
|
||||
use openshell_core::proto::{
|
||||
FilesystemPolicy, L7Allow, L7Rule, LandlockPolicy, NetworkBinary, NetworkEndpoint,
|
||||
NetworkPolicyRule, ProcessPolicy, SandboxPolicy,
|
||||
};
|
||||
@@ -328,7 +328,14 @@ pub fn load_sandbox_policy(cli_path: Option<&str>) -> Result<Option<SandboxPolic
|
||||
///
|
||||
/// When the gateway provides no policy at sandbox creation time, the sandbox
|
||||
/// supervisor probes this path before falling back to the restrictive default.
|
||||
pub const CONTAINER_POLICY_PATH: &str = "/etc/navigator/policy.yaml";
|
||||
pub const CONTAINER_POLICY_PATH: &str = "/etc/openshell/policy.yaml";
|
||||
|
||||
/// Legacy path used before the navigator → openshell rename.
|
||||
///
|
||||
/// Existing community sandbox images still ship their policy at this path.
|
||||
/// The sandbox supervisor tries [`CONTAINER_POLICY_PATH`] first, then falls
|
||||
/// back to this legacy path for backward compatibility.
|
||||
pub const LEGACY_CONTAINER_POLICY_PATH: &str = "/etc/navigator/policy.yaml";
|
||||
|
||||
/// Return a restrictive default policy suitable for sandboxes that have no
|
||||
/// explicit policy configured.
|
||||
@@ -756,7 +763,12 @@ network_policies:
|
||||
|
||||
#[test]
|
||||
fn container_policy_path_is_expected() {
|
||||
assert_eq!(CONTAINER_POLICY_PATH, "/etc/navigator/policy.yaml");
|
||||
assert_eq!(CONTAINER_POLICY_PATH, "/etc/openshell/policy.yaml");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_container_policy_path_is_expected() {
|
||||
assert_eq!(LEGACY_CONTAINER_POLICY_PATH, "/etc/navigator/policy.yaml");
|
||||
}
|
||||
|
||||
// ---- Policy validation tests ----
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "navigator-providers"
|
||||
name = "openshell-providers"
|
||||
description = "Provider discovery and registry for OpenShell"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -11,7 +11,7 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
navigator-core = { path = "../navigator-core" }
|
||||
openshell-core = { path = "../openshell-core" }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[lints]
|
||||
@@ -12,7 +12,7 @@ mod test_helpers;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub use navigator_core::proto::Provider;
|
||||
pub use openshell_core::proto::Provider;
|
||||
|
||||
pub use context::{DiscoveryContext, RealDiscoveryContext};
|
||||
pub use discovery::discover_with_spec;
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "navigator-router"
|
||||
name = "openshell-router"
|
||||
description = "Inference routing library for OpenShell"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -11,7 +11,7 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
navigator-core = { path = "../navigator-core" }
|
||||
openshell-core = { path = "../openshell-core" }
|
||||
bytes = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@@ -1,6 +1,6 @@
|
||||
# navigator-router
|
||||
# openshell-router
|
||||
|
||||
`navigator-router` is the inference routing and upstream execution engine used by `navigator-server`.
|
||||
`openshell-router` is the inference routing and upstream execution engine used by `openshell-server`.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
- Persistence of routes/entities.
|
||||
- Loading sandbox or policy objects.
|
||||
|
||||
These are owned by `navigator-server`.
|
||||
These are owned by `openshell-server`.
|
||||
|
||||
## Integration contract with navigator-server
|
||||
## Integration contract with openshell-server
|
||||
|
||||
Current split:
|
||||
|
||||
- `navigator-server`:
|
||||
- `openshell-server`:
|
||||
- authenticates request origin
|
||||
- resolves cluster-managed inference route candidates from providers
|
||||
- loads enabled route candidates from the entity store
|
||||
- `navigator-router`:
|
||||
- `openshell-router`:
|
||||
- picks a route from candidates (`proxy_with_candidates`)
|
||||
- forwards the HTTP request upstream and returns the raw response
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
pub use navigator_core::inference::AuthHeader;
|
||||
pub use openshell_core::inference::AuthHeader;
|
||||
|
||||
use crate::RouterError;
|
||||
|
||||
@@ -111,7 +111,7 @@ impl RouteConfig {
|
||||
}
|
||||
|
||||
fn resolve(&self) -> Result<ResolvedRoute, RouterError> {
|
||||
let protocols = navigator_core::inference::normalize_protocols(&self.protocols);
|
||||
let protocols = openshell_core::inference::normalize_protocols(&self.protocols);
|
||||
if protocols.is_empty() {
|
||||
return Err(RouterError::Internal(format!(
|
||||
"route '{}' has no protocols",
|
||||
@@ -135,10 +135,10 @@ impl RouteConfig {
|
||||
|
||||
/// Derive auth header style and default headers from a provider type string.
|
||||
///
|
||||
/// Delegates to [`navigator_core::inference::auth_for_provider_type`] which
|
||||
/// Delegates to [`openshell_core::inference::auth_for_provider_type`] which
|
||||
/// uses the centralized `InferenceProviderProfile` registry.
|
||||
fn auth_from_provider_type(provider_type: Option<&str>) -> (AuthHeader, Vec<(String, String)>) {
|
||||
navigator_core::inference::auth_for_provider_type(provider_type.unwrap_or(""))
|
||||
openshell_core::inference::auth_for_provider_type(provider_type.unwrap_or(""))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -41,7 +41,7 @@ pub fn mock_response(route: &ResolvedRoute, source_protocol: &str) -> ProxyRespo
|
||||
status: 200,
|
||||
headers: vec![
|
||||
("content-type".to_string(), "application/json".to_string()),
|
||||
("x-navigator-mock".to_string(), "true".to_string()),
|
||||
("x-openshell-mock".to_string(), "true".to_string()),
|
||||
],
|
||||
body: body_bytes,
|
||||
}
|
||||
@@ -210,7 +210,7 @@ mod tests {
|
||||
assert!(
|
||||
resp.headers
|
||||
.iter()
|
||||
.any(|(k, v)| k == "x-navigator-mock" && v == "true")
|
||||
.any(|(k, v)| k == "x-openshell-mock" && v == "true")
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use navigator_router::Router;
|
||||
use navigator_router::config::{AuthHeader, ResolvedRoute, RouteConfig, RouterConfig};
|
||||
use openshell_router::Router;
|
||||
use openshell_router::config::{AuthHeader, ResolvedRoute, RouteConfig, RouterConfig};
|
||||
use wiremock::matchers::{bearer_token, body_partial_json, header, method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
@@ -132,7 +132,7 @@ async fn proxy_no_compatible_route_returns_error() {
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
matches!(err, navigator_router::RouterError::NoCompatibleRoute(_)),
|
||||
matches!(err, openshell_router::RouterError::NoCompatibleRoute(_)),
|
||||
"expected NoCompatibleRoute, got: {err:?}"
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "navigator-sandbox"
|
||||
name = "openshell-sandbox"
|
||||
description = "OpenShell process sandbox and monitor"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -11,13 +11,13 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "navigator-sandbox"
|
||||
name = "openshell-sandbox"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
navigator-core = { path = "../navigator-core" }
|
||||
navigator-policy = { path = "../navigator-policy" }
|
||||
navigator-router = { path = "../navigator-router" }
|
||||
openshell-core = { path = "../openshell-core" }
|
||||
openshell-policy = { path = "../openshell-policy" }
|
||||
openshell-router = { path = "../openshell-router" }
|
||||
|
||||
# Async runtime
|
||||
tokio = { workspace = true }
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package navigator.sandbox
|
||||
package openshell.sandbox
|
||||
|
||||
default allow_network = false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user