|
1 | 1 | # Arch Doc Writer Memory |
2 | 2 |
|
3 | 3 | ## Project Structure |
4 | | -- Crates: `navigator-cli`, `navigator-server`, `navigator-sandbox`, `navigator-bootstrap`, `navigator-core`, `navigator-providers`, `navigator-router` |
| 4 | +- Crates: `navigator-cli`, `navigator-server`, `navigator-sandbox`, `navigator-bootstrap`, `navigator-core`, `navigator-providers`, `navigator-router`, `navigator-policy` |
5 | 5 | - CLI entry: `crates/navigator-cli/src/main.rs` (clap parser + dispatch) |
6 | 6 | - CLI logic: `crates/navigator-cli/src/run.rs` (all command implementations) |
7 | 7 | - Sandbox entry: `crates/navigator-sandbox/src/lib.rs` (`run_sandbox()`) |
8 | 8 | - OPA engine: `crates/navigator-sandbox/src/opa.rs` (single file, not a directory) |
9 | 9 | - Identity cache: `crates/navigator-sandbox/src/identity.rs` (SHA256 TOFU, uses Mutex<HashMap> NOT DashMap) |
10 | 10 | - L7 inspection: `crates/navigator-sandbox/src/l7/` (mod.rs, tls.rs, relay.rs, rest.rs, provider.rs, inference.rs) |
11 | 11 | - Proxy: `crates/navigator-sandbox/src/proxy.rs` |
| 12 | +- Policy crate: `crates/navigator-policy/src/lib.rs` (YAML<->proto conversion, validation, restrictive default) |
12 | 13 | - Server multiplex: `crates/navigator-server/src/multiplex.rs` |
13 | 14 | - SSH tunnel: `crates/navigator-server/src/ssh_tunnel.rs` |
14 | 15 | - Sandbox SSH server: `crates/navigator-sandbox/src/ssh.rs` |
15 | 16 | - Providers: `crates/navigator-providers/src/providers/` (per-provider modules) |
16 | 17 | - Bootstrap: `crates/navigator-bootstrap/src/lib.rs` (cluster lifecycle) |
17 | | -- Proto files: `proto/` directory (navigator.proto, sandbox.proto, datamodel.proto) |
| 18 | +- Proto files: `proto/` directory (navigator.proto, sandbox.proto, datamodel.proto, inference.proto) |
18 | 19 |
|
19 | 20 | ## Architecture Docs |
20 | 21 | - Files renamed from numbered prefix format to descriptive names (e.g., `2 - server-architecture.md` -> `gateway-architecture.md`) |
|
24 | 25 |
|
25 | 26 | ## Key Patterns |
26 | 27 | - OPA baked-in rules: `include_str!("../data/sandbox-policy.rego")` in opa.rs |
27 | | -- Policy loading: gRPC mode (NAVIGATOR_SANDBOX_ID + NAVIGATOR_ENDPOINT) or file mode (--policy-rules + --policy-data) |
| 28 | +- Policy loading: gRPC mode (NEMOCLAW_SANDBOX_ID + NEMOCLAW_ENDPOINT) or file mode (--policy-rules + --policy-data) |
| 29 | +- Env vars: sandbox uses NEMOCLAW_* prefix (e.g., NEMOCLAW_SANDBOX_ID, NEMOCLAW_ENDPOINT, NEMOCLAW_POLICY_RULES) |
| 30 | +- CLI flag: `--navigator-endpoint` (NOT `--nemoclaw-endpoint`) |
28 | 31 | - Provider env injection: both entrypoint process (tokio Command) and SSH shell (std Command) |
29 | 32 | - Cluster bootstrap: `sandbox_create_with_bootstrap()` auto-deploys when no cluster exists (main.rs ~line 632) |
30 | 33 | - CLI cluster resolution: --cluster flag > NAVIGATOR_CLUSTER env > active cluster file |
|
41 | 44 |
|
42 | 45 | ## Server Crate Details |
43 | 46 | - Two gRPC services: Navigator (grpc.rs) and Inference (inference.rs), multiplexed via GrpcRouter by URI path |
44 | | -- Gateway is control-plane only for inference: route CRUD + GetSandboxInferenceBundle |
45 | | -- GetSandboxInferenceBundle: returns SandboxResolvedRoute list + revision hash + generated_at_ms for a sandbox_id |
| 47 | +- Gateway is control-plane only for inference: SetClusterInference + GetClusterInference + GetInferenceBundle |
| 48 | +- GetInferenceBundle: resolves managed route from provider record at request time, returns ResolvedRoute list + revision hash + generated_at_ms |
| 49 | +- SetClusterInference: takes provider_name + model_id, stores only references (endpoint/key/protocols resolved at bundle time) |
46 | 50 | - Persistence: single `objects` table, protobuf payloads, Store enum dispatches SQLite vs Postgres by URL prefix |
47 | 51 | - Persistence CRUD: upsert ON CONFLICT (id) not (object_type, id); list ORDER BY created_at_ms ASC, name ASC (not id!) |
48 | 52 | - --db-url has no code default; Helm values.yaml sets `sqlite:/var/navigator/navigator.db` |
|
83 | 87 | - Poll loop: `run_policy_poll_loop()` in lib.rs, spawned after child process, gRPC mode only |
84 | 88 | - `OpaEngine::reload_from_proto()`: reuses `from_proto()` pipeline, atomically swaps inner engine, LKG on failure |
85 | 89 | - `CachedNavigatorClient` in grpc_client.rs: persistent mTLS channel for poll + status report (mirrors CachedInferenceClient) |
86 | | -- Dynamic domains: network_policies, inference (OPA engine swap). Static domains: filesystem, landlock, process (pre_exec, immutable) |
| 90 | +- Dynamic domains: network_policies only (inference removed from policy). Static domains: filesystem, landlock, process (pre_exec, immutable) |
87 | 91 | - Server-side: `UpdateSandboxPolicy` RPC rejects changes to static fields or network mode changes |
88 | 92 | - Server-side validation: `validate_static_fields_unchanged()` + `validate_network_mode_unchanged()` in grpc.rs |
89 | | -- Poll interval: `NAVIGATOR_POLICY_POLL_INTERVAL_SECS` env var (default 30), no CLI flag |
| 93 | +- Poll interval: `NEMOCLAW_POLICY_POLL_INTERVAL_SECS` env var (default 30), no CLI flag |
90 | 94 | - Version tracking: monotonic i64 per sandbox, `GetSandboxPolicyResponse` has version + policy_hash |
91 | 95 | - Version 1 backfill: lazy on first `GetSandboxPolicy` from spec.policy if no policy_revisions row exists |
92 | 96 | - `supersede_pending_policies()`: marks older pending revisions as superseded when new version persisted |
|
99 | 103 | - CLI: `sandbox_policy_set()` in run.rs (~line 2901): loads YAML, calls UpdateSandboxPolicy, optionally polls for status |
100 | 104 | - CLI: `sandbox_policy_get()` in run.rs (~line 3015): supports --rev N (version=0 means latest) and --full (YAML output via policy_to_yaml) |
101 | 105 | - CLI: `sandbox_logs()` in run.rs (~line 3124): --source (all/gateway/sandbox) and --level (error/warn/info/debug/trace) filters |
102 | | -- Deterministic hashing: `deterministic_policy_hash()` in grpc.rs (~line 1133): sorts network_policies by key, hashes fields individually |
| 106 | +- Deterministic hashing: `deterministic_policy_hash()` in grpc.rs (~line 1222): sorts network_policies by key, hashes fields individually, NO inference field |
103 | 107 | - Idempotent UpdateSandboxPolicy: compares hash of new policy to latest stored hash, returns existing version if match |
104 | | -- `policy_to_yaml()` in run.rs (~line 1623): converts proto to YAML via `PolicyYaml` struct (uses BTreeMap for ordered keys) |
105 | | -- `policy_record_to_revision()` in grpc.rs (~line 1234): `include_policy` param controls whether full proto is included |
| 108 | +- `policy_to_yaml()` in run.rs: converts proto to YAML via navigator_policy::serialize_sandbox_policy (moved to navigator-policy crate) |
| 109 | +- `policy_record_to_revision()` in grpc.rs (~line 1334): `include_policy` param controls whether full proto is included |
106 | 110 | - Server-side log filtering: `source_matches()` + `level_matches()` in grpc.rs, applied in both get_sandbox_logs and watch_sandbox |
107 | | -- Standalone `proxy_inference()` was removed; proxy uses `CachedInferenceClient` via `InferenceContext.grpc_client` (OnceCell) |
| 111 | +- Standalone `proxy_inference()` was removed; inference handled in-sandbox by navigator-router |
| 112 | +- Provider types: claude, codex, opencode, generic, openai, anthropic, nvidia, gitlab, github, outlook |
108 | 113 |
|
109 | 114 | ## Policy System Details |
110 | | -- YAML data file top-level keys: filesystem_policy, landlock, process, network_policies, inference |
| 115 | +- YAML data file top-level keys: filesystem_policy, landlock, process, network_policies (NO inference key -- removed) |
| 116 | +- Proto SandboxPolicy fields: version, filesystem, landlock, process, network_policies (NO inference field) |
111 | 117 | - Proto message field `filesystem` maps to YAML key `filesystem_policy` (different names!) |
112 | | -- Behavioral trigger: network_policies non-empty -> proxy mode, empty -> block mode (seccomp blocks AF_INET/AF_INET6) |
| 118 | +- IMPORTANT: Sandbox always runs in Proxy mode. NetworkMode::Block exists as enum variant but is NEVER set. |
| 119 | +- Both file mode and gRPC mode set NetworkMode::Proxy unconditionally (see load_policy() in lib.rs and TryFrom in policy.rs) |
| 120 | +- Reason: proxy always needed so inference.local is addressable + all egress evaluated by OPA |
| 121 | +- OPA two-action model: Allow, Deny (NetworkAction in opa.rs). InspectForInference was REMOVED. |
| 122 | +- Rego network_action rule: "allow" or "deny" only (no "inspect_for_inference") |
113 | 123 | - Behavioral trigger: endpoint `protocol` field -> L7 inspection; absent -> L4 raw copy_bidirectional |
114 | 124 | - Behavioral trigger: `tls: terminate` -> MITM TLS with ephemeral CA; requires `protocol` to also be set |
115 | 125 | - Behavioral trigger: `enforcement: enforce` -> deny at proxy; `audit` (default) -> log + forward |
116 | 126 | - Access presets: read-only (GET/HEAD/OPTIONS), read-write (+POST/PUT/PATCH), full (*/*) |
117 | 127 | - Validation: rules+access mutual exclusion, protocol requires rules/access, sql+enforce blocked, empty rules rejected |
| 128 | +- YAML policy parsing moved to navigator-policy crate (parse_sandbox_policy, serialize_sandbox_policy) |
| 129 | +- PolicyFile uses deny_unknown_fields for strict YAML parsing |
| 130 | +- restrictive_default_policy() in navigator-policy: no network policies, sandbox user, best_effort landlock |
| 131 | +- CONTAINER_POLICY_PATH: /etc/navigator/policy.yaml (well-known path for container-shipped policy) |
| 132 | +- clear_process_identity(): clears run_as_user/run_as_group for custom images |
| 133 | +- Policy safety validation: validate_sandbox_policy() checks root identity, path traversal, relative paths, overly broad paths, max 256 paths, max 4096 chars |
118 | 134 | - Identity binding: /proc/net/tcp -> inode -> PID -> /proc/PID/exe + ancestors + cmdline, SHA256 TOFU cache |
119 | 135 | - Network namespace: 10.200.0.1 (host/proxy) <-> 10.200.0.2 (sandbox), port 3128 default |
120 | 136 | - Enforcement order in pre_exec: setns -> drop_privileges -> landlock -> seccomp |
|
132 | 148 |
|
133 | 149 | ## Inference Routing Details |
134 | 150 | - Sandbox-local execution via navigator-router crate |
135 | | -- OPA three-action model: Allow, InspectForInference, Deny (`NetworkAction` in opa.rs) |
136 | | -- InferenceContext: Router + patterns + `Arc<RwLock<Vec<ResolvedRoute>>>` route cache |
| 151 | +- InferenceContext in proxy.rs: Router + patterns + `Arc<RwLock<Vec<ResolvedRoute>>>` route cache |
137 | 152 | - Route sources: `--inference-routes` YAML file (standalone) > cluster bundle via gRPC; empty routes gracefully disable |
138 | 153 | - Cluster bundle refreshed every ROUTE_REFRESH_INTERVAL_SECS (30s) |
139 | | -- Patterns: POST /v1/chat/completions, /v1/completions, /v1/responses, /v1/messages |
| 154 | +- Patterns: POST /v1/chat/completions, /v1/completions, /v1/responses, /v1/messages; GET /v1/models, /v1/models/* |
| 155 | +- inference.local CONNECT intercepted BEFORE OPA evaluation in proxy |
| 156 | +- InferenceProviderProfile in navigator-core/src/inference.rs: centralized provider metadata |
| 157 | +- proxy.rs: ONLY CONNECT to inference.local is handled; non-CONNECT requests get 403 for ALL hosts |
| 158 | +- Buffer: INITIAL_INFERENCE_BUF=64KiB, MAX_INFERENCE_BUF=10MiB; grows by doubling |
140 | 159 | - Dev sandbox: `mise run sandbox -e VAR_NAME` forwards host env vars; NVIDIA_API_KEY always passed |
141 | 160 |
|
142 | 161 | ## Log Streaming Details |
|
162 | 181 | ## Naming Conventions |
163 | 182 | - The project name "Navigator" appears in code but docs should use generic terms per user preference |
164 | 183 | - CLI binary: `navigator` (aliased as `nav` in dev via mise) |
165 | | -- Provider types: claude, codex, opencode, openclaw, generic, nvidia, gitlab, github, outlook |
| 184 | +- Provider types: claude, codex, opencode, generic, openai, anthropic, nvidia, gitlab, github, outlook (see ProviderRegistry::new()) |
0 commit comments