-
Notifications
You must be signed in to change notification settings - Fork 287
Description
Summary
The current provider implementation tightly couples provider credentials to environment variable names, offers no mechanism for providers to write config files into sandboxes, and stores credentials as plaintext in the database. This issue proposes three improvements to the provider system.
Context
Current State
- Data model: A
Providerhascredentials: map<string, string>andconfig: map<string, string>(protodatamodel.proto:77-87). Credential keys are environment variable names (e.g.,ANTHROPIC_API_KEY). There is no abstraction layer between what a provider "knows" and how it's projected into a sandbox. - Injection: The sandbox supervisor fetches credentials via
GetSandboxProviderEnvironmentgRPC, which returns a flatHashMap<String, String>. These are injected as env vars viaCommand::env()(process.rs:107,ssh.rs:682). There is no mechanism to write config files to disk. - Persistence: Credentials are stored as raw protobuf bytes in the
objectstable (payload BLOB/BYTEA). No encryption at rest. No encryption infrastructure exists in the codebase. - Hooks:
ProviderPlugin::apply_to_sandbox()exists as a no-op stub (lib.rs:60-66). It is never called or overridden by any provider plugin.
Key Files
| Area | File | Description |
|---|---|---|
| Plugin trait | crates/navigator-providers/src/lib.rs:45-67 |
ProviderPlugin trait definition |
| Registry | crates/navigator-providers/src/lib.rs:69-121 |
ProviderRegistry with all provider registrations |
| Discovery | crates/navigator-providers/src/discovery.rs |
discover_with_spec() env/file scanning |
| Credential resolution | crates/navigator-server/src/grpc.rs:1456-1498 |
resolve_provider_environment() |
| Sandbox startup | crates/navigator-sandbox/src/lib.rs:117-502 |
run_sandbox() full sequence |
| Process spawn | crates/navigator-sandbox/src/process.rs:88-190 |
spawn_impl() with env injection |
| Persistence | crates/navigator-server/src/persistence/mod.rs:259-285 |
put_message()/get_message() — raw protobuf, no encryption |
| PKI bootstrap | crates/navigator-bootstrap/src/pki.rs |
Cluster PKI generation |
| Architecture doc | architecture/sandbox-providers.md |
Current design documentation |
Proposed Changes
1. Provider Properties Abstraction
Decouple provider properties from environment variable names. Instead of credential keys being env var names directly, providers should have typed properties with explicit mappings.
Current flow:
Host env: ANTHROPIC_API_KEY=sk-abc
→ discover: credentials["ANTHROPIC_API_KEY"] = "sk-abc"
→ persist: credentials map with key "ANTHROPIC_API_KEY"
→ inject: cmd.env("ANTHROPIC_API_KEY", "sk-abc")
Proposed flow:
Host env: ANTHROPIC_API_KEY=sk-abc
→ discover FROM env var: property "api_key" = "sk-abc" (via env_var_mappings)
→ persist: properties map with key "api_key"
→ apply TO sandbox env vars: "api_key" → ANTHROPIC_API_KEY (via env_var_mappings)
→ apply TO config files: "api_key" used by hooks to write config
Each provider plugin should declare:
- Properties it supports (e.g.,
api_key,token,endpoint,org_id) - Env var mappings: which env vars to discover FROM and which to project TO
- Config file templates: what files to write in the sandbox (see hooks below)
This enables a single property value to be projected as both an env var AND a config file entry, and allows discovery from one env var name while projecting to a different one.
2. Sandbox Pre-Spawn Hooks
The sandbox supervisor should execute provider-specific hooks before spawning the child process, following the existing write_ca_files() pattern (lib.rs:177-211).
Design considerations:
- Hooks run in the sandbox supervisor process (root), before privilege drop and sandboxing
- Hooks can write arbitrary files to disk (e.g.,
.gitconfig,claude.json,config.yml) - Hooks must update the
SandboxPolicy.filesystem.read_onlypaths so the sandboxed child can access written files - File paths should be passed to the child via env vars where appropriate
Architectural decision — where hook logic lives:
Currently navigator-sandbox does not depend on navigator-providers. The server resolves providers into a flat env var map and the sandbox only sees key-value pairs with no type information.
To enable provider-specific hooks, the system needs to communicate more than just env vars. Options include:
- Extend the gRPC response to include structured hook data (file templates, env var mappings) resolved server-side — keeps the sandbox "dumb"
- Add
navigator-providersas a sandbox dependency so the sandbox can interpret provider types and run plugin hooks locally - Hybrid: server sends properties + type metadata, sandbox has a lightweight hook executor
Use cases:
- Claude: write
~/.claude.jsonwith API key config - GitHub: write
~/.gitconfigwith credential helper,~/.config/gh/hosts.yml - GitLab: write
~/.config/glab-cli/config.yml - Generic: write arbitrary config files from provider config map
3. Credential Encryption at Rest
Provider credentials should be encrypted before being stored in the database.
Approach: Dedicated symmetric encryption key
- Generate a symmetric encryption key (e.g., AES-256-GCM) at cluster bootstrap time
- Store it as a new K8s secret (e.g.,
navigator-encryption-key) - Mount it to the
navigator-serverpod alongside existing TLS secrets - Encrypt credential values (or the entire credentials map) before
put_message()serialization - Decrypt after
get_message()deserialization
Why not use existing TLS keys:
The mTLS CA private key is not persisted on the cluster (explicitly set to empty on reload, bootstrap/src/lib.rs:683). The server TLS key could technically work, but coupling encryption to TLS cert rotation means rotating certs would make encrypted data unrecoverable without a re-encryption migration.
Implementation areas:
navigator-bootstrap: generate and store encryption key as K8s secret duringreconcile_pki()or a newreconcile_encryption_key()stepnavigator-server/src/persistence: add encrypt/decrypt layer aroundput_message()/get_message()for provider objects- Helm chart (
deploy/helm/navigator/templates/statefulset.yaml): mount the new secret - Migration: handle existing unencrypted providers (detect and re-encrypt on first access, or a one-time migration)
Crypto dependencies: ring is already in the transitive dependency tree via rustls. It provides aead::AES_256_GCM which would be suitable. Alternatively, add aes-gcm or chacha20poly1305 as an explicit dependency.
Acceptance Criteria
- Provider plugins declare typed properties with explicit env var discovery/projection mappings
- Provider properties are persisted independently of env var names
- Backward compatibility: existing providers continue to work (migration path for credential key format)
- Sandbox supervisor executes provider-specific hooks before spawning the child process
- Hooks can write config files to the sandbox filesystem following the
write_ca_files()pattern - Written files are accessible to the sandboxed child via policy filesystem rules
- A dedicated symmetric encryption key is generated at cluster bootstrap
- Provider credentials are encrypted at rest in the database
- Existing unencrypted credentials are migrated gracefully
- Architecture doc (
architecture/sandbox-providers.md) is updated to reflect the new design - Existing provider tests updated; new tests for properties mapping, hooks, and encryption