diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index d01179095..cae477618 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -14,7 +14,6 @@ use openshell_core::{driver_mounts, proto_struct}; use serde::Serialize; use serde_json::Value; use std::collections::{BTreeMap, HashSet}; -#[cfg(target_os = "linux")] use std::path::Path; /// Returns `true` when `SELinux` is enabled (enforcing or permissive). diff --git a/crates/openshell-supervisor-middleware/src/lib.rs b/crates/openshell-supervisor-middleware/src/lib.rs index d9972223e..5351bbdd9 100644 --- a/crates/openshell-supervisor-middleware/src/lib.rs +++ b/crates/openshell-supervisor-middleware/src/lib.rs @@ -437,6 +437,7 @@ pub enum TransformedBodyPolicy<'a> { pub struct HttpRequestInput { pub request_id: String, pub sandbox_id: String, + pub sandbox_name: String, pub scheme: String, pub host: String, pub port: u16, @@ -1581,6 +1582,7 @@ impl ChainRunner { let HttpRequestInput { request_id, sandbox_id, + sandbox_name, scheme, host, port, @@ -1597,6 +1599,7 @@ impl ChainRunner { let context = RequestContext { request_id, sandbox_id, + sandbox_name, originating_process: None, }; let target = HttpRequestTarget { @@ -2047,7 +2050,8 @@ mod tests { fn input(body: &str) -> HttpRequestInput { HttpRequestInput { request_id: "req".into(), - sandbox_id: "sbx".into(), + sandbox_id: "sbx-id".into(), + sandbox_name: "sbx-name".into(), scheme: "https".into(), host: "api.example.com".into(), port: 443, @@ -3215,7 +3219,8 @@ mod tests { assert_eq!(received[0].config.as_ref(), Some(&evaluation_config)); let context = received[0].context.as_ref().expect("request context"); assert_eq!(context.request_id, "req"); - assert_eq!(context.sandbox_id, "sbx"); + assert_eq!(context.sandbox_id, "sbx-id"); + assert_eq!(context.sandbox_name, "sbx-name"); assert!(context.originating_process.is_none()); let target = received[0].target.as_ref().expect("request target"); assert_eq!(target.scheme, "https"); @@ -4678,6 +4683,7 @@ mod tests { session_id: session_id.into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, @@ -5111,6 +5117,7 @@ mod tests { session_id: "builtin-regex-session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, @@ -5177,6 +5184,7 @@ mod tests { session_id: "builtin-regex-gap-session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, @@ -5239,6 +5247,7 @@ mod tests { session_id: "in-process-session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, @@ -5310,6 +5319,7 @@ mod tests { session_id: "ws-session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, @@ -5394,6 +5404,7 @@ mod tests { session_id: "ws-session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "ws".into(), host: "api.openai.com".into(), port: 80, @@ -5689,6 +5700,7 @@ mod tests { session_id: "session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, diff --git a/crates/openshell-supervisor-middleware/src/websocket.rs b/crates/openshell-supervisor-middleware/src/websocket.rs index cea7addb0..48891dd3f 100644 --- a/crates/openshell-supervisor-middleware/src/websocket.rs +++ b/crates/openshell-supervisor-middleware/src/websocket.rs @@ -38,6 +38,7 @@ pub struct WebSocketPreflightInput { pub session_id: String, pub request_id: String, pub sandbox_id: String, + pub sandbox_name: String, pub scheme: String, pub host: String, pub port: u16, @@ -920,6 +921,7 @@ async fn open_stage(entry: DescribedChainEntry, input: WebSocketPreflightInput) context: Some(RequestContext { request_id: input.request_id, sandbox_id: input.sandbox_id, + sandbox_name: input.sandbox_name, originating_process: None, }), target: Some(HttpRequestTarget { diff --git a/crates/openshell-supervisor-network/src/l7/middleware.rs b/crates/openshell-supervisor-network/src/l7/middleware.rs index b8c863f0c..56f518fbf 100644 --- a/crates/openshell-supervisor-network/src/l7/middleware.rs +++ b/crates/openshell-supervisor-network/src/l7/middleware.rs @@ -480,6 +480,7 @@ pub async fn apply_middleware_chain_for_scheme openshell_supervisor_middleware::HttpRequestInput { openshell_supervisor_middleware::HttpRequestInput { request_id: uuid::Uuid::new_v4().to_string(), - sandbox_id: openshell_ocsf::ctx::ctx().sandbox_id.clone(), + sandbox_id: sandbox.sandbox_id.clone(), + sandbox_name: sandbox.sandbox_name.clone(), scheme: scheme.into(), host: ctx.host.clone(), port: ctx.port, @@ -1079,6 +1084,51 @@ mod tests { assert!(!body.to_string().contains("secret-value")); } + #[test] + fn middleware_input_carries_real_sandbox_name() { + let sandbox = openshell_ocsf::SandboxContext { + sandbox_id: "sbx-123".into(), + sandbox_name: "nightly-build".into(), + container_image: String::new(), + hostname: "h".into(), + product_version: "0".into(), + proxy_ip: [127, 0, 0, 1].into(), + proxy_port: 3128, + }; + + let eval = L7EvalContext { + host: "api.example.test".into(), + port: 443, + policy_name: "api-policy".into(), + binary_path: "/usr/bin/curl".into(), + ancestors: Vec::new(), + cmdline_paths: Vec::new(), + secret_resolver: None, + ..Default::default() + }; + let req = crate::l7::provider::L7Request { + action: "POST".into(), + target: "/v1/messages".into(), + query_params: std::collections::HashMap::new(), + raw_header: Vec::new(), + body_length: crate::l7::provider::BodyLength::None, + }; + + let input = super::middleware_request_input( + &sandbox, + "https", + &req, + &eval, + Vec::new(), + Vec::new(), + String::new(), + Vec::new(), + ); + + assert_eq!(input.sandbox_name, "nightly-build"); + assert_eq!(input.sandbox_id, "sbx-123"); + } + #[tokio::test] async fn middleware_failure_uses_platform_response_without_policy_guidance() { let ctx = L7EvalContext { diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 6bd0a9847..46a4b370b 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -990,6 +990,7 @@ pub(crate) async fn websocket_middleware_preflight( session_id: uuid::Uuid::new_v4().to_string(), request_id: uuid::Uuid::new_v4().to_string(), sandbox_id: openshell_ocsf::ctx::ctx().sandbox_id.clone(), + sandbox_name: openshell_ocsf::ctx::ctx().sandbox_name.clone(), scheme: scheme.to_string(), host: ctx.host.clone(), port: ctx.port, @@ -5870,6 +5871,7 @@ network_policies: }; let input = middleware_request_input( + openshell_ocsf::ctx::ctx(), "http", &req, &ctx, diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 7ba286b10..bdb54a9bc 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -3598,6 +3598,7 @@ network_policies: session_id: "session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: scheme.into(), host: "api.openai.com".into(), port: if scheme == "wss" { 443 } else { 80 }, @@ -4482,6 +4483,7 @@ network_policies: session_id: "session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, @@ -4629,6 +4631,7 @@ network_policies: session_id: "disabled-session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, @@ -4748,6 +4751,7 @@ network_policies: session_id: "builtin-regex-session".into(), request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 6dd92b40d..f3278d60f 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -7123,6 +7123,7 @@ network_policies: session_id, request_id: "request".into(), sandbox_id: "sandbox".into(), + sandbox_name: "sandbox-name".into(), scheme: "wss".into(), host: "api.openai.com".into(), port: 443, diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 42beedeff..db529a147 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -41,6 +41,8 @@ If post-transformation policy evaluation itself fails, OpenShell denies the requ Middleware receives the request before credential injection. Operator-run services cannot inspect OpenShell-managed credentials. Middleware-visible request headers are delivered in wire order and repeated header names are preserved as separate entries. OpenShell filters credential, routing, framing, and hop-by-hop headers before invoking middleware. It rejects malformed request headers and unsupported transfer-coding sequences before middleware or policy dispatch. Headers named by a request's `Connection` field are omitted from middleware input and removed before forwarding, except for the validated WebSocket upgrade pair. +The request context identifies the originating sandbox to operator-run services. It carries the sandbox ID (`sandbox_id`) and the sandbox name (`sandbox_name`), letting audit and approval interfaces show a human-readable name instead of an opaque ID. The name is best-effort: a supervisor that cannot resolve it, or an older supervisor that predates the field, sends an empty string. Services should fall back to the sandbox ID when the name is empty. + ## Choose a Middleware Type | Type | Registration | Payload limit | Deployment | diff --git a/proto/supervisor_middleware.proto b/proto/supervisor_middleware.proto index 2c7c63c11..61ea44247 100644 --- a/proto/supervisor_middleware.proto +++ b/proto/supervisor_middleware.proto @@ -280,6 +280,8 @@ message RequestContext { string sandbox_id = 2; // Workload process that originated the request, when available. Process originating_process = 3; + // Sandbox name that originated the request. + string sandbox_name = 4; } // HttpRequestTarget describes the admitted HTTP destination and request target.