diff --git a/architecture/inference-routing.md b/architecture/inference-routing.md
index 0b1b1643c..6224fd647 100644
--- a/architecture/inference-routing.md
+++ b/architecture/inference-routing.md
@@ -225,12 +225,53 @@ Validation at load time requires either `api_key` or `api_key_env` to resolve, a
 | `503` | Upstream unavailable (timeout or connection failure) |
 | `413` | Request body exceeds 10 MiB buffer limit |
 
+## System Inference Route
+
+In addition to the user-facing `inference.local` route, the gateway supports a second managed route named `sandbox-system` for platform system functions (e.g. an embedded agent harness for policy analysis).
+
+### Key differences from user inference
+
+| Aspect | User (`inference.local`) | System (`sandbox-system`) |
+|--------|--------------------------|---------------------------|
+| **Consumer** | Agent code inside sandbox | Supervisor binary only |
+| **Access** | Proxy-intercepted CONNECT | In-process API on `InferenceContext` |
+| **Network surface** | HTTPS to `inference.local:443` | None -- function call |
+| **Route cache** | `InferenceContext.routes` | `InferenceContext.system_routes` |
+
+### In-process API
+
+`InferenceContext::system_inference()` provides the supervisor with direct access to inference using the system routes. It calls `Router::proxy_with_candidates()` with the system route cache -- the same backend proxy logic used for user inference, but without any CONNECT/TLS overhead.
+
+```rust
+ctx.system_inference(
+    "openai_chat_completions",
+    "POST",
+    "/v1/chat/completions",
+    headers,
+    body,
+).await
+```
+
+### Access control
+
+The system route is not exposed through the CONNECT proxy. The supervisor runs in the host network namespace and calls the router directly. User processes are in an isolated sandbox network namespace and cannot reach the in-process API.
+
+### Bundle delivery
+
+Both routes are included in `GetInferenceBundleResponse.routes` (which is `repeated ResolvedRoute`). The sandbox partitions routes by `ResolvedRoute.name` during `bundle_to_resolved_routes()`: routes named `sandbox-system` go to the system cache, everything else goes to the user cache. Both caches are refreshed on the same polling interval.
+
+### Storage
+
+The system route is stored as a separate `InferenceRoute` record in the gateway store with `name = "sandbox-system"`. The `SetClusterInferenceRequest.route_name` field selects which route to target (empty string defaults to `inference.local`).
+
 ## CLI Surface
 
 Cluster inference commands:
 
-- `openshell cluster inference set --provider <name> --model <id>` -- configures cluster inference by referencing a provider record name
-- `openshell cluster inference get` -- displays current cluster inference configuration
+- `openshell cluster inference set --provider <name> --model <id>` -- configures user-facing cluster inference
+- `openshell cluster inference set --system --provider <name> --model <id>` -- configures system inference
+- `openshell cluster inference get` -- displays both user and system inference configuration
+- `openshell cluster inference get --system` -- displays only the system inference configuration
 
 The `--provider` flag references a provider record name (not a provider type). The provider must already exist in the cluster and have a supported inference type (`openai`, `anthropic`, or `nvidia`).
 
diff --git a/crates/navigator-cli/src/main.rs b/crates/navigator-cli/src/main.rs
index b1f1ae88c..cb42f93d9 100644
--- a/crates/navigator-cli/src/main.rs
+++ b/crates/navigator-cli/src/main.rs
@@ -795,6 +795,12 @@ enum InferenceCommands {
         /// Model identifier to force for generation calls.
         #[arg(long)]
         model: String,
+
+        /// Configure the system inference route instead of the user-facing
+        /// route. System inference is used by platform functions (e.g. the
+        /// agent harness) and is not accessible to user code.
+        #[arg(long)]
+        system: bool,
     },
 
     /// Update gateway-level inference configuration (partial update).
@@ -806,10 +812,19 @@ enum InferenceCommands {
         /// Model identifier (unchanged if omitted).
         #[arg(long)]
         model: Option<String>,
+
+        /// Target the system inference route.
+        #[arg(long)]
+        system: bool,
     },
 
     /// Get gateway-level inference provider and model.
-    Get,
+    Get {
+        /// Show the system inference route instead of the user-facing route.
+        /// When omitted, both routes are displayed.
+        #[arg(long)]
+        system: bool,
+    },
 }
 
 #[derive(Subcommand, Debug)]
@@ -1379,20 +1394,33 @@ async fn main() -> Result<()> {
             let mut tls = tls.with_gateway_name(&ctx.name);
             apply_edge_auth(&mut tls, &ctx.name);
             match command {
-                InferenceCommands::Set { provider, model } => {
-                    run::gateway_inference_set(endpoint, &provider, &model, &tls).await?;
+                InferenceCommands::Set {
+                    provider,
+                    model,
+                    system,
+                } => {
+                    let route_name = if system { "sandbox-system" } else { "" };
+                    run::gateway_inference_set(endpoint, &provider, &model, route_name, &tls)
+                        .await?;
                 }
-                InferenceCommands::Update { provider, model } => {
+                InferenceCommands::Update {
+                    provider,
+                    model,
+                    system,
+                } => {
+                    let route_name = if system { "sandbox-system" } else { "" };
                     run::gateway_inference_update(
                         endpoint,
                         provider.as_deref(),
                         model.as_deref(),
+                        route_name,
                         &tls,
                     )
                     .await?;
                 }
-                InferenceCommands::Get => {
-                    run::gateway_inference_get(endpoint, &tls).await?;
+                InferenceCommands::Get { system } => {
+                    let route_name = if system { Some("sandbox-system") } else { None };
+                    run::gateway_inference_get(endpoint, route_name, &tls).await?;
                 }
             }
         }
diff --git a/crates/navigator-cli/src/run.rs b/crates/navigator-cli/src/run.rs
index ddce9560e..a9162c188 100644
--- a/crates/navigator-cli/src/run.rs
+++ b/crates/navigator-cli/src/run.rs
@@ -2789,6 +2789,7 @@ pub async fn gateway_inference_set(
     server: &str,
     provider_name: &str,
     model_id: &str,
+    route_name: &str,
     tls: &TlsOptions,
 ) -> Result<()> {
     let mut client = grpc_inference_client(server, tls).await?;
@@ -2796,13 +2797,20 @@ pub async fn gateway_inference_set(
         .set_cluster_inference(SetClusterInferenceRequest {
             provider_name: provider_name.to_string(),
             model_id: model_id.to_string(),
+            route_name: route_name.to_string(),
         })
         .await
         .into_diagnostic()?;
 
     let configured = response.into_inner();
-    println!("{}", "Gateway inference configured:".cyan().bold());
+    let label = if configured.route_name == "sandbox-system" {
+        "System inference configured:"
+    } else {
+        "Gateway inference configured:"
+    };
+    println!("{}", label.cyan().bold());
     println!();
+    println!("  {} {}", "Route:".dimmed(), configured.route_name);
     println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
     println!("  {} {}", "Model:".dimmed(), configured.model_id);
     println!("  {} {}", "Version:".dimmed(), configured.version);
@@ -2813,6 +2821,7 @@ pub async fn gateway_inference_update(
     server: &str,
     provider_name: Option<&str>,
     model_id: Option<&str>,
+    route_name: &str,
     tls: &TlsOptions,
 ) -> Result<()> {
     if provider_name.is_none() && model_id.is_none() {
@@ -2825,7 +2834,9 @@ pub async fn gateway_inference_update(
 
     // Fetch current config to use as base for the partial update.
     let current = client
-        .get_cluster_inference(GetClusterInferenceRequest {})
+        .get_cluster_inference(GetClusterInferenceRequest {
+            route_name: route_name.to_string(),
+        })
         .await
         .into_diagnostic()?
         .into_inner();
@@ -2837,35 +2848,94 @@ pub async fn gateway_inference_update(
         .set_cluster_inference(SetClusterInferenceRequest {
             provider_name: provider.to_string(),
             model_id: model.to_string(),
+            route_name: route_name.to_string(),
         })
         .await
         .into_diagnostic()?;
 
     let configured = response.into_inner();
-    println!("{}", "Gateway inference updated:".cyan().bold());
+    let label = if configured.route_name == "sandbox-system" {
+        "System inference updated:"
+    } else {
+        "Gateway inference updated:"
+    };
+    println!("{}", label.cyan().bold());
     println!();
+    println!("  {} {}", "Route:".dimmed(), configured.route_name);
     println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
     println!("  {} {}", "Model:".dimmed(), configured.model_id);
     println!("  {} {}", "Version:".dimmed(), configured.version);
     Ok(())
 }
 
-pub async fn gateway_inference_get(server: &str, tls: &TlsOptions) -> Result<()> {
+pub async fn gateway_inference_get(
+    server: &str,
+    route_name: Option<&str>,
+    tls: &TlsOptions,
+) -> Result<()> {
     let mut client = grpc_inference_client(server, tls).await?;
-    let response = client
-        .get_cluster_inference(GetClusterInferenceRequest {})
-        .await
-        .into_diagnostic()?;
 
-    let configured = response.into_inner();
-    println!("{}", "Gateway inference:".cyan().bold());
-    println!();
-    println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
-    println!("  {} {}", "Model:".dimmed(), configured.model_id);
-    println!("  {} {}", "Version:".dimmed(), configured.version);
+    if let Some(name) = route_name {
+        // Show a single route (--system was specified).
+        let response = client
+            .get_cluster_inference(GetClusterInferenceRequest {
+                route_name: name.to_string(),
+            })
+            .await
+            .into_diagnostic()?;
+
+        let configured = response.into_inner();
+        let label = if name == "sandbox-system" {
+            "System inference:"
+        } else {
+            "Gateway inference:"
+        };
+        println!("{}", label.cyan().bold());
+        println!();
+        println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
+        println!("  {} {}", "Model:".dimmed(), configured.model_id);
+        println!("  {} {}", "Version:".dimmed(), configured.version);
+    } else {
+        // Show both routes by default.
+        print_inference_route(&mut client, "Gateway inference", "").await;
+        println!();
+        print_inference_route(&mut client, "System inference", "sandbox-system").await;
+    }
     Ok(())
 }
 
+async fn print_inference_route(
+    client: &mut crate::tls::GrpcInferenceClient,
+    label: &str,
+    route_name: &str,
+) {
+    match client
+        .get_cluster_inference(GetClusterInferenceRequest {
+            route_name: route_name.to_string(),
+        })
+        .await
+    {
+        Ok(response) => {
+            let configured = response.into_inner();
+            println!("{}", format!("{label}:").cyan().bold());
+            println!();
+            println!("  {} {}", "Provider:".dimmed(), configured.provider_name);
+            println!("  {} {}", "Model:".dimmed(), configured.model_id);
+            println!("  {} {}", "Version:".dimmed(), configured.version);
+        }
+        Err(e) if e.code() == Code::NotFound => {
+            println!("{}", format!("{label}:").cyan().bold());
+            println!();
+            println!("  {}", "Not configured".dimmed());
+        }
+        Err(e) => {
+            println!("{}", format!("{label}:").cyan().bold());
+            println!();
+            println!("  {} {}", "Error:".red(), e.message());
+        }
+    }
+}
+
 pub fn git_repo_root(local_path: &Path) -> Result<PathBuf> {
     let git_dir = if local_path.is_dir() {
         local_path
diff --git a/crates/navigator-router/src/config.rs b/crates/navigator-router/src/config.rs
index 530ebc63c..1caf3d262 100644
--- a/crates/navigator-router/src/config.rs
+++ b/crates/navigator-router/src/config.rs
@@ -35,6 +35,8 @@ pub struct RouteConfig {
 /// caller during resolution.
 #[derive(Clone)]
 pub struct ResolvedRoute {
+    /// Route name used for identification (e.g. "inference.local", "sandbox-system").
+    pub name: String,
     pub endpoint: String,
     pub model: String,
     pub api_key: String,
@@ -48,6 +50,7 @@ pub struct ResolvedRoute {
 impl std::fmt::Debug for ResolvedRoute {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("ResolvedRoute")
+            .field("name", &self.name)
             .field("endpoint", &self.endpoint)
             .field("model", &self.model)
             .field("api_key", &"[REDACTED]")
@@ -119,6 +122,7 @@ impl RouteConfig {
         let (auth, default_headers) = auth_from_provider_type(self.provider_type.as_deref());
 
         Ok(ResolvedRoute {
+            name: self.name.clone(),
             endpoint: self.endpoint.clone(),
             model: self.model.clone(),
             api_key: self.resolve_api_key()?,
@@ -245,6 +249,7 @@ routes:
     #[test]
     fn resolved_route_debug_redacts_api_key() {
         let route = ResolvedRoute {
+            name: "test".to_string(),
             endpoint: "https://api.example.com/v1".to_string(),
             model: "test-model".to_string(),
             api_key: "sk-super-secret-key-12345".to_string(),
diff --git a/crates/navigator-router/src/mock.rs b/crates/navigator-router/src/mock.rs
index ccc1066a6..435773c1d 100644
--- a/crates/navigator-router/src/mock.rs
+++ b/crates/navigator-router/src/mock.rs
@@ -124,6 +124,7 @@ mod tests {
 
     fn make_route(endpoint: &str, protocols: &[&str], model: &str) -> ResolvedRoute {
         ResolvedRoute {
+            name: "test".to_string(),
             endpoint: endpoint.to_string(),
             model: model.to_string(),
             api_key: "key".to_string(),
diff --git a/crates/navigator-router/tests/backend_integration.rs b/crates/navigator-router/tests/backend_integration.rs
index 740b12f30..d8d95a2e9 100644
--- a/crates/navigator-router/tests/backend_integration.rs
+++ b/crates/navigator-router/tests/backend_integration.rs
@@ -8,6 +8,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate};
 
 fn mock_candidates(base_url: &str) -> Vec<ResolvedRoute> {
     vec![ResolvedRoute {
+        name: "inference.local".to_string(),
         endpoint: base_url.to_string(),
         model: "meta/llama-3.1-8b-instruct".to_string(),
         api_key: "test-api-key".to_string(),
@@ -109,6 +110,7 @@ async fn proxy_upstream_401_returns_error() {
 async fn proxy_no_compatible_route_returns_error() {
     let router = Router::new().unwrap();
     let candidates = vec![ResolvedRoute {
+        name: "inference.local".to_string(),
         endpoint: "http://localhost:1234".to_string(),
         model: "test".to_string(),
         api_key: "key".to_string(),
@@ -169,6 +171,7 @@ async fn proxy_strips_auth_header() {
 async fn proxy_mock_route_returns_canned_response() {
     let router = Router::new().unwrap();
     let candidates = vec![ResolvedRoute {
+        name: "inference.local".to_string(),
         endpoint: "mock://test".to_string(),
         model: "mock/test-model".to_string(),
         api_key: "unused".to_string(),
@@ -302,6 +305,7 @@ async fn proxy_uses_x_api_key_for_anthropic_route() {
 
     let router = Router::new().unwrap();
     let candidates = vec![ResolvedRoute {
+        name: "inference.local".to_string(),
         endpoint: mock_server.uri(),
         model: "claude-sonnet-4-20250514".to_string(),
         api_key: "test-anthropic-key".to_string(),
@@ -359,6 +363,7 @@ async fn proxy_anthropic_does_not_send_bearer_auth() {
 
     let router = Router::new().unwrap();
     let candidates = vec![ResolvedRoute {
+        name: "inference.local".to_string(),
         endpoint: mock_server.uri(),
         model: "claude-sonnet-4-20250514".to_string(),
         api_key: "anthropic-key".to_string(),
diff --git a/crates/navigator-sandbox/src/lib.rs b/crates/navigator-sandbox/src/lib.rs
index 6b9d6982e..f657db63f 100644
--- a/crates/navigator-sandbox/src/lib.rs
+++ b/crates/navigator-sandbox/src/lib.rs
@@ -13,7 +13,7 @@ pub mod opa;
 mod policy;
 mod process;
 pub mod procfs;
-mod proxy;
+pub mod proxy;
 mod sandbox;
 mod ssh;
 
@@ -651,11 +651,19 @@ async fn build_inference_context(
         "Inference routing enabled with local execution"
     );
 
+    // Partition routes by name into user-facing and system caches.
+    let (user_routes, system_routes) = partition_routes(routes);
+
     let router =
         Router::new().map_err(|e| miette::miette!("failed to initialize inference router: {e}"))?;
     let patterns = l7::inference::default_patterns();
 
-    let ctx = Arc::new(proxy::InferenceContext::new(patterns, router, routes));
+    let ctx = Arc::new(proxy::InferenceContext::new(
+        patterns,
+        router,
+        user_routes,
+        system_routes,
+    ));
 
     // Spawn background route cache refresh for cluster mode at startup so
     // request handling never depends on control-plane latency.
@@ -664,6 +672,7 @@ async fn build_inference_context(
     {
         spawn_route_refresh(
             ctx.route_cache(),
+            ctx.system_route_cache(),
             endpoint.to_string(),
             route_refresh_interval_secs(),
             initial_revision,
@@ -673,6 +682,31 @@ async fn build_inference_context(
     Ok(Some(ctx))
 }
 
+/// Route name for the sandbox system inference route.
+const SANDBOX_SYSTEM_ROUTE_NAME: &str = "sandbox-system";
+
+/// Split resolved routes into user-facing and system caches by route name.
+///
+/// Routes named `"sandbox-system"` go to the system cache; everything else
+/// (including `"inference.local"` and empty names) goes to the user cache.
+fn partition_routes(
+    routes: Vec<navigator_router::config::ResolvedRoute>,
+) -> (
+    Vec<navigator_router::config::ResolvedRoute>,
+    Vec<navigator_router::config::ResolvedRoute>,
+) {
+    let mut user = Vec::new();
+    let mut system = Vec::new();
+    for r in routes {
+        if r.name == SANDBOX_SYSTEM_ROUTE_NAME {
+            system.push(r);
+        } else {
+            user.push(r);
+        }
+    }
+    (user, system)
+}
+
 /// Convert a proto bundle response into resolved routes for the router.
 pub(crate) fn bundle_to_resolved_routes(
     bundle: &navigator_core::proto::GetInferenceBundleResponse,
@@ -684,6 +718,7 @@ pub(crate) fn bundle_to_resolved_routes(
             let (auth, default_headers) =
                 navigator_core::inference::auth_for_provider_type(&r.provider_type);
             navigator_router::config::ResolvedRoute {
+                name: r.name.clone(),
                 endpoint: r.base_url.clone(),
                 model: r.model_id.clone(),
                 api_key: r.api_key.clone(),
@@ -695,14 +730,15 @@ pub(crate) fn bundle_to_resolved_routes(
         .collect()
 }
 
-/// Spawn a background task that periodically refreshes the route cache from the gateway.
+/// Spawn a background task that periodically refreshes both route caches from the gateway.
 ///
 /// The loop uses the bundle `revision` hash to avoid unnecessary cache writes
 /// when routes haven't changed. `initial_revision` is the revision captured
 /// during the startup fetch in [`build_inference_context`] so the first refresh
 /// cycle can already skip a no-op update.
 pub(crate) fn spawn_route_refresh(
-    cache: Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>>,
+    user_cache: Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>>,
+    system_cache: Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>>,
     endpoint: String,
     interval_secs: u64,
     initial_revision: Option<String>,
@@ -726,13 +762,16 @@ pub(crate) fn spawn_route_refresh(
                     }
 
                     let routes = bundle_to_resolved_routes(&bundle);
+                    let (user_routes, system_routes) = partition_routes(routes);
                     info!(
-                        route_count = routes.len(),
+                        user_route_count = user_routes.len(),
+                        system_route_count = system_routes.len(),
                         revision = %bundle.revision,
                         "Inference routes updated"
                     );
                     current_revision = Some(bundle.revision);
-                    *cache.write().await = routes;
+                    *user_cache.write().await = user_routes;
+                    *system_cache.write().await = system_routes;
                 }
                 Err(e) => {
                     warn!(error = %e, "Failed to refresh inference route cache, keeping stale routes");
@@ -1260,6 +1299,55 @@ mod tests {
         assert!(routes.is_empty());
     }
 
+    #[test]
+    fn bundle_to_resolved_routes_preserves_name_field() {
+        let bundle = navigator_core::proto::GetInferenceBundleResponse {
+            routes: vec![navigator_core::proto::ResolvedRoute {
+                name: "sandbox-system".to_string(),
+                base_url: "https://api.example.com/v1".to_string(),
+                api_key: "key".to_string(),
+                model_id: "model".to_string(),
+                protocols: vec!["openai_chat_completions".to_string()],
+                provider_type: "openai".to_string(),
+            }],
+            revision: "rev".to_string(),
+            generated_at_ms: 0,
+        };
+
+        let routes = bundle_to_resolved_routes(&bundle);
+        assert_eq!(routes[0].name, "sandbox-system");
+    }
+
+    #[test]
+    fn routes_segregated_by_name() {
+        let routes = vec![
+            navigator_router::config::ResolvedRoute {
+                name: "inference.local".to_string(),
+                endpoint: "https://api.openai.com/v1".to_string(),
+                model: "gpt-4o".to_string(),
+                api_key: "key1".to_string(),
+                protocols: vec!["openai_chat_completions".to_string()],
+                auth: navigator_core::inference::AuthHeader::Bearer,
+                default_headers: vec![],
+            },
+            navigator_router::config::ResolvedRoute {
+                name: "sandbox-system".to_string(),
+                endpoint: "https://api.anthropic.com/v1".to_string(),
+                model: "claude-sonnet-4-20250514".to_string(),
+                api_key: "key2".to_string(),
+                protocols: vec!["anthropic_messages".to_string()],
+                auth: navigator_core::inference::AuthHeader::Custom("x-api-key"),
+                default_headers: vec![],
+            },
+        ];
+
+        let (user, system) = partition_routes(routes);
+        assert_eq!(user.len(), 1);
+        assert_eq!(user[0].name, "inference.local");
+        assert_eq!(system.len(), 1);
+        assert_eq!(system[0].name, "sandbox-system");
+    }
+
     // -- build_inference_context tests --
 
     #[tokio::test]
@@ -1531,6 +1619,7 @@ filesystem_policy:
         use tokio::sync::RwLock;
 
         let routes = vec![navigator_router::config::ResolvedRoute {
+            name: "inference.local".to_string(),
             endpoint: "http://original:8000/v1".to_string(),
             model: "original-model".to_string(),
             api_key: "key".to_string(),
diff --git a/crates/navigator-sandbox/src/proxy.rs b/crates/navigator-sandbox/src/proxy.rs
index a65650c38..6f2761384 100644
--- a/crates/navigator-sandbox/src/proxy.rs
+++ b/crates/navigator-sandbox/src/proxy.rs
@@ -46,11 +46,16 @@ enum InferenceOutcome {
 
 /// Inference routing context for sandbox-local execution.
 ///
-/// Holds a `Router` (HTTP client) and a cached set of resolved routes.
+/// Holds a `Router` (HTTP client) and cached sets of resolved routes.
+/// User routes serve `inference.local` traffic; system routes are consumed
+/// in-process by the supervisor for platform functions (e.g. agent harness).
 pub struct InferenceContext {
     pub patterns: Vec<crate::l7::inference::InferenceApiPattern>,
     router: navigator_router::Router,
+    /// Routes for the user-facing `inference.local` endpoint.
     routes: Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>>,
+    /// Routes for supervisor-only system inference (`sandbox-system`).
+    system_routes: Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>>,
 }
 
 impl InferenceContext {
@@ -58,20 +63,48 @@ impl InferenceContext {
         patterns: Vec<crate::l7::inference::InferenceApiPattern>,
         router: navigator_router::Router,
         routes: Vec<navigator_router::config::ResolvedRoute>,
+        system_routes: Vec<navigator_router::config::ResolvedRoute>,
     ) -> Self {
         Self {
             patterns,
             router,
             routes: Arc::new(tokio::sync::RwLock::new(routes)),
+            system_routes: Arc::new(tokio::sync::RwLock::new(system_routes)),
         }
     }
 
-    /// Get a handle to the route cache for background refresh.
+    /// Get a handle to the user route cache for background refresh.
     pub fn route_cache(
         &self,
     ) -> Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>> {
         self.routes.clone()
     }
+
+    /// Get a handle to the system route cache for background refresh.
+    pub fn system_route_cache(
+        &self,
+    ) -> Arc<tokio::sync::RwLock<Vec<navigator_router::config::ResolvedRoute>>> {
+        self.system_routes.clone()
+    }
+
+    /// Make an inference call using system routes (supervisor-only).
+    ///
+    /// This is the in-process API for platform functions. It bypasses the
+    /// CONNECT proxy entirely — the supervisor calls the router directly
+    /// from the host network namespace.
+    pub async fn system_inference(
+        &self,
+        protocol: &str,
+        method: &str,
+        path: &str,
+        headers: Vec<(String, String)>,
+        body: bytes::Bytes,
+    ) -> Result<navigator_router::ProxyResponse, navigator_router::RouterError> {
+        let routes = self.system_routes.read().await;
+        self.router
+            .proxy_with_candidates(protocol, method, path, headers, body, &routes)
+            .await
+    }
 }
 
 #[derive(Debug)]
diff --git a/crates/navigator-sandbox/tests/system_inference.rs b/crates/navigator-sandbox/tests/system_inference.rs
new file mode 100644
index 000000000..e7f0ad543
--- /dev/null
+++ b/crates/navigator-sandbox/tests/system_inference.rs
@@ -0,0 +1,152 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Integration test for the in-process system inference API.
+//!
+//! Uses the router's built-in `mock://` route support to verify the full
+//! in-process path: route selection → `proxy_with_candidates()` →
+//! mock backend → response.
+
+use navigator_router::Router;
+use navigator_router::config::{AuthHeader, ResolvedRoute};
+use navigator_sandbox::proxy::InferenceContext;
+
+fn make_system_route() -> ResolvedRoute {
+    ResolvedRoute {
+        name: "sandbox-system".to_string(),
+        endpoint: "mock://system-test".to_string(),
+        model: "system/policy-analyzer".to_string(),
+        api_key: "system-key".to_string(),
+        protocols: vec!["openai_chat_completions".to_string()],
+        auth: AuthHeader::Bearer,
+        default_headers: Vec::new(),
+    }
+}
+
+fn make_user_route() -> ResolvedRoute {
+    ResolvedRoute {
+        name: "inference.local".to_string(),
+        endpoint: "mock://user-test".to_string(),
+        model: "user/gpt-4o".to_string(),
+        api_key: "user-key".to_string(),
+        protocols: vec!["openai_chat_completions".to_string()],
+        auth: AuthHeader::Bearer,
+        default_headers: Vec::new(),
+    }
+}
+
+#[tokio::test]
+async fn system_inference_routes_to_mock_backend() {
+    let router = Router::new().unwrap();
+    let patterns = navigator_sandbox::l7::inference::default_patterns();
+
+    let ctx = InferenceContext::new(
+        patterns,
+        router,
+        vec![make_user_route()],
+        vec![make_system_route()],
+    );
+
+    let body = serde_json::to_vec(&serde_json::json!({
+        "model": "anything",
+        "messages": [{"role": "user", "content": "analyze this policy"}]
+    }))
+    .unwrap();
+
+    let response = ctx
+        .system_inference(
+            "openai_chat_completions",
+            "POST",
+            "/v1/chat/completions",
+            vec![("content-type".to_string(), "application/json".to_string())],
+            bytes::Bytes::from(body),
+        )
+        .await
+        .expect("system_inference should succeed");
+
+    assert_eq!(response.status, 200);
+
+    // Verify the mock response came back with the system model
+    let resp_body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
+    assert_eq!(resp_body["model"], "system/policy-analyzer");
+
+    // Verify the mock header is present
+    assert!(
+        response
+            .headers
+            .iter()
+            .any(|(k, v)| k == "x-navigator-mock" && v == "true")
+    );
+}
+
+#[tokio::test]
+async fn system_inference_uses_system_routes_not_user_routes() {
+    let router = Router::new().unwrap();
+    let patterns = navigator_sandbox::l7::inference::default_patterns();
+
+    // Only user routes configured — no system routes
+    let ctx = InferenceContext::new(patterns, router, vec![make_user_route()], vec![]);
+
+    let body = serde_json::to_vec(&serde_json::json!({
+        "model": "gpt-4o",
+        "messages": [{"role": "user", "content": "hello"}]
+    }))
+    .unwrap();
+
+    let result = ctx
+        .system_inference(
+            "openai_chat_completions",
+            "POST",
+            "/v1/chat/completions",
+            vec![],
+            bytes::Bytes::from(body),
+        )
+        .await;
+
+    // Should fail because the system route cache is empty — user routes
+    // are not accessible through system_inference().
+    assert!(
+        result.is_err(),
+        "system_inference should fail when no system routes are configured"
+    );
+}
+
+#[tokio::test]
+async fn system_inference_with_anthropic_protocol() {
+    let router = Router::new().unwrap();
+    let patterns = navigator_sandbox::l7::inference::default_patterns();
+
+    let system_route = ResolvedRoute {
+        name: "sandbox-system".to_string(),
+        endpoint: "mock://anthropic-system".to_string(),
+        model: "claude-sonnet-4-20250514".to_string(),
+        api_key: "ant-key".to_string(),
+        protocols: vec!["anthropic_messages".to_string()],
+        auth: AuthHeader::Custom("x-api-key"),
+        default_headers: vec![("anthropic-version".to_string(), "2023-06-01".to_string())],
+    };
+
+    let ctx = InferenceContext::new(patterns, router, vec![], vec![system_route]);
+
+    let body = serde_json::to_vec(&serde_json::json!({
+        "model": "claude-sonnet-4-20250514",
+        "max_tokens": 1,
+        "messages": [{"role": "user", "content": "analyze policy"}]
+    }))
+    .unwrap();
+
+    let response = ctx
+        .system_inference(
+            "anthropic_messages",
+            "POST",
+            "/v1/messages",
+            vec![("content-type".to_string(), "application/json".to_string())],
+            bytes::Bytes::from(body),
+        )
+        .await
+        .expect("anthropic system_inference should succeed");
+
+    assert_eq!(response.status, 200);
+    let resp_body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
+    assert_eq!(resp_body["type"], "message");
+}
diff --git a/crates/navigator-server/src/inference.rs b/crates/navigator-server/src/inference.rs
index 14b7a2095..dda820ff6 100644
--- a/crates/navigator-server/src/inference.rs
+++ b/crates/navigator-server/src/inference.rs
@@ -26,6 +26,20 @@ impl InferenceService {
 }
 
 const CLUSTER_INFERENCE_ROUTE_NAME: &str = "inference.local";
+const SANDBOX_SYSTEM_ROUTE_NAME: &str = "sandbox-system";
+
+/// Map a request `route_name` to the canonical store key.
+///
+/// Empty string defaults to `CLUSTER_INFERENCE_ROUTE_NAME` for backward compat.
+fn effective_route_name(name: &str) -> Result<&str, Status> {
+    match name.trim() {
+        "" | "inference.local" => Ok(CLUSTER_INFERENCE_ROUTE_NAME),
+        "sandbox-system" => Ok(SANDBOX_SYSTEM_ROUTE_NAME),
+        other => Err(Status::invalid_argument(format!(
+            "unknown route_name '{other}'; expected 'inference.local' or 'sandbox-system'"
+        ))),
+    }
+}
 
 impl ObjectType for InferenceRoute {
     fn object_type() -> &'static str {
@@ -61,8 +75,10 @@ impl Inference for InferenceService {
         request: Request<SetClusterInferenceRequest>,
     ) -> Result<Response<SetClusterInferenceResponse>, Status> {
         let req = request.into_inner();
+        let route_name = effective_route_name(&req.route_name)?;
         let route = upsert_cluster_inference_route(
             self.state.store.as_ref(),
+            route_name,
             &req.provider_name,
             &req.model_id,
         )
@@ -77,23 +93,26 @@ impl Inference for InferenceService {
             provider_name: config.provider_name.clone(),
             model_id: config.model_id.clone(),
             version: route.version,
+            route_name: route_name.to_string(),
         }))
     }
 
     async fn get_cluster_inference(
         &self,
-        _request: Request<GetClusterInferenceRequest>,
+        request: Request<GetClusterInferenceRequest>,
     ) -> Result<Response<GetClusterInferenceResponse>, Status> {
+        let req = request.into_inner();
+        let route_name = effective_route_name(&req.route_name)?;
         let route = self
             .state
             .store
-            .get_message_by_name::<InferenceRoute>(CLUSTER_INFERENCE_ROUTE_NAME)
+            .get_message_by_name::<InferenceRoute>(route_name)
             .await
             .map_err(|e| Status::internal(format!("fetch route failed: {e}")))?
             .ok_or_else(|| {
-                Status::not_found(
-                    "cluster inference is not configured; run 'openshell cluster inference set --provider <name> --model <id>'",
-                )
+                Status::not_found(format!(
+                    "inference route '{route_name}' is not configured; run 'openshell cluster inference set --provider <name> --model <id>'"
+                ))
             })?;
 
         let config = route
@@ -111,12 +130,14 @@ impl Inference for InferenceService {
             provider_name: config.provider_name.clone(),
             model_id: config.model_id.clone(),
             version: route.version,
+            route_name: route_name.to_string(),
         }))
     }
 }
 
 async fn upsert_cluster_inference_route(
     store: &Store,
+    route_name: &str,
     provider_name: &str,
     model_id: &str,
 ) -> Result<InferenceRoute, Status> {
@@ -142,7 +163,7 @@ async fn upsert_cluster_inference_route(
     let config = build_cluster_inference_config(&provider, model_id);
 
     let existing = store
-        .get_message_by_name::<InferenceRoute>(CLUSTER_INFERENCE_ROUTE_NAME)
+        .get_message_by_name::<InferenceRoute>(route_name)
         .await
         .map_err(|e| Status::internal(format!("fetch route failed: {e}")))?;
 
@@ -156,7 +177,7 @@ async fn upsert_cluster_inference_route(
     } else {
         InferenceRoute {
             id: uuid::Uuid::new_v4().to_string(),
-            name: CLUSTER_INFERENCE_ROUTE_NAME.to_string(),
+            name: route_name.to_string(),
             config: Some(config),
             version: 1,
         }
@@ -256,12 +277,15 @@ fn find_provider_config_value(provider: &Provider, preferred_keys: &[&str]) -> O
     None
 }
 
-/// Resolve the inference bundle (managed cluster route + revision hash).
+/// Resolve the inference bundle (all managed routes + revision hash).
 async fn resolve_inference_bundle(store: &Store) -> Result<GetInferenceBundleResponse, Status> {
-    let routes = resolve_managed_cluster_route(store)
-        .await?
-        .into_iter()
-        .collect::<Vec<_>>();
+    let mut routes = Vec::new();
+    if let Some(r) = resolve_route_by_name(store, CLUSTER_INFERENCE_ROUTE_NAME).await? {
+        routes.push(r);
+    }
+    if let Some(r) = resolve_route_by_name(store, SANDBOX_SYSTEM_ROUTE_NAME).await? {
+        routes.push(r);
+    }
 
     let now_ms = std::time::SystemTime::now()
         .duration_since(std::time::UNIX_EPOCH)
@@ -290,9 +314,12 @@ async fn resolve_inference_bundle(store: &Store) -> Result<GetInferenceBundleRes
     })
 }
 
-async fn resolve_managed_cluster_route(store: &Store) -> Result<Option<ResolvedRoute>, Status> {
+async fn resolve_route_by_name(
+    store: &Store,
+    route_name: &str,
+) -> Result<Option<ResolvedRoute>, Status> {
     let route = store
-        .get_message_by_name::<InferenceRoute>(CLUSTER_INFERENCE_ROUTE_NAME)
+        .get_message_by_name::<InferenceRoute>(route_name)
         .await
         .map_err(|e| Status::internal(format!("fetch route failed: {e}")))?;
 
@@ -305,15 +332,15 @@ async fn resolve_managed_cluster_route(store: &Store) -> Result<Option<ResolvedR
     };
 
     if config.provider_name.trim().is_empty() {
-        return Err(Status::failed_precondition(
-            "managed route is missing provider_name",
-        ));
+        return Err(Status::failed_precondition(format!(
+            "route '{route_name}' is missing provider_name"
+        )));
     }
 
     if config.model_id.trim().is_empty() {
-        return Err(Status::failed_precondition(
-            "managed route is missing model_id",
-        ));
+        return Err(Status::failed_precondition(format!(
+            "route '{route_name}' is missing model_id"
+        )));
     }
 
     let provider = store
@@ -330,7 +357,7 @@ async fn resolve_managed_cluster_route(store: &Store) -> Result<Option<ResolvedR
     let resolved = resolve_provider_route(&provider)?;
 
     Ok(Some(ResolvedRoute {
-        name: CLUSTER_INFERENCE_ROUTE_NAME.to_string(),
+        name: route_name.to_string(),
         base_url: resolved.base_url,
         model_id: config.model_id.clone(),
         api_key: resolved.api_key,
@@ -377,15 +404,25 @@ mod tests {
             .await
             .expect("provider should persist");
 
-        let first = upsert_cluster_inference_route(&store, "openai-dev", "gpt-4o")
-            .await
-            .expect("first set should succeed");
+        let first = upsert_cluster_inference_route(
+            &store,
+            CLUSTER_INFERENCE_ROUTE_NAME,
+            "openai-dev",
+            "gpt-4o",
+        )
+        .await
+        .expect("first set should succeed");
         assert_eq!(first.name, CLUSTER_INFERENCE_ROUTE_NAME);
         assert_eq!(first.version, 1);
 
-        let second = upsert_cluster_inference_route(&store, "openai-dev", "gpt-4.1")
-            .await
-            .expect("second set should succeed");
+        let second = upsert_cluster_inference_route(
+            &store,
+            CLUSTER_INFERENCE_ROUTE_NAME,
+            "openai-dev",
+            "gpt-4.1",
+        )
+        .await
+        .expect("second set should succeed");
         assert_eq!(second.version, 2);
         assert_eq!(second.id, first.id);
 
@@ -400,7 +437,7 @@ mod tests {
             .await
             .expect("store should connect");
 
-        let route = resolve_managed_cluster_route(&store)
+        let route = resolve_route_by_name(&store, CLUSTER_INFERENCE_ROUTE_NAME)
             .await
             .expect("resolution should not fail");
         assert!(route.is_none());
@@ -516,7 +553,7 @@ mod tests {
             .await
             .expect("route should persist");
 
-        let managed = resolve_managed_cluster_route(&store)
+        let managed = resolve_route_by_name(&store, CLUSTER_INFERENCE_ROUTE_NAME)
             .await
             .expect("route should resolve")
             .expect("managed route should exist");
@@ -553,7 +590,7 @@ mod tests {
             .await
             .expect("route should persist");
 
-        let first = resolve_managed_cluster_route(&store)
+        let first = resolve_route_by_name(&store, CLUSTER_INFERENCE_ROUTE_NAME)
             .await
             .expect("route should resolve")
             .expect("managed route should exist");
@@ -572,10 +609,154 @@ mod tests {
             .await
             .expect("provider rotation should persist");
 
-        let second = resolve_managed_cluster_route(&store)
+        let second = resolve_route_by_name(&store, CLUSTER_INFERENCE_ROUTE_NAME)
             .await
             .expect("route should resolve")
             .expect("managed route should exist");
         assert_eq!(second.api_key, "sk-rotated");
     }
+
+    #[tokio::test]
+    async fn upsert_system_route_creates_with_correct_name() {
+        let store = Store::connect("sqlite::memory:?cache=shared")
+            .await
+            .expect("store");
+
+        let provider = make_provider("anthropic-dev", "anthropic", "ANTHROPIC_API_KEY", "sk-ant");
+        store.put_message(&provider).await.expect("persist");
+
+        let route = upsert_cluster_inference_route(
+            &store,
+            SANDBOX_SYSTEM_ROUTE_NAME,
+            "anthropic-dev",
+            "claude-sonnet-4-20250514",
+        )
+        .await
+        .expect("should succeed");
+
+        assert_eq!(route.name, SANDBOX_SYSTEM_ROUTE_NAME);
+        assert_eq!(route.version, 1);
+        let config = route.config.as_ref().expect("config");
+        assert_eq!(config.provider_name, "anthropic-dev");
+        assert_eq!(config.model_id, "claude-sonnet-4-20250514");
+    }
+
+    #[tokio::test]
+    async fn bundle_includes_both_user_and_system_routes() {
+        let store = Store::connect("sqlite::memory:?cache=shared")
+            .await
+            .expect("store");
+
+        let openai = make_provider("openai-dev", "openai", "OPENAI_API_KEY", "sk-oai");
+        store.put_message(&openai).await.expect("persist openai");
+        let anthropic = make_provider("anthropic-dev", "anthropic", "ANTHROPIC_API_KEY", "sk-ant");
+        store
+            .put_message(&anthropic)
+            .await
+            .expect("persist anthropic");
+
+        let user_route = make_route(CLUSTER_INFERENCE_ROUTE_NAME, "openai-dev", "gpt-4o");
+        store
+            .put_message(&user_route)
+            .await
+            .expect("persist user route");
+        let system_route = make_route(
+            SANDBOX_SYSTEM_ROUTE_NAME,
+            "anthropic-dev",
+            "claude-sonnet-4-20250514",
+        );
+        store
+            .put_message(&system_route)
+            .await
+            .expect("persist system route");
+
+        let resp = resolve_inference_bundle(&store)
+            .await
+            .expect("bundle should resolve");
+
+        assert_eq!(resp.routes.len(), 2);
+        assert_eq!(resp.routes[0].name, CLUSTER_INFERENCE_ROUTE_NAME);
+        assert_eq!(resp.routes[0].model_id, "gpt-4o");
+        assert_eq!(resp.routes[1].name, SANDBOX_SYSTEM_ROUTE_NAME);
+        assert_eq!(resp.routes[1].model_id, "claude-sonnet-4-20250514");
+    }
+
+    #[tokio::test]
+    async fn bundle_with_only_system_route() {
+        let store = Store::connect("sqlite::memory:?cache=shared")
+            .await
+            .expect("store");
+
+        let provider = make_provider("openai-dev", "openai", "OPENAI_API_KEY", "sk-test");
+        store.put_message(&provider).await.expect("persist");
+        let system_route = make_route(SANDBOX_SYSTEM_ROUTE_NAME, "openai-dev", "gpt-4o-mini");
+        store.put_message(&system_route).await.expect("persist");
+
+        let resp = resolve_inference_bundle(&store)
+            .await
+            .expect("bundle should resolve");
+
+        assert_eq!(resp.routes.len(), 1);
+        assert_eq!(resp.routes[0].name, SANDBOX_SYSTEM_ROUTE_NAME);
+        assert_eq!(resp.routes[0].model_id, "gpt-4o-mini");
+    }
+
+    #[tokio::test]
+    async fn get_returns_system_route_when_requested() {
+        let store = Store::connect("sqlite::memory:?cache=shared")
+            .await
+            .expect("store");
+
+        let provider = make_provider("openai-dev", "openai", "OPENAI_API_KEY", "sk-test");
+        store.put_message(&provider).await.expect("persist");
+
+        upsert_cluster_inference_route(
+            &store,
+            SANDBOX_SYSTEM_ROUTE_NAME,
+            "openai-dev",
+            "gpt-4o-mini",
+        )
+        .await
+        .expect("upsert should succeed");
+
+        let route = store
+            .get_message_by_name::<InferenceRoute>(SANDBOX_SYSTEM_ROUTE_NAME)
+            .await
+            .expect("fetch should succeed")
+            .expect("route should exist");
+
+        assert_eq!(route.name, SANDBOX_SYSTEM_ROUTE_NAME);
+        let config = route.config.as_ref().expect("config");
+        assert_eq!(config.model_id, "gpt-4o-mini");
+    }
+
+    #[test]
+    fn effective_route_name_defaults_empty_to_inference_local() {
+        assert_eq!(
+            effective_route_name("").unwrap(),
+            CLUSTER_INFERENCE_ROUTE_NAME
+        );
+        assert_eq!(
+            effective_route_name("  ").unwrap(),
+            CLUSTER_INFERENCE_ROUTE_NAME
+        );
+        assert_eq!(
+            effective_route_name("inference.local").unwrap(),
+            CLUSTER_INFERENCE_ROUTE_NAME
+        );
+    }
+
+    #[test]
+    fn effective_route_name_accepts_sandbox_system() {
+        assert_eq!(
+            effective_route_name("sandbox-system").unwrap(),
+            SANDBOX_SYSTEM_ROUTE_NAME
+        );
+    }
+
+    #[test]
+    fn effective_route_name_rejects_unknown_name() {
+        let err = effective_route_name("unknown-route").unwrap_err();
+        assert_eq!(err.code(), tonic::Code::InvalidArgument);
+    }
 }
diff --git a/proto/inference.proto b/proto/inference.proto
index 7f48abbfb..e35b3eb32 100644
--- a/proto/inference.proto
+++ b/proto/inference.proto
@@ -41,7 +41,8 @@ message ClusterInferenceConfig {
 message InferenceRoute {
   string id = 1;
   ClusterInferenceConfig config = 2;
-  // Object name (always "inference.local" for the managed route).
+  // Object name ("inference.local" for the user-facing route,
+  // "sandbox-system" for the sandbox system-level route).
   string name = 3;
   // Monotonic version incremented on every update.
   uint64 version = 4;
@@ -52,20 +53,31 @@ message SetClusterInferenceRequest {
   string provider_name = 1;
   // Model identifier to force on generation calls.
   string model_id = 2;
+  // Route name to target. Empty string defaults to "inference.local" (user-facing).
+  // Use "sandbox-system" for the sandbox system-level inference route.
+  string route_name = 3;
 }
 
 message SetClusterInferenceResponse {
   string provider_name = 1;
   string model_id = 2;
   uint64 version = 3;
+  // Route name that was configured.
+  string route_name = 4;
 }
 
-message GetClusterInferenceRequest {}
+message GetClusterInferenceRequest {
+  // Route name to query. Empty string defaults to "inference.local" (user-facing).
+  // Use "sandbox-system" for the sandbox system-level inference route.
+  string route_name = 1;
+}
 
 message GetClusterInferenceResponse {
   string provider_name = 1;
   string model_id = 2;
   uint64 version = 3;
+  // Route name that was queried.
+  string route_name = 4;
 }
 
 message GetInferenceBundleRequest {}
