Repository navigation
Description
Problem Statement
Sandboxes need access to inference for platform system functions — initially an embedded agent harness for policy analysis and modifications. Today, the only inference endpoint is inference.local, which serves userland agents/code. Intermixing system inference into this endpoint blurs the boundary between user and platform concerns: different credentials, different models, different billing, different availability guarantees.
We need a separate system-level inference capability for sandbox platform functions, configured independently via nemoclaw inference set --sandbox --provider foo --model bar. Critically, userland code must not be able to access system inference — only the supervisor binary (or supervisor-spawned system processes) should have access.
Supersedes #203 (closed — multi-model routing was the wrong abstraction).
Technical Context
Why NOT a proxy-intercepted hostname
The initial design proposed sandbox.inference.local as a second proxy-intercepted hostname (mirroring inference.local). Investigation revealed this is the wrong approach:
-
The supervisor already has the inference router in-process. The
navigator-sandboxbinary holds anInferenceContextwithArc<RwLock<Vec<ResolvedRoute>>>and anavigator_router::Router. It can make inference API calls directly — no proxy hop needed. -
Network namespace separation already isolates the supervisor. The supervisor runs in the host network namespace; user processes are in an isolated sandbox netns connected only via a veth pair to the proxy at
10.200.0.1:3128. Any hostname exposed through the proxy is reachable by userland. -
inference.localcurrently has zero access control. The proxy short-circuits atproxy.rs:216before OPA evaluation, binary identity checks, or any authentication. Adding a second hostname through the same proxy inherits these gaps. -
In-process calls are simpler and more secure. No hostname to intercept, no TLS cert to generate, no proxy routing changes, no network surface to attack.
Chosen approach: in-process API with Unix socket IPC fallback
- Primary: Supervisor calls the router directly via an in-process API when the embedded agent harness is part of the supervisor binary.
- Fallback (IPC): If the harness runs as a separate process, the supervisor exposes an HTTP endpoint on a Unix domain socket. The socket path is placed in a Landlock-restricted directory inaccessible to userland. The harness process gets access via fd inheritance or a permitted path. It speaks the same OpenAI-compatible HTTP/JSON API — no special SDK needed.
Affected Components
| Component | Key Files | Role |
|---|---|---|
| Proto | proto/inference.proto |
Wire format — needs route name field on set/get requests |
| Server | crates/navigator-server/src/inference.rs |
Storage and resolution — needs to handle two named routes |
| Sandbox lifecycle | crates/navigator-sandbox/src/lib.rs |
Bundle conversion, route refresh, and in-process inference API |
| CLI | crates/navigator-cli/src/main.rs, run.rs |
Needs --sandbox flag on set/get/update commands |
| Architecture docs | architecture/inference-routing.md, sandbox.md, gateway.md |
Need updating |
No changes required in: router (navigator-router), proxy (proxy.rs), inference pattern detection (l7/inference.rs), TLS (l7/tls.rs), DNS/network namespace, gRPC client.
Technical Investigation
Architecture Overview
Supervisor process model (crates/navigator-sandbox/src/lib.rs):
- The supervisor IS the
navigator-sandboxbinary (PID 1 in the container) - Runs in the host network namespace with root privileges
- Spawns user processes (entrypoint + SSH sessions) into an isolated sandbox netns via
setns()(process.rs:171) - User processes are privilege-dropped (
process.rs:180) and Landlock/seccomp restricted (process.rs:183) - The proxy binds on the veth host IP
10.200.0.1:3128— the sole network gateway from sandbox netns
Current inference pipeline (user-facing, inference.local):
- User process sends
CONNECT inference.local:443to proxy viaHTTPS_PROXYenv var - Proxy matches hostname string at
proxy.rs:216(short-circuits before OPA/identity checks) - TLS terminates with ephemeral cert, HTTP parsed, pattern detected
route_inference_request()reads routes fromInferenceContext.routes, passes to router- Router picks first protocol-matching route, overwrites
modelfield in body (backend.rs:82-93)
Proposed system inference pipeline (supervisor-only):
- Supervisor receives system inference request (from embedded harness or via Unix socket IPC)
- Reads system routes from a separate route cache (populated from the same bundle delivery)
- Calls
navigator_router::Router::proxy_to_backend()directly — same backend proxy logic, no CONNECT/TLS overhead - Returns response to the harness
Code References
| Location | Description |
|---|---|
crates/navigator-sandbox/src/lib.rs:143 |
run_sandbox() — supervisor orchestrator, owns all contexts |
crates/navigator-sandbox/src/lib.rs:569-674 |
build_inference_context() — builds router + routes from bundle |
crates/navigator-sandbox/src/lib.rs:677-696 |
bundle_to_resolved_routes() — proto→router conversion |
crates/navigator-sandbox/src/lib.rs:704-743 |
spawn_route_refresh() — polls gateway every 5s, replaces route cache |
crates/navigator-sandbox/src/proxy.rs:21 |
INFERENCE_LOCAL_HOST = "inference.local" — hardcoded constant |
crates/navigator-sandbox/src/proxy.rs:48-75 |
InferenceContext — holds patterns, router, routes: Arc<RwLock<Vec<ResolvedRoute>>> |
crates/navigator-sandbox/src/proxy.rs:216-229 |
inference.local short-circuit — no auth, no OPA, no identity check |
crates/navigator-sandbox/src/process.rs:163-183 |
User process pre_exec: setns() → drop_privileges() → Landlock/seccomp |
crates/navigator-sandbox/src/sandbox/linux/netns.rs:53 |
Network namespace creation — veth pair, 10.200.0.1 (host) / 10.200.0.2 (sandbox) |
crates/navigator-sandbox/src/sandbox/linux/landlock.rs |
Landlock filesystem restrictions on user processes |
crates/navigator-server/src/inference.rs:28 |
CLUSTER_INFERENCE_ROUTE_NAME = "inference.local" — hardcoded |
crates/navigator-server/src/inference.rs:118-171 |
upsert_cluster_inference_route() — hardcodes route name |
crates/navigator-server/src/inference.rs:260-291 |
resolve_inference_bundle() — resolves single route |
crates/navigator-server/src/inference.rs:293-340 |
resolve_managed_cluster_route() — hardcoded to single name |
proto/inference.proto:50-55 |
SetClusterInferenceRequest — no route name field |
proto/inference.proto:83-89 |
GetInferenceBundleResponse — already repeated ResolvedRoute routes |
proto/inference.proto:74-81 |
Proto ResolvedRoute — has name field (line 75) |
crates/navigator-router/src/lib.rs:59-97 |
proxy_with_candidates() — route selection by protocol |
crates/navigator-router/src/backend.rs:21-122 |
proxy_to_backend() — backend proxying, model overwrite |
crates/navigator-cli/src/main.rs:811-837 |
ClusterInferenceCommands — no sandbox flag |
crates/navigator-cli/src/run.rs:2787-2809 |
cluster_inference_set() — single-route gRPC request |
crates/navigator-cli/src/run.rs:2852-2866 |
cluster_inference_get() — displays single config |
Current Behavior
- One inference endpoint:
inference.local, accessible to all sandbox processes without authentication - One
InferenceRouterecord stored (name:"inference.local") - Bundle response returns 0 or 1 routes
- Supervisor holds the
InferenceContextwith a single route cache - No system-level inference capability exists
What Would Need to Change
Proto layer (proto/inference.proto):
- Add
string route_name = 3toSetClusterInferenceRequest(empty ="inference.local"for backward compat) - Add
string route_name = 1toGetClusterInferenceRequest - Consider adding
string route_nameto response messages for disambiguation
Server layer (crates/navigator-server/src/inference.rs):
- Add
SANDBOX_INFERENCE_ROUTE_NAMEconstant (e.g.,"sandbox-system") - Parameterize
upsert_cluster_inference_route()to accept route name - Parameterize
set_cluster_inference()andget_cluster_inference()handlers - Update
resolve_inference_bundle()to resolve both named routes - The store already supports multiple
inference_routerecords with different names — no store changes
Sandbox lifecycle (crates/navigator-sandbox/src/lib.rs):
- Add a second route cache for system routes to
InferenceContext(or a newSystemInferenceContext) - During
bundle_to_resolved_routes(), segregate routes by protoResolvedRoute.name spawn_route_refresh()populates both caches- Expose an in-process API for the supervisor to make inference calls using the system routes
IPC fallback (new code in crates/navigator-sandbox/):
- Unix domain socket HTTP server bound to a Landlock-restricted path
- Accepts OpenAI-compatible requests, routes through system routes via the router
- Socket path passed to harness process via env var or fd inheritance
CLI (crates/navigator-cli/src/main.rs, run.rs):
- Add
--sandboxflag toSet,Get,Updatecommands - When
--sandbox, setroute_name = "sandbox-system"in the request inference get(without flag) shows both endpoints by default
Patterns to Follow
- The
CLUSTER_INFERENCE_ROUTE_NAMEconstant pattern — add a parallel constant - The existing route storage uses
get_message_by_name::<InferenceRoute>(name)— second named route just works GetInferenceBundleResponse.routesis alreadyrepeated— designed for multiple routes- The SSH handshake secret pattern (
ssh.rs:206) — HMAC-based auth with nonce — could inform IPC auth if needed
Proposed Approach
Store a second InferenceRoute record (name: "sandbox-system") via nemoclaw inference set --sandbox. Include both routes in the inference bundle. On the sandbox side, segregate routes by name during bundle conversion and hold a separate system route cache. Expose system inference as an in-process API on the supervisor, with a Unix socket IPC fallback for non-embedded harness processes. The proxy, router, TLS, and DNS layers require no changes.
Scope Assessment
- Complexity: Low-Medium — server and CLI changes are straightforward parameterization; the in-process API is a thin wrapper around existing router calls; IPC is the most novel piece but well-understood
- Confidence: High — leverages existing infrastructure (bundle delivery, route caching, router) with minimal new surface
- Estimated files to change: ~8-10 (proto, server inference, sandbox lib, CLI main, CLI run, docs; plus new IPC module if building the fallback)
- Issue type:
feat
Risks & Open Questions
- Agent harness packaging: How the harness gets into the sandbox container is a separate concern. This feature provides the inference capability; the harness deployment is orthogonal.
- Observability: System inference calls should be logged separately from user inference. At minimum, a separate log file or distinct log tags/labels. The in-process API should emit structured logs with a
source=systemdiscriminator. - Unconfigured endpoint behavior: Should match current
inference.localbehavior — when the sandbox route isn't configured, the system route cache is empty and inference calls return an appropriate error (503 or equivalent). Currently wheninference.localis unconfigured, the bundle returns empty routes and the proxy returns an error to the caller. inference getdisplay: Show both user and system configurations by default.--sandboxflag filters to system only.- IPC socket security: The Unix socket path must be in a directory that Landlock prevents user processes from accessing. Verify that the Landlock policy restricts the chosen path. The supervisor creates the socket before spawning user processes, so ordering is natural.
- Future extensibility: Keep it simple — two route types (user + system) is concrete. N route types is speculative and not needed now.
Test Considerations
- Server unit tests (
crates/navigator-server/src/inference.rs): Test storing/retrieving two routes independently, resolving both in a single bundle, version independence between routes. Follow existing patterns (lines 342-581). - Bundle conversion test (
crates/navigator-sandbox/src/lib.rs): Test that route segregation by name correctly populates user vs. system caches. - In-process API test: Test that the supervisor can make inference calls using system routes, with correct model overwrite and credential resolution.
- IPC test (if built): Test Unix socket endpoint accepts requests, routes through system routes, and is inaccessible from a Landlock-restricted process.
- E2E test (
e2e/python/test_inference_routing.py): Configure both user and system routes, verify user endpoint still works, verify system endpoint uses correct credentials/model. Verify userland cannot reach system inference. - Backward compat: Existing single-route clusters work unchanged (no system route configured, user route unaffected).
Created by spike investigation. Supersedes #203. Use build-from-issue to plan and implement.
Activity
johntmyers commentedon Mar 10, 2026
Implementation Plan
Issue type:
featComplexity: Medium
Confidence: High — all code paths are well-understood, proto extension is purely additive, existing test infrastructure covers the major flows
Summary
Add a second managed inference route named
"sandbox-system"stored alongside"inference.local". On the gateway, the CLI gains a--sandboxflag to target the system route, andinference getdisplays both endpoints. On the sandbox, the bundle is segregated by route name into two caches, and a new in-process API onInferenceContextexposes the system route for supervisor-only platform function calls. The IPC/Unix-socket fallback is explicitly out of scope for this build.Scope
proto/inference.proto: Addroute_namefields to Set/Get request/response messagescrates/navigator-server/src/inference.rs: AddSANDBOX_SYSTEM_ROUTE_NAMEconstant, parameterizeupsert_cluster_inference_route()andresolve_managed_cluster_route()to accept route name, updateresolve_inference_bundle()to resolve both routes, addeffective_route_name()validation helpercrates/navigator-router/src/config.rs: Addname: Stringfield toResolvedRoutecrates/navigator-sandbox/src/lib.rs: Propagatenameinbundle_to_resolved_routes(), segregate routes by name inbuild_inference_context(), updatespawn_route_refresh()to populate both cachescrates/navigator-sandbox/src/proxy.rs: Addsystem_routescache toInferenceContext, addsystem_route_cache()andsystem_inference()methodscrates/navigator-cli/src/main.rs: Add--sandboxflag toSet,Get,Updatecommandscrates/navigator-cli/src/run.rs: Passroute_namethroughcluster_inference_set(),cluster_inference_get(),cluster_inference_update()crates/navigator-sandbox/tests/system_inference.rs: Integration test with mock HTTP backendarchitecture/inference-routing.md: Add "System Inference Route" sectionImplementation Steps
Proto extension — Add
string route_namefields toSetClusterInferenceRequest(field 3),SetClusterInferenceResponse(field 4),GetClusterInferenceRequest(field 1),GetClusterInferenceResponse(field 4). All optional with empty string defaulting to"inference.local"for backward compat.Add
namefield to routerResolvedRoute— Addpub name: Stringtocrates/navigator-router/src/config.rs::ResolvedRoute. Fix all construction sites across the workspace (tests,bundle_to_resolved_routes,RouteConfig::resolve()). This step will trigger compiler errors that guide fixes.Server-side multi-route support — Add
SANDBOX_SYSTEM_ROUTE_NAME = "sandbox-system"constant. Addeffective_route_name()helper that maps empty →"inference.local"and validates known names. Parameterizeupsert_cluster_inference_route()to acceptroute_nameinstead of using the hardcoded constant. Generalizeresolve_managed_cluster_route()to accept any route name. Updateresolve_inference_bundle()to resolve both"inference.local"and"sandbox-system", collecting allSomeresults. Updateset_cluster_inference()andget_cluster_inference()handlers to readroute_namefrom request.Sandbox route segregation — Add
system_routes: Arc<RwLock<Vec<ResolvedRoute>>>toInferenceContext. Update constructor. Inbuild_inference_context(), after converting the bundle, partition routes bynameinto user and system vecs. Updatespawn_route_refresh()to accept both cacheArcs and partition on each refresh tick. Addsystem_route_cache()accessor.In-process system inference API — Implement
InferenceContext::system_inference()that readsself.system_routes, callsself.router.proxy_with_candidates()with those routes, and returns the response. Pure in-process, no proxy/TLS/network involvement.CLI
--sandboxflag — Addsandbox: booltoSet,Get,Updatecommands. Map toroute_name = "sandbox-system"when set. Forgetwithout--sandbox, fetch both routes and display a table. Handle not-found gracefully ("not configured").Integration test — Rust integration test at
crates/navigator-sandbox/tests/system_inference.rsthat spins up a mock HTTP backend (e.g.,hyperserver returning canned responses), constructs anInferenceContextwith system routes pointing at it, callssystem_inference(), and verifies the request was proxied correctly (model overwrite, headers, response body). Tests the full in-process path without needing a cluster or gateway.Documentation — Update
architecture/inference-routing.mdwith a "System Inference Route" section covering the route name, CLI flag, in-process API, and segregation logic.Test Plan
Existing tests to update:
ResolvedRouteconstruction sites innavigator-routerandnavigator-sandboxtests need the newnamefieldcrates/navigator-sandbox/src/lib.rstests:bundle_to_resolved_routes_converts_all_fields,route_cache_preserves_contentNew unit tests — server (
crates/navigator-server/src/inference.rs):upsert_system_route_creates_with_correct_namebundle_includes_both_user_and_system_routesbundle_with_only_system_routeget_returns_system_route_when_requestedeffective_route_name_defaults_empty_to_inference_localeffective_route_name_rejects_unknown_nameNew unit tests — sandbox (
crates/navigator-sandbox/src/lib.rs):bundle_to_resolved_routes_preserves_name_fieldroutes_segregated_by_nameIntegration test (
crates/navigator-sandbox/tests/system_inference.rs):hyperon127.0.0.1:0) returning canned chat completion responsesInferenceContextwith system routes pointing at the mocksystem_inference()with a chat completion requestmodelfield was overwritten, response was returned correctlyproxy_with_candidates()→proxy_to_backend()→ mock backend → response) without a clusterE2E tests: Deferred — the in-process API has no external HTTP surface accessible from Python E2E tests. The integration test above covers the sandbox-side flow; server unit tests cover the control plane.
Risks & Open Questions
"inference.local". Risk: Low.route_namevalidation: Restricted to{"", "inference.local", "sandbox-system"}. Easy to extend later if needed.ResolvedRoute.namebreaking change: Addingnameto the router'sResolvedRouteis workspace-internal. All construction sites are in-tree.inference getwithout--sandboxshows both; either may be "not configured".Documentation Impact
architecture/inference-routing.md: New "System Inference Route" sectionRevision 2 — added integration test with mock HTTP backend for smoke testing the in-process API
Revision 1 — initial plan
feat(inference): add sandbox-system inference route for platform-leve…
johntmyers commentedon Mar 10, 2026
✅ Implementation Complete
PR: #209
What was built
A separate system-level inference route (
sandbox-system) accessible via an in-process API on the supervisor. The supervisor runs in the host network namespace, naturally isolating this from userland code in the sandbox netns.Key changes
route_namefields onSetClusterInference{Request,Response},GetClusterInference{Request,Response}ResolvedRoute.namefield for route segregationeffective_route_name()validator;SANDBOX_SYSTEM_ROUTE_NAMEconstantInferenceContext::system_inference()in-process API--sandboxflag oninference set/get/updatearchitecture/inference-routing.mdNot in scope (per discussion)
system_inference()(the agent harness that will call it)feat(inference): add sandbox-system inference route for platform-leve…
feat(inference): add sandbox-system inference route for platform-leve…