From 1a65cdc9fa025961685acd167aa5f787734c3861 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 9 Oct 2026 08:36:31 -0700 Subject: [PATCH] feat(isolation): publish backend protocol contracts independently of runtime Signed-off-by: Drew Newberry --- crates/openshell-core/README.md | 7 + .../openshell-core/src/extension_protocol.rs | 4 + .../src/isolation_registration.rs | 209 ++++++++++ crates/openshell-core/src/lib.rs | 1 + crates/openshell-core/src/proto/mod.rs | 9 + crates/openshell-driver-docker/src/tests.rs | 1 + .../src/boundary_protocol.rs | 50 ++- crates/openshell-sandbox-backend/src/lib.rs | 2 +- crates/openshell-server/src/compute/mod.rs | 5 + crates/openshell-server/src/storage_proto.rs | 38 +- proto/README.md | 8 + proto/compute_driver.proto | 36 ++ proto/isolation_backend.proto | 126 ++++++ proto/sandbox_protocol.proto | 26 ++ proto/sandbox_protocol/v1/README.md | 375 ++++++++++++++++++ proto/sandbox_protocol/v1/vectors.json | 282 +++++++++++++ 16 files changed, 1158 insertions(+), 21 deletions(-) create mode 100644 crates/openshell-core/src/isolation_registration.rs create mode 100644 proto/isolation_backend.proto create mode 100644 proto/sandbox_protocol.proto create mode 100644 proto/sandbox_protocol/v1/README.md create mode 100644 proto/sandbox_protocol/v1/vectors.json diff --git a/crates/openshell-core/README.md b/crates/openshell-core/README.md index b4cdb05f5..1cf1b7e70 100644 --- a/crates/openshell-core/README.md +++ b/crates/openshell-core/README.md @@ -3,6 +3,13 @@ Shared types, constants, configuration, and helpers used across OpenShell crates. +## Isolation registrations + +`isolation_registration` owns the operator registry's backend names, byte-stream +endpoints, and shared validation. Its fallible conversions carry registrations +through the compute-driver protobuf and back into the supervisor's JSON launch +file. These routes contain no credentials or driver-owned launch coordinates. + ## Object Metadata Top-level user-facing objects use a Kubernetes-style metadata convention. The diff --git a/crates/openshell-core/src/extension_protocol.rs b/crates/openshell-core/src/extension_protocol.rs index 9c25c6cb8..c66a1efad 100644 --- a/crates/openshell-core/src/extension_protocol.rs +++ b/crates/openshell-core/src/extension_protocol.rs @@ -19,6 +19,10 @@ pub const PROTOCOL_MINOR: u32 = 0; /// configured signer before accepting sandbox creation. pub const COMPUTE_LAUNCH_AUTHENTICATION: &str = "openshell.compute.launch-authentication"; +/// Driver stages operator-selected external isolation routes for the supervisor. +pub const COMPUTE_ISOLATION_BACKEND_REGISTRATION: &str = + "openshell.compute.isolation-backend-registration"; + const MAX_IMPLEMENTATION_NAME_BYTES: usize = 128; const MAX_IMPLEMENTATION_VERSION_BYTES: usize = 128; const MAX_CAPABILITY_BYTES: usize = 128; diff --git a/crates/openshell-core/src/isolation_registration.rs b/crates/openshell-core/src/isolation_registration.rs new file mode 100644 index 000000000..1633dbb74 --- /dev/null +++ b/crates/openshell-core/src/isolation_registration.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Operator-owned routes for external isolation backends. + +use std::collections::BTreeSet; +use std::net::SocketAddr; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +/// Reserved selection for drivers that launch the built-in sandbox runtime. +pub const BUILTIN_BACKEND_NAME: &str = "openshell-sandbox"; + +/// Trusted byte-stream endpoint. Generation-specific TLS is supplied by the driver. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub enum SandboxTransport { + Unix { + socket_path: PathBuf, + }, + Tcp { + /// Logical HTTP authority used for routing. + authority: String, + /// Concrete connection candidates pinned by the operator or driver. + addresses: Vec, + }, + Vsock { + guest_cid: u32, + port: u32, + }, +} + +impl SandboxTransport { + pub fn validate(&self) -> Result<(), &'static str> { + let valid = match self { + Self::Unix { socket_path } => socket_path.is_absolute(), + Self::Tcp { + authority, + addresses, + } => { + !authority.is_empty() + && !addresses.is_empty() + && addresses + .iter() + .all(|address| address.port() != 0 && !address.ip().is_unspecified()) + } + Self::Vsock { guest_cid, port } => { + *guest_cid >= 3 && *guest_cid != u32::MAX && *port != 0 && *port != u32::MAX + } + }; + if valid { + Ok(()) + } else { + Err("invalid boundary control transport endpoint") + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BackendRegistration { + pub name: String, + pub endpoint: SandboxTransport, +} + +pub fn validate_backend_name(name: &str) -> Result<(), &'static str> { + if name == BUILTIN_BACKEND_NAME + || name.is_empty() + || name.len() > 128 + || !name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + { + return Err("invalid or reserved delegated backend name"); + } + Ok(()) +} + +/// Validate every entry, including unused registrations. +pub fn validate_registrations(registrations: &[BackendRegistration]) -> Result<(), &'static str> { + let mut names = BTreeSet::new(); + for registration in registrations { + validate_backend_name(®istration.name)?; + if !names.insert(®istration.name) { + return Err("duplicate isolation backend registration"); + } + registration.endpoint.validate()?; + } + Ok(()) +} + +impl TryFrom<&BackendRegistration> for crate::proto::compute::v1::IsolationBackendRegistration { + type Error = &'static str; + + fn try_from(registration: &BackendRegistration) -> Result { + use crate::proto::compute::v1::{ + IsolationBackendTcpEndpoint, IsolationBackendVsockEndpoint, + isolation_backend_registration::Endpoint, + }; + validate_backend_name(®istration.name)?; + registration.endpoint.validate()?; + let endpoint = match ®istration.endpoint { + SandboxTransport::Unix { socket_path } => Endpoint::UnixSocketPath( + socket_path + .to_str() + .ok_or("isolation backend Unix path must be UTF-8")? + .into(), + ), + SandboxTransport::Tcp { + authority, + addresses, + } => Endpoint::Tcp(IsolationBackendTcpEndpoint { + authority: authority.clone(), + addresses: addresses.iter().map(ToString::to_string).collect(), + }), + SandboxTransport::Vsock { guest_cid, port } => { + Endpoint::Vsock(IsolationBackendVsockEndpoint { + guest_cid: *guest_cid, + port: *port, + }) + } + }; + Ok(Self { + name: registration.name.clone(), + endpoint: Some(endpoint), + }) + } +} + +impl TryFrom<&crate::proto::compute::v1::IsolationBackendRegistration> for BackendRegistration { + type Error = &'static str; + + fn try_from( + registration: &crate::proto::compute::v1::IsolationBackendRegistration, + ) -> Result { + use crate::proto::compute::v1::isolation_backend_registration::Endpoint; + let endpoint = match registration + .endpoint + .as_ref() + .ok_or("isolation backend endpoint is missing")? + { + Endpoint::UnixSocketPath(path) => SandboxTransport::Unix { + socket_path: path.into(), + }, + Endpoint::Tcp(tcp) => SandboxTransport::Tcp { + authority: tcp.authority.clone(), + addresses: tcp + .addresses + .iter() + .map(|address| { + address + .parse() + .map_err(|_| "invalid isolation backend TCP address") + }) + .collect::>()?, + }, + Endpoint::Vsock(vsock) => SandboxTransport::Vsock { + guest_cid: vsock.guest_cid, + port: vsock.port, + }, + }; + validate_backend_name(®istration.name)?; + endpoint.validate()?; + Ok(Self { + name: registration.name.clone(), + endpoint, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::proto::compute::v1::IsolationBackendRegistration; + + #[test] + fn operator_routes_survive_compute_protocol_and_supervisor_json() { + for endpoint in [ + SandboxTransport::Unix { + socket_path: "/run/backend.sock".into(), + }, + SandboxTransport::Tcp { + authority: "backend.example:443".into(), + addresses: vec![ + "127.0.0.1:47000".parse().unwrap(), + "[::1]:47000".parse().unwrap(), + ], + }, + SandboxTransport::Vsock { + guest_cid: 3, + port: 47000, + }, + ] { + let original = BackendRegistration { + name: "agent-substrate".into(), + endpoint, + }; + let wire = IsolationBackendRegistration::try_from(&original).unwrap(); + let decoded = BackendRegistration::try_from(&wire).unwrap(); + assert_eq!(decoded, original); + let json = serde_json::to_string(&decoded).unwrap(); + assert_eq!( + serde_json::from_str::(&json).unwrap(), + original + ); + } + } +} diff --git a/crates/openshell-core/src/lib.rs b/crates/openshell-core/src/lib.rs index 1cbf6968e..cd6ec14a7 100644 --- a/crates/openshell-core/src/lib.rs +++ b/crates/openshell-core/src/lib.rs @@ -32,6 +32,7 @@ pub mod gpu; pub mod grpc_client; pub mod host_pattern; pub mod image; +pub mod isolation_registration; pub mod jwt; pub mod local_api_socket; pub mod mcp; diff --git a/crates/openshell-core/src/proto/mod.rs b/crates/openshell-core/src/proto/mod.rs index c6076620a..f17126e8e 100644 --- a/crates/openshell-core/src/proto/mod.rs +++ b/crates/openshell-core/src/proto/mod.rs @@ -34,6 +34,9 @@ pub mod datamodel { pub mod sandbox { pub use super::generated::openshell::sandbox::v1; + pub mod protocol { + pub use super::super::generated::openshell::sandbox::protocol::v1; + } } pub mod compute { @@ -44,6 +47,12 @@ pub mod extension { pub use super::generated::openshell::extension::v1; } +pub mod isolation { + pub mod backend { + pub use super::super::generated::openshell::isolation::backend::v1; + } +} + #[allow( clippy::all, clippy::pedantic, diff --git a/crates/openshell-driver-docker/src/tests.rs b/crates/openshell-driver-docker/src/tests.rs index 3d8968f2d..bd9a2ccf6 100644 --- a/crates/openshell-driver-docker/src/tests.rs +++ b/crates/openshell-driver-docker/src/tests.rs @@ -100,6 +100,7 @@ fn test_sandbox() -> DriverSandbox { await_main_process_attachment: false, workload_identity: None, launch_authentication: test_launch_authentication(), + isolation_backend: None, }), status: None, workspace: String::new(), diff --git a/crates/openshell-sandbox-backend/src/boundary_protocol.rs b/crates/openshell-sandbox-backend/src/boundary_protocol.rs index 70d491d31..22cab052e 100644 --- a/crates/openshell-sandbox-backend/src/boundary_protocol.rs +++ b/crates/openshell-sandbox-backend/src/boundary_protocol.rs @@ -276,25 +276,7 @@ impl<'de> Deserialize<'de> for SupervisorInstanceId { #[error("supervisor instance ID must be a non-nil canonical lowercase UUID")] pub struct SupervisorInstanceIdError; -/// Driver-selected byte-stream transport for the `OpenShell` Sandbox Protocol. -/// Authentication is configured separately and is identical for every variant. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] -pub enum SandboxTransport { - Unix { - socket_path: PathBuf, - }, - Tcp { - /// Stable logical Kubernetes Service authority used for diagnostics. - authority: String, - /// Explicit connection candidates resolved by the compute driver. - addresses: Vec, - }, - Vsock { - guest_cid: u32, - port: u32, - }, -} +pub use openshell_core::isolation_registration::SandboxTransport; /// Supervisor-side, generation-pinned TLS server authentication. #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -1461,6 +1443,36 @@ pub enum FrameError { mod tests { use super::*; + #[test] + fn published_protocol_vectors_match_typed_messages_digest_and_framing() { + use sha2::Digest as _; + let vectors: serde_json::Value = serde_json::from_str(include_str!( + "../../../proto/sandbox_protocol/v1/vectors.json" + )) + .unwrap(); + assert_eq!(vectors["version"], "1.0"); + for vector in vectors["requests"].as_array().unwrap() { + let envelope: RequestEnvelope = + serde_json::from_value(vector["envelope"].clone()).unwrap(); + envelope.validate_payload_digest().unwrap(); + assert_eq!(serde_json::to_value(&envelope).unwrap(), vector["envelope"]); + let canonical = vector["canonical_request"].as_str().unwrap(); + assert_eq!( + format!("{:x}", Sha256::digest(canonical.as_bytes())), + envelope.payload_digest + ); + let hex = vector["frame_hex"].as_str().unwrap(); + let frame: Vec = (0..hex.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).unwrap()) + .collect(); + let decoded: RequestEnvelope = decode_frame(&frame).unwrap(); + assert_eq!(decoded, envelope); + let decoded: RequestEnvelope = decode_frame(&encode_frame(&decoded).unwrap()).unwrap(); + assert_eq!(decoded, envelope); + } + } + fn complete_audit_evidence() -> NativeLinuxSandboxAuditEvidence { NativeLinuxSandboxAuditEvidence { capabilities: CapabilityEvidence { diff --git a/crates/openshell-sandbox-backend/src/lib.rs b/crates/openshell-sandbox-backend/src/lib.rs index 87b25dccd..6baa4bf5a 100644 --- a/crates/openshell-sandbox-backend/src/lib.rs +++ b/crates/openshell-sandbox-backend/src/lib.rs @@ -16,7 +16,7 @@ pub mod sandbox_auth; pub use runtime::OpenShellRuntimeBackend; /// Stable isolation backend name implemented by `openshell-sandbox`. -pub const BACKEND_NAME: &str = "openshell-sandbox"; +pub const BACKEND_NAME: &str = openshell_core::isolation_registration::BUILTIN_BACKEND_NAME; /// Resource claim set by compute drivers when the workload requests GPU access. pub const GPU_RESOURCE_CLAIM: &str = "openshell.gpu"; diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 51b2c5e6e..3059f1ff2 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -1958,6 +1958,7 @@ impl ComputeRuntime { move |driver| async move { driver .start_sandbox(Request::new(StartSandboxRequest { + isolation_backend: None, sandbox_id: request_id, name: sandbox_name, launch_authentication, @@ -3363,6 +3364,7 @@ impl ComputeRuntime { async move { driver .start_sandbox(Request::new(StartSandboxRequest { + isolation_backend: None, sandbox_id, name: sandbox_name, launch_authentication, @@ -3639,6 +3641,7 @@ impl ComputeRuntime { |driver| async move { driver .start_sandbox(Request::new(StartSandboxRequest { + isolation_backend: None, sandbox_id: driver_sandbox_id, name: sandbox_name, launch_authentication: Vec::new(), @@ -4140,6 +4143,7 @@ impl ComputeRuntime { move |driver| async move { driver .start_sandbox(Request::new(StartSandboxRequest { + isolation_backend: None, sandbox_id: request_id, name: operation_name, launch_authentication, @@ -6007,6 +6011,7 @@ fn driver_sandbox_spec_from_public( driver_name: &str, ) -> Result> { Ok(DriverSandboxSpec { + isolation_backend: None, log_level: spec.log_level.clone(), environment: spec.environment.clone(), template: spec diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 493fab2b1..ff27f3a95 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -513,11 +513,31 @@ mod tests { let mut methods = Vec::new(); let mut public_roots = BTreeSet::new(); let mut compiled_method_count = 0; + let mut isolation_methods = BTreeSet::new(); + let mut sandbox_protocol_methods = BTreeSet::new(); for file in &public.file { let package = file.package.as_deref().unwrap_or_default(); for service in &file.service { let service_name = service.name.as_deref().expect("service name"); compiled_method_count += service.method.len(); + if package == "openshell.isolation.backend.v1" { + assert_eq!(service_name, "IsolationBackend"); + isolation_methods.extend( + service + .method + .iter() + .map(|method| method.name.as_deref().expect("isolation method name")), + ); + } + if package == "openshell.sandbox.protocol.v1" { + assert_eq!(service_name, "DelegatedIsolationBoundary"); + sandbox_protocol_methods.extend(service.method.iter().map(|method| { + method + .name + .as_deref() + .expect("sandbox protocol method name") + })); + } if !matches!( (package, service_name), ("openshell.v1", "OpenShell") | ("openshell.inference.v1", "Inference") @@ -541,6 +561,19 @@ mod tests { } } methods.sort(); + assert_eq!( + isolation_methods, + BTreeSet::from([ + "GetCapabilities", + "OpenBoundary", + "ResumeBoundary", + "SuspendBoundary" + ]) + ); + assert_eq!( + sandbox_protocol_methods, + BTreeSet::from(["Exchange", "Mediate"]) + ); for signature in PROVIDER_READINESS_RPC_SIGNATURES { assert!( methods.iter().any(|method| method == signature), @@ -555,7 +588,10 @@ mod tests { } assert_eq!( compiled_method_count, - 102 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), + 102 + PROVIDER_READINESS_RPC_SIGNATURES.len() + + PEER_OWNER_RPC_SIGNATURES.len() + + isolation_methods.len() + + sandbox_protocol_methods.len(), "classify every compiled RPC" ); assert_eq!( diff --git a/proto/README.md b/proto/README.md index 725fb2112..d08bc6518 100644 --- a/proto/README.md +++ b/proto/README.md @@ -85,3 +85,11 @@ identifies the service being exposed. - Regenerate Rust, Python, Go, and TypeScript bindings after contract changes. Run `mise run pre-commit`, the affected SDK checks, and relevant server tests before submitting the change. + +## Isolation backend protocol + +[`isolation_backend.proto`](isolation_backend.proto) and +[`sandbox_protocol.proto`](sandbox_protocol.proto) are supervisor-to-backend +contracts, not gateway RPCs. Backend authors can generate bindings independently +and implement [Sandbox Protocol 1.0](sandbox_protocol/v1/README.md). Its negotiated +version covers the gRPC services, JSON messages, framing, and lifecycle semantics. diff --git a/proto/compute_driver.proto b/proto/compute_driver.proto index f278174b5..5de0dc248 100644 --- a/proto/compute_driver.proto +++ b/proto/compute_driver.proto @@ -208,6 +208,37 @@ message DriverSandboxSpec { // material between the host supervisor and workload-side sandbox runtime; // it must never expose the supervisor bearer tokens to the workload. bytes launch_authentication = 16 [(openshell.options.v1.secret) = true]; + // Operator-selected external isolation backend. Never derived from the + // public sandbox spec or workload environment. Omitted for the built-in + // runtime. Requires openshell.compute.isolation-backend-registration. + IsolationBackendRegistration isolation_backend = 17; +} + +// Trusted operator registration delivered to the compute driver on every +// launch. The driver stages this route outside workload mounts, admits this +// exact name, and supplies its opaque descriptor and generation-specific launch +// coordinates to the stock supervisor. It must reject unsupported selections +// before provisioning; it must never fall back to another isolation backend. +message IsolationBackendRegistration { + string name = 1; + oneof endpoint { + // Absolute path in the supervisor's filesystem namespace. + string unix_socket_path = 2; + IsolationBackendTcpEndpoint tcp = 3; + IsolationBackendVsockEndpoint vsock = 4; + } +} + +message IsolationBackendTcpEndpoint { + // Logical HTTP authority; generation-specific TLS identity is driver-owned. + string authority = 1; + // Operator-pinned socket addresses reachable from the supervisor. + repeated string addresses = 2; +} + +message IsolationBackendVsockEndpoint { + uint32 guest_cid = 1; + uint32 port = 2; } // Identity inputs admitted by the gateway before workload provisioning. @@ -424,6 +455,11 @@ message StartSandboxRequest { // stable resource represented by this identity while replacing only the // generation-specific runtime component. string expected_runtime_identity = 5; + // Same trusted selection as create; applies to this new generation. + // The gateway pins the backend name at creation and rejects start/recovery + // under a different selection. Operator endpoints may change without + // changing that identity; the driver still validates the retained resource. + IsolationBackendRegistration isolation_backend = 6; } message StartSandboxResponse { diff --git a/proto/isolation_backend.proto b/proto/isolation_backend.proto new file mode 100644 index 000000000..a8fd91007 --- /dev/null +++ b/proto/isolation_backend.proto @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.isolation.backend.v1; + +import "extension.proto"; +import "options.proto"; + +// Isolation Backend control contract. The supervisor uses this service on a +// trusted, authenticated endpoint selected by operator configuration or compute +// driver provisioning. Workload control, network mediation, and byte streams +// remain on the Sandbox Protocol. +// PeerMetadata negotiates the base contract and optional suspend/resume support. +// Version 1.0 requires openshell.isolation.contract on both peers and includes +// boundary routing. Optional state retention support advertises +// openshell.isolation.suspend-resume. Add optional capabilities +// with compatible minor versions; incompatible semantics require a major bump. +service IsolationBackend { + // Negotiate the isolation contract before opening a boundary. This call + // conveys no sandbox credentials or driver descriptor. + rpc GetCapabilities(GetIsolationCapabilitiesRequest) returns (GetIsolationCapabilitiesResponse); + + // Bind a driver-provisioned resource to one fresh sandbox launch. The + // caller credential must match sandbox_id and generation_id. This call must + // not run untrusted workload code. The subsequent Sandbox Protocol attach, + // confirm, and start sequence remains mandatory. A resource that already is + // the boundary may only validate and reserve its binding, then return a handle. + // Reopening the same session and generation returns the same result. The + // descriptor must bind to the same provisioned resource; a different resource + // binding fails. A route opened before attach never authorizes execution. + // Driver deletion invalidates all handles and retained-state references, + // including abandoned launches and suspended boundaries. + rpc OpenBoundary(OpenBoundaryRequest) returns (OpenBoundaryResponse); + + // Freeze the workload, retain its state, and fence the old generation before + // returning. The caller credential must match the active boundary. The + // compute driver owns storage and handoff of the reference bytes; the backend + // owns placement and retention of the state itself. Fenced means old-generation + // calls and streams are rejected and the old workload cannot make progress. + rpc SuspendBoundary(SuspendBoundaryRequest) returns (SuspendBoundaryResponse); + + // Restore a suspended boundary into a fresh launch session and generation. + // The restored workload remains frozen until attach, confirm, and start + // complete under the new supervisor: no untrusted workload code executes + // before start. A permitted cold start is reported. Unsupported optional + // suspend/resume RPCs return UNIMPLEMENTED without advancing lifecycle state. + rpc ResumeBoundary(ResumeBoundaryRequest) returns (OpenBoundaryResponse); +} + +message GetIsolationCapabilitiesRequest { + openshell.extension.v1.PeerMetadata supervisor = 1; +} + +message GetIsolationCapabilitiesResponse { + openshell.extension.v1.PeerMetadata backend = 1; +} + +message OpenBoundaryRequest { + reserved 6; + reserved "request_id"; + + // Stable gateway-assigned sandbox identity, not a public resource name. + string sandbox_id = 1; + // Fresh for each launch and unchanged for retries of that launch. + string session_id = 2; + // Fresh runtime generation bound to the supervisor's launch credential. + string generation_id = 3; + // Must match trusted admission and the configured endpoint registration. + string backend_name = 4; + // Opaque descriptor from the trusted compute driver. The backend validates + // its resource binding; workload-controlled input must not select it. + bytes driver_descriptor = 5 [(openshell.options.v1.secret) = true]; +} + +message OpenBoundaryResponse { + // Opaque handle scoped to this sandbox and runtime generation. The supervisor + // presents it when opening Sandbox Protocol streams. + string boundary_id = 1; + + enum LaunchOutcome { + LAUNCH_OUTCOME_UNSPECIFIED = 0; + LAUNCH_OUTCOME_FRESH = 1; + LAUNCH_OUTCOME_RESTORED = 2; + LAUNCH_OUTCOME_COLD_START_FALLBACK = 3; + } + + // Required. A restoration result remains frozen until the new supervisor + // authenticates, confirms enforcement, installs current policy and secrets, + // and explicitly starts the boundary. Fallback must be visible to callers. + LaunchOutcome outcome = 2; +} + +message SuspendBoundaryRequest { + reserved 4; + reserved "request_id"; + + // Retries for this session and generation return the same reference or a + // terminal error. + string boundary_id = 1; + string session_id = 2; + string generation_id = 3; +} + +message SuspendBoundaryResponse { + // Opaque protected state reference. It may be used only with an authorized + // ResumeBoundary request for the same sandbox and a fresh generation. + // A backend may invalidate superseded references; resuming one then fails + // terminally with FAILED_PRECONDITION rather than restoring a different state. + bytes restore_reference = 1 [(openshell.options.v1.secret) = true]; +} + +message ResumeBoundaryRequest { + // Fresh launch identity and trusted driver descriptor. Replays reuse the + // same launch request; the old session and generation are never reused. + OpenBoundaryRequest launch = 1; + // Reference returned by SuspendBoundary for this sandbox. It carries no + // authority without the new authenticated launch credential. + bytes restore_reference = 2 [(openshell.options.v1.secret) = true]; + // If false, fail when the memory image cannot be restored. If true, a clean + // boot from the image and persistent volumes is permitted and reported as + // LAUNCH_OUTCOME_COLD_START_FALLBACK. This never permits unauthorized or + // superseded references to select a new launch. + bool allow_cold_start_fallback = 3; +} diff --git a/proto/sandbox_protocol.proto b/proto/sandbox_protocol.proto new file mode 100644 index 000000000..95c8aa502 --- /dev/null +++ b/proto/sandbox_protocol.proto @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.sandbox.protocol.v1; + +// Stream entrypoint for per-boundary or shared out-of-process Isolation Backends. +// Each client stream starts with a boundary_id frame. The backend checks that +// handle against the authenticated sandbox and generation before accepting any +// data. Subsequent data fragments carry the existing Sandbox Protocol bytes. +// OpenBoundary returns the handle before policy discovery and lifecycle traffic. +service DelegatedIsolationBoundary { + rpc Exchange(stream DelegatedBoundaryChunk) returns (stream DelegatedBoundaryChunk); + rpc Mediate(stream DelegatedBoundaryChunk) returns (stream DelegatedBoundaryChunk); +} + +message DelegatedBoundaryChunk { + oneof payload { + // Nonempty handle required as the first client frame; forbidden on later frames. + string boundary_id = 1; + // Ordered fragment of the existing Sandbox Protocol. Response frames only + // use data. Empty data is allowed and does not bind a stream. + bytes data = 2; + } +} diff --git a/proto/sandbox_protocol/v1/README.md b/proto/sandbox_protocol/v1/README.md new file mode 100644 index 000000000..9be7e1ced --- /dev/null +++ b/proto/sandbox_protocol/v1/README.md @@ -0,0 +1,375 @@ + + +# OpenShell Sandbox Protocol 1.0 + +This is the wire contract for an isolation backend implemented outside the +OpenShell tree. An implementation may use any language and owns its native +isolation mechanisms. Generate gRPC bindings from +[`isolation_backend.proto`](../../isolation_backend.proto), +[`sandbox_protocol.proto`](../../sandbox_protocol.proto), and their imports. +No OpenShell guest executable or linked backend implementation is required. + +The negotiated isolation version covers **both** the control RPCs and the +framing and JSON messages below. Version 1.0 requires +`openshell.isolation.contract` on both peers. An incompatible change to a +message, framing, authentication, or lifecycle guarantee requires a major bump. +A minor revision may add optional capabilities; it must preserve all 1.0 +messages and behavior. A new operation requires an advertised capability +before a sender uses it. Adding fields to an existing 1.0 request requires a +major bump: additional request fields are not part of 1.0 and must not be sent. +Unknown required capabilities fail startup. This +contract is independently versioned from OpenShell releases. + +## Trusted registration and launch + +The gateway reads `isolation_backend` from the selected compute driver's +`[openshell.drivers.]` table and resolves it through +`openshell.supervisor.isolation_backends` in operator-owned `gateway.toml`. +Selecting `openshell.gateway.compute_driver` selects both the driver and its +backend; their names can differ. +It sends the resolved `IsolationBackendRegistration` through the compute +protocol in validation/create specs and every start request, including recovery. +This requires the driver's `openshell.compute.isolation-backend-registration` +capability; unsupported drivers fail gateway startup. Missing selection or +`openshell-sandbox` retains the built-in runtime. The public sandbox spec has +no selector or endpoint. + +The driver must reject any registered backend it cannot place, before creating +workload resources. It converts the typed endpoint to the JSON registration +below and supplies the exact admitted name. Rust drivers can use +`openshell_core::isolation_registration::BackendRegistration::try_from` and +serialize the result in a `{"backends":[...]}` wrapper. This compute launch +handoff does not change the supervisor-to-backend Protocol 1.0 contract. +The gateway pins the admitted backend name in internal sandbox metadata and +rejects start/recovery under a different selection. Legacy records without +that binding retain `openshell-sandbox`. Endpoint addresses are not persisted. + +These files and flags are supervisor CLI inputs versioned with the supervisor +release, independently of the negotiated wire protocol. Both file formats +reject unknown fields. + +The operator supplies `--isolation-backends-file` to `openshell-supervisor`: + +```json +{ + "backends": [ + { + "name": "agent-substrate", + "endpoint": { + "kind": "unix", + "socket_path": "/run/agent-substrate/isolation.sock" + } + } + ] +} +``` + +TCP endpoints use `{"kind":"tcp","authority":"backend.example:443", +"addresses":["192.0.2.10:443"]}`. Addresses are operator-pinned concrete socket +addresses; the supervisor does not resolve workload-provided hostnames. +Vsock endpoints use `{"kind":"vsock","guest_cid":3,"port":47000}`. +Vsock connects to a guest CID of at least 3; wildcard CIDs and ports are invalid. +Registration names contain 1–128 ASCII letters, digits, `-`, `_`, or `.`, and +match exactly. Duplicate names, malformed endpoints, unknown +names, and replacement of `openshell-sandbox` fail startup. + +The trusted compute driver or orchestrator supplies: + +- `OPENSHELL_ADMITTED_ISOLATION_BACKEND`: the independently admitted backend + name, never copied from sandbox environment or request input. +- `--backend-descriptor-file`: the driver's opaque native bytes. The supervisor + passes them unchanged as `OpenBoundaryRequest.driver_descriptor`. +- `--boundary-launch-file`: JSON with `backend_name`, `sandbox_id`, `generation`, `session_id`, + `workload_identity`, `tls`, `resource_claims`, `outer_fence`, and optional + `host_gateway_ip`. These are trusted launch coordinates, separate from the + native descriptor. Its backend name must match the admitted name. No endpoint + is accepted in this file. +- `--auth-bundle-file`: the existing gateway-issued `SupervisorAuthBundle`. + +`tls` contains `server_name` and `trust_anchor_pem`. The driver pins the serving +identity for this generation, including for Unix sockets and vsock. Both TLS +fields must be nonempty, and the trust anchor must parse as certificates. TCP +`authority` controls HTTP routing; `tls.server_name` controls SNI and certificate +verification. The backend derives launch identity, claims and fence from its +native descriptor or trusted driver state; it must independently verify them +before returning confirmation. + +Both JSON files and native descriptor bytes are protected from workload writes. +The +registration file belongs to the operator and must be delivered outside +workload mounts. In-tree drivers continue to select `openshell-sandbox`; an +external compute driver stages the files and launches the stock supervisor. +The public sandbox API has no endpoint or isolation-backend selector. + +## Authentication and ordering + +TLS 1.3 verifies the generation-pinned serving certificate and negotiates HTTP/2 +with ALPN `h2`. Every control RPC except `GetCapabilities`, and every stream, +uses `authorization: Bearer `. The backend validates the token's +signature with the trusted gateway verification keys and its expiry, audience, +gateway ID, sandbox ID, runtime generation, and authorization epoch. +TLS authenticates the backend; the bearer authenticates the supervisor. + +The JWT header uses `alg: "EdDSA"`, an operator-provisioned `kid`, and +`typ: "openshell-sandbox-session+jwt"`. Required claims are: +`iss: "openshell-gateway:"`, +`sub: "spiffe://openshell/sandbox/"`, `aud: "openshell-sandbox"`, +`iat: i64`, `exp: i64`, `jti: UUID`, `sandbox_id: string`, +`runtime_generation: string`, `auth_epoch: nonzero u64`, and +`component: "openshell-supervisor"`. Verify Ed25519 signatures against trusted +keys; never select a key or issuer supplied by the workload. Reject unknown +claims, wrong component/type/audience/issuer/subject, or a different sandbox, +generation, or admitted authorization epoch. Allow at most 30 seconds of clock +skew. For exp != 0, require iat < exp, a lifetime at most one hour, and an +unexpired token (with that skew). exp == 0 is the gateway's configured +nonexpiring profile; accept it only when the operator enables that profile on +the backend. Launch session binding is checked by OpenBoundary and +confirmation; it is not a JWT claim. + +The logical supervisor instance UUID is separate from the launch session. +Attach pins it for the generation. Reject another instance, and never replace +an active connection with another of the same epoch until the old connection +has closed. Confirmation promotes a staged authenticated connection. Retain +these ownership rules across transport recovery and credential renewal. + +On **each physical connection**, negotiate `PeerMetadata`, call authenticated +`OpenBoundary`, then use its nonempty opaque handle as the first client frame +on every `Exchange` or `Mediate` stream. Handles are at most 128 UTF-8 bytes. +Each control RPC message fits within 64 KiB of encoded protobuf, including the +opaque driver descriptor and launch coordinates in `OpenBoundary`. +After validating the binding, the server sends initial gRPC metadata before +waiting for the JSON request. The client waits for that metadata to complete +stream opening; delaying it until a response payload causes a deadlock. +All subsequent client frames and all response frames contain `data`. Empty data +is allowed; it never binds a stream. Protobuf message boundaries have no meaning +inside the concatenated data bytes. A handle without authentication or without +opening on the current connection has no authority. + +Opening only validates/reserves a route. Image policy discovery is read-only. +`attach` binds the admitted context; `confirm` establishes enforcement; +`start_agent` is the only operation that makes the admitted agent runnable. +No untrusted instruction may execute before those checks complete. Exec and +forwarding require `Running`. Reject out-of-order operations without advancing +state. Only the same logical supervisor may recover its transport in the same +generation. A replacement supervisor needs a newly authorized launch. + +A backend must validate native enforcement evidence before issuing confirmation. +The shared client verifies common properties and their immutable launch binding; +it does not interpret an external backend's `backend_audit`. The built-in backend +continues to validate Linux-specific evidence. An external backend remains +responsible for the RFC 0012 lifecycle and failure invariants, including bounded +termination when required enforcement disappears. Backend selection is a trust +boundary, not a mechanism for accepting untrusted attestations. + +## Control framing and replay + +Each logical stream begins with one request and its matching response: + +```text +u32 big-endian UTF-8 JSON length | JSON bytes +``` + +The maximum JSON payload is 1,048,576 bytes. Integers use their complete wire +range; avoid conversion through floating-point numbers. Byte arrays in JSON +are arrays of integers in 0..255, not base64. Paths, IP addresses, socket +addresses, UUIDs, and SHA-256 digests are strings. Socket addresses use +`IPv4:port` or `[IPv6]:port`. Digests are 64 lowercase hex characters. +Optional values are JSON `null` unless explicitly omitted below. Map keys are +strings. Fields are required unless described as optional or defaulted. + +Request envelope: + +```json +{ + "request_id": "00000000-0000-4000-8000-000000000001", + "payload_digest": "", + "request": { "operation": "discover_policy" } +} +``` + +`request_id` is a cryptographically random UUID scoped to the runtime generation. +`exec_expires_at_unix_ms` is omitted for non-exec requests; exec requires an +absolute admission deadline, normally 30 seconds after request creation. +Host and backend wall clocks must be synchronized; exec deadlines have no skew +allowance. The deadline limits exec +admission/recovery, not the lifetime of an admitted process. + +The sender emits all request fields, including nulls and defaults. To compute +`payload_digest`, canonicalize the `request` object as transmitted, without +dropping or inventing fields. For exec, add `exec_expires_at_unix_ms` to a copy +of that object for hashing only. Recursively sort object keys by UTF-8 byte +order (equivalently, Unicode code point order). Serialize compact UTF-8 JSON +without whitespace. Escape quote, backslash, backspace, form feed, newline, +carriage return and tab as `\"`, `\\`, `\b`, `\f`, `\n`, `\r`, `\t`; +escape other control characters below U+0020 as lowercase `\u00xx`. Do not +escape non-ASCII characters or `/`. Integers use base-10 digits without leading +zeros; floating point values are not used. Hash these bytes with SHA-256. +Object order in the transmitted message is immaterial. See +[`vectors.json`](vectors.json) for independently generated request byte/digest examples +checked by Rust and the independent Python fixture. Frame hex provides one +valid serialization; its object key order is not normative. + +The backend validates the digest and records replayable mutations by generation +and request ID. The same ID and digest returns the original result without +repeating effects; the same ID with a different digest fails. Replayable requests +are attach, confirm, start_agent, update_provider_environment, exec, signal, +terminate, terminate_boundary, exec_signal, and resize. Non-exec mutation results +remain replayable for the generation. Exec IDs remain admitted until their +absolute deadline, independently of process/output retention. Expired execs fail +even if their ID was discarded. An unknown outcome after transport loss does +not authorize launching another exec under a new ID. + +Responses use `{"request_id":"","response":{"result":"..."}}`. +The result may be followed by stream data as specified below. Structured errors +use `{"result":"error","kind":"invalid|denied|unavailable|terminated|process", +"message":"..."}` and must not include secrets. + +## Request and response messages + +`operation` selects the request; `result` selects the response. Fields listed +below belong to that object alongside its tag. Empty field lists mean the tag +alone. IDs are opaque generation-scoped strings, never local process IDs. + +| Operation | Request fields | Success result and fields | +|---|---|---| +| `discover_policy` | — | `image_policy`: `yaml: string\|null`, `invalid: bool` | +| `attach` | `supervisor_instance_id: UUID`, `policy: Policy`, `resource_claims: map` | `attached`: `snapshot: SessionSnapshot` | +| `confirm` | — | `confirmed`: `confirmation: Confirmation` | +| `probe_provider_files` | — | `provider_files_supported` | +| `start_agent` | `sandbox_id: string`, `spec: AgentSpec`, `policy: Policy`, `ca_cert: string\|null`, `ca_bundle: string\|null`, `provider_env_revision: u64`, `provider_env: map`, `provider_files: map` (default `{}`) | `started`: `process_id: string`, `provider_env_revision: u64`, `provider_env_generation: u64` | +| `update_provider_environment` | `generation: u64`, `revision: u64`, `provider_env: map`, `provider_files: map` (default `{}`) | `provider_environment_updated`: `revision: u64`, `generation: u64`, `applied: bool` | +| `attach_process` | `process_id: string` | `process_attached`: `terminal: bool`; then process I/O | +| `wait` | `process_id: string` | `exited`: `status: ExitStatus` | +| `signal` | `process_id: string`, `signal: Signal` | `signaled` | +| `terminate` | `process_id: string` | `terminated` | +| `terminate_boundary` | — | `boundary_terminated` | +| `exec` | `spec: ExecSpec` | `exec_started`: `process_id: string`, `pty: bool`; then process I/O | +| `exec_signal` | `process_id: string`, `signal: Signal` | `signaled` | +| `resize` | `process_id: string`, `cols: u16`, `rows: u16` | `resized` | +| `loopback_connect` | `host: IP string`, `port: u16` | `port_connected`; then raw bidirectional bytes | +| `accept_network` | — | `network_connected`: `identity: BinaryIdentity`, `destination: socket address`, `socket: SocketMetadata`, `policy_generation: u64`, `timing: Timing`; then TCP decision/relay | +| `open_mediation` | —; sent on `Mediate` | `mediation_ready`; then persistent DNS framing | + +Image policy parse/read failure returns `invalid: true` or an error; it must +never be represented as missing policy. `attach` includes policy with the +supervisor's ready mediation listener. Replayed attach returns the owned session +snapshot without transferring ownership to another supervisor instance. +Termination acknowledgments require all targeted owned processes to be terminal. +`wait` returns a stable status, including on repeat calls. + +Before sending provider files, the supervisor probes support. Provider snapshots +replace the previous environment/files atomically. `generation` orders +publications within a session; stale publications must not replace newer ones. +`applied` is true for an installed publication or its exact replay. Existing +process environments remain unchanged; subsequent execs observe the installed +snapshot. Workload launch applies all controls before its first instruction. + +### Compound JSON types + +- **AgentSpec**: `program: string`, `args: string[]`, `workdir: string|null`, + `timeout_secs: u64`, `interactive: bool`. An empty program asks the backend to + resolve the image's default login shell. +- **ExecSpec**: `program: string`, `args: string[]`, `env: [string,string][]`, + `workdir: string|null`, `pty: bool`, `shell: ShellSpec|null` (default `null`), + `runtime_helper: "sftp"|null` (default `null`). +- **ShellSpec**: `command: string|null`, `login: bool`. The backend resolves the shell + against the workload image. Shell and runtime helper are mutually exclusive. +- **Policy**: `version: u32`, `read_only: string[]`, `read_write: string[]`, + `include_workdir: bool`, `network: "block"|"proxy"|"allow"`, + `proxy_addr: socket address|null`, `landlock: "best_effort"|"hard_requirement"`, + `run_as_user: string|null`, `run_as_group: string|null`. These wire names remain + historical; the external backend translates admitted constraints into its own + mechanisms and fails when it cannot enforce them. +- **Identity**: `uid: u32`, `gid: u32`, `supplementary_gids: u32[]`, + `source: string`, `resource_digest: string`. UID, GID, and groups are nonzero; + groups are sorted, unique, and exclude the primary GID. Source and resource + digest are nonempty driver-owned values. +- **Property**: `enforced: bool`, `mechanism: string`. Confirmation requires + `enforced: true` and a nonempty mechanism for each property. +- **Properties**: `filesystem_confinement: Property`, `egress_interception: + Property`, `request_attribution: Property`, `privilege_floor: Property`. +- **OuterFence**: `generation: string`, `established: string[]`, + `evidence_digest: SHA-256 string`. Established guarantees are exactly + `default_deny_egress`, `no_unmanaged_egress_path`, `revocation_verified`, and + `controller_loss_fails_closed`. The digest commits to + `u64be(UTF-8 generation length) || generation || validated native evidence`. + Serialize the guarantee array in the listed order. The enforcement owner + validates native evidence before projecting these guarantees. +- **Confirmation**: `generation: string`, `identity: Identity`, `properties: + Properties`, `authenticated_supervisor: bool`, `session_id: UUID`, + `outer_fence: OuterFence`, `runtime_exit_terminates_workload: bool`, + `resource_claims: map`, `backend_audit: any JSON`. Generation, + session, identity, claims, and fence must match trusted launch metadata; + both booleans must be true. The backend owns validation of its audit payload. +- **SessionSnapshot**: `generation: string`, `processes: ProcessSnapshot[]`. +- **ProcessSnapshot**: `process_id: string`, `kind: "main"|"exec"`, `terminal: + bool`, `status: ExitStatus|null`, `retained_output: OutputWindow`. +- **OutputWindow**: `first_sequence: u64`, `next_sequence: u64`, `truncated: bool`. +- **ExitStatus**: `{"kind":"exited","value":i32}` or + `{"kind":"signaled","value":i32}`. +- **Signal**: `"term"`, `"kill"`, `"int"`, or `"hup"`. +- **ExecutableIdentity**: `path: string`, `digest: SHA-256 string|null`. +- **BinaryIdentity**: `{"result":"resolved","executable":ExecutableIdentity, + "ancestors":ExecutableIdentity[],"cmdline_paths":string[]}` or + `{"result":"failed","message":string}`. Identity must come from trusted + backend observation; never substitute workload claims on resolution failure. +- **SocketMetadata**: `socket_cookie: u64`, `nonblocking: bool`, + `process_generation: u64`. +- **Timing**: `notification_to_queue_us: u64`, `queue_wait_us: u64`. + +## Process I/O, TCP relay, and DNS + +Process I/O uses `u8 channel | u32be payload length | payload`, with a maximum +payload of 65,536 bytes: + +| Channel | Direction | Meaning | +|---|---|---| +| 0 | supervisor → backend | stdin bytes | +| 1 | backend → supervisor | stdout bytes | +| 2 | backend → supervisor | stderr bytes (non-PTY only) | +| 3 | backend → supervisor | UTF-8 JSON ExitStatus | +| 4 | supervisor → backend | stdin closed; empty payload | +| 5 | supervisor → backend | UTF-8 JSON TCP decision | + +PTY sessions merge stderr into stdout. Send exit only after output drains; +closing stdin must not close the response direction. Relay and exec streams +must not starve control or DNS traffic. The reference transport supports 128 +concurrent streams, 256 KiB per-stream windows, and a 48 MiB connection window. + +After `network_connected`, the backend waits for channel 5. The JSON decision is +`"RelayReady"` or `{"Denied":"PolicyDenied|IdentityUnavailable| +InvalidDestination|ResourceExhausted|MediationUnavailable"}` (one exact enum +value, without `|`). Denial leaves the socket uncommitted; mediation loss denies +by default. `RelayReady` switches to raw bidirectional bytes, and does not bypass +subsequent L7 policy. Each TCP open owns an independent `Exchange` stream. + +After `mediation_ready`, DNS uses `u8 kind | u64be stream ID | u32be JSON length | +JSON`, with a maximum JSON payload of 262,144 bytes. Kind 5 carries a backend +query: `request: byte[]`, `transport: "Udp"|"Tcp"`, `identity: BinaryIdentity`, +`timing: Timing`. Kind 6 carries the matching supervisor result: +`{"result":"response","value":byte[]}` or +`{"result":"error","value":string}`. Stream IDs correlate independent queries +on the persistent channel. Malformed framing, unknown kinds, lost mediation, or +queue exhaustion fail closed; they never authorize direct DNS/egress. + +## Conformance and optional retention + +An external backend must exercise descriptor rejection, lifecycle ordering, +confirmation mismatch, exec replay/deadlines, provider publication, forwarding, +TCP/DNS decisions, connection recovery, credential rotation, and enforcement +loss in its actual driver placement. OpenShell's delegated adapter tests verify +opaque payload forwarding and generic evidence without a native Linux audit. +Its existing transport suites cover recovery, credential renewal, and flow +control; Docker lifecycle E2E verifies the built-in placement. Run +`mise run e2e:isolation-protocol` for the stock supervisor against an independent +Python implementation generated from the public protos. That fixture tests +interoperability and launch ordering, and simulates enforcement evidence with a +fixed test bearer. It does not exercise JWT validation or provide isolation. + +Suspend/resume is a separate optional capability described in +`isolation_backend.proto`. Protocol 1.0 implementations may return +`UNIMPLEMENTED` without advertising it. This registration path supports fresh +launches; it does not yet orchestrate state-preserving restoration. diff --git a/proto/sandbox_protocol/v1/vectors.json b/proto/sandbox_protocol/v1/vectors.json new file mode 100644 index 000000000..591fa478a --- /dev/null +++ b/proto/sandbox_protocol/v1/vectors.json @@ -0,0 +1,282 @@ +{ + "version": "1.0", + "requests": [ + { + "canonical_request": "{\"operation\":\"discover_policy\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000001", + "payload_digest": "f316ae675411d6d0fc8d903e8ec9fbd7c4cfcff90ba4b9a0ea42ee12e6ecb393", + "request": { + "operation": "discover_policy" + } + }, + "frame_hex": "000000b37b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303031222c227061796c6f61645f646967657374223a2266333136616536373534313164366430666338643930336538656339666264376334636663666639306261346239613065613432656531326536656362333933222c2272657175657374223a7b226f7065726174696f6e223a22646973636f7665725f706f6c696379227d7d" + }, + { + "canonical_request": "{\"operation\":\"attach\",\"policy\":{\"include_workdir\":true,\"landlock\":\"hard_requirement\",\"network\":\"block\",\"proxy_addr\":null,\"read_only\":[\"/usr\"],\"read_write\":[\"/sandbox\"],\"run_as_group\":null,\"run_as_user\":null,\"version\":1},\"resource_claims\":{\"actor\":\"external-1\",\"quote\\\"\\\\\\n\":\"line\\n\\b\\f\\r\\t\\u0000\\u001f / 雪 😀\",\"\":\"bmp-last\",\"𐀀\":\"astral-first\"},\"supervisor_instance_id\":\"00000000-0000-4000-8000-000000000002\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000002", + "payload_digest": "6a398dbe0829e4833d9135b8d11e3913aee5a02eb90480289b5250ed939bfe13", + "request": { + "operation": "attach", + "supervisor_instance_id": "00000000-0000-4000-8000-000000000002", + "policy": { + "version": 1, + "read_only": [ + "/usr" + ], + "read_write": [ + "/sandbox" + ], + "include_workdir": true, + "network": "block", + "proxy_addr": null, + "landlock": "hard_requirement", + "run_as_user": null, + "run_as_group": null + }, + "resource_claims": { + "actor": "external-1", + "": "bmp-last", + "𐀀": "astral-first", + "quote\"\\\n": "line\n\b\f\r\t\u0000\u001f / 雪 😀" + } + } + }, + "frame_hex": "000002367b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303032222c227061796c6f61645f646967657374223a2236613339386462653038323965343833336439313335623864313165333931336165653561303265623930343830323839623532353065643933396266653133222c2272657175657374223a7b226f7065726174696f6e223a22617474616368222c2273757065727669736f725f696e7374616e63655f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303032222c22706f6c696379223a7b2276657273696f6e223a312c22726561645f6f6e6c79223a5b222f757372225d2c22726561645f7772697465223a5b222f73616e64626f78225d2c22696e636c7564655f776f726b646972223a747275652c226e6574776f726b223a22626c6f636b222c2270726f78795f61646472223a6e756c6c2c226c616e646c6f636b223a22686172645f726571756972656d656e74222c2272756e5f61735f75736572223a6e756c6c2c2272756e5f61735f67726f7570223a6e756c6c7d2c227265736f757263655f636c61696d73223a7b226163746f72223a2265787465726e616c2d31222c22ee8080223a22626d702d6c617374222c22f0908080223a2261737472616c2d6669727374222c2271756f74655c225c5c5c6e223a226c696e655c6e5c625c665c725c745c75303030305c7530303166202f20e99baa20f09f9880227d7d7d" + }, + { + "canonical_request": "{\"operation\":\"confirm\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000003", + "payload_digest": "c47c155e09b02a1684c146535ab246541a4775d433a91576130bf1a33462b67e", + "request": { + "operation": "confirm" + } + }, + "frame_hex": "000000ab7b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303033222c227061796c6f61645f646967657374223a2263343763313535653039623032613136383463313436353335616232343635343161343737356434333361393135373631333062663161333334363262363765222c2272657175657374223a7b226f7065726174696f6e223a22636f6e6669726d227d7d" + }, + { + "canonical_request": "{\"operation\":\"probe_provider_files\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000004", + "payload_digest": "49d3b528d28661e09c4ac1534e4e6f2f2b9e7f8ee5bdd2fd2b6c0b7619392709", + "request": { + "operation": "probe_provider_files" + } + }, + "frame_hex": "000000b87b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303034222c227061796c6f61645f646967657374223a2234396433623532386432383636316530396334616331353334653465366632663262396537663865653562646432666432623663306237363139333932373039222c2272657175657374223a7b226f7065726174696f6e223a2270726f62655f70726f76696465725f66696c6573227d7d" + }, + { + "canonical_request": "{\"ca_bundle\":null,\"ca_cert\":null,\"operation\":\"start_agent\",\"policy\":{\"include_workdir\":true,\"landlock\":\"hard_requirement\",\"network\":\"block\",\"proxy_addr\":null,\"read_only\":[\"/usr\"],\"read_write\":[\"/sandbox\"],\"run_as_group\":null,\"run_as_user\":null,\"version\":1},\"provider_env\":{\"A\":\"first\",\"Z\":\"last\"},\"provider_env_revision\":1,\"provider_files\":{},\"sandbox_id\":\"sandbox-1\",\"spec\":{\"args\":[],\"interactive\":false,\"program\":\"/bin/true\",\"timeout_secs\":5,\"workdir\":\"/sandbox\"}}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000005", + "payload_digest": "d330c975b9c419ed32b1c98976a041caa165293ecd3078dcbb3c4b3ff5014c0b", + "request": { + "operation": "start_agent", + "sandbox_id": "sandbox-1", + "spec": { + "program": "/bin/true", + "args": [], + "workdir": "/sandbox", + "timeout_secs": 5, + "interactive": false + }, + "policy": { + "version": 1, + "read_only": [ + "/usr" + ], + "read_write": [ + "/sandbox" + ], + "include_workdir": true, + "network": "block", + "proxy_addr": null, + "landlock": "hard_requirement", + "run_as_user": null, + "run_as_group": null + }, + "ca_cert": null, + "ca_bundle": null, + "provider_env_revision": 1, + "provider_env": { + "Z": "last", + "A": "first" + }, + "provider_files": {} + } + }, + "frame_hex": "000002677b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303035222c227061796c6f61645f646967657374223a2264333330633937356239633431396564333262316339383937366130343163616131363532393365636433303738646362623363346233666635303134633062222c2272657175657374223a7b226f7065726174696f6e223a2273746172745f6167656e74222c2273616e64626f785f6964223a2273616e64626f782d31222c2273706563223a7b2270726f6772616d223a222f62696e2f74727565222c2261726773223a5b5d2c22776f726b646972223a222f73616e64626f78222c2274696d656f75745f73656373223a352c22696e746572616374697665223a66616c73657d2c22706f6c696379223a7b2276657273696f6e223a312c22726561645f6f6e6c79223a5b222f757372225d2c22726561645f7772697465223a5b222f73616e64626f78225d2c22696e636c7564655f776f726b646972223a747275652c226e6574776f726b223a22626c6f636b222c2270726f78795f61646472223a6e756c6c2c226c616e646c6f636b223a22686172645f726571756972656d656e74222c2272756e5f61735f75736572223a6e756c6c2c2272756e5f61735f67726f7570223a6e756c6c7d2c2263615f63657274223a6e756c6c2c2263615f62756e646c65223a6e756c6c2c2270726f76696465725f656e765f7265766973696f6e223a312c2270726f76696465725f656e76223a7b225a223a226c617374222c2241223a226669727374227d2c2270726f76696465725f66696c6573223a7b7d7d7d" + }, + { + "canonical_request": "{\"generation\":9007199254740993,\"operation\":\"update_provider_environment\",\"provider_env\":{},\"provider_files\":{},\"revision\":2}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000006", + "payload_digest": "d927db9193ba4efe4c4b2d19242b1a03d3fea01fce9bbba0e9871cea19f3b647", + "request": { + "operation": "update_provider_environment", + "generation": 9007199254740993, + "revision": 2, + "provider_env": {}, + "provider_files": {} + } + }, + "frame_hex": "000001107b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303036222c227061796c6f61645f646967657374223a2264393237646239313933626134656665346334623264313932343262316130336433666561303166636539626262613065393837316365613139663362363437222c2272657175657374223a7b226f7065726174696f6e223a227570646174655f70726f76696465725f656e7669726f6e6d656e74222c2267656e65726174696f6e223a393030373139393235343734303939332c227265766973696f6e223a322c2270726f76696465725f656e76223a7b7d2c2270726f76696465725f66696c6573223a7b7d7d7d" + }, + { + "canonical_request": "{\"operation\":\"attach_process\",\"process_id\":\"generation-1:main\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000007", + "payload_digest": "09735dc472b7fb71d3034b7d0e801f56618027ccdb2ba1d1850a29faa516b11f", + "request": { + "operation": "attach_process", + "process_id": "generation-1:main" + } + }, + "frame_hex": "000000d37b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303037222c227061796c6f61645f646967657374223a2230393733356463343732623766623731643330333462376430653830316635363631383032376363646232626131643138353061323966616135313662313166222c2272657175657374223a7b226f7065726174696f6e223a226174746163685f70726f63657373222c2270726f636573735f6964223a2267656e65726174696f6e2d313a6d61696e227d7d" + }, + { + "canonical_request": "{\"operation\":\"wait\",\"process_id\":\"generation-1:main\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000008", + "payload_digest": "e61da144ef7e66274658d99d77a8155d50a935510f5489d54465ca4c689d05db", + "request": { + "operation": "wait", + "process_id": "generation-1:main" + } + }, + "frame_hex": "000000c97b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303038222c227061796c6f61645f646967657374223a2265363164613134346566376536363237343635386439396437376138313535643530613933353531306635343839643534343635636134633638396430356462222c2272657175657374223a7b226f7065726174696f6e223a2277616974222c2270726f636573735f6964223a2267656e65726174696f6e2d313a6d61696e227d7d" + }, + { + "canonical_request": "{\"operation\":\"signal\",\"process_id\":\"generation-1:main\",\"signal\":\"term\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000009", + "payload_digest": "d91abab7d4835262bdada10f98d61994a9de5d6edfee33853c48d6952c08fc51", + "request": { + "operation": "signal", + "process_id": "generation-1:main", + "signal": "term" + } + }, + "frame_hex": "000000db7b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303039222c227061796c6f61645f646967657374223a2264393161626162376434383335323632626461646131306639386436313939346139646535643665646665653333383533633438643639353263303866633531222c2272657175657374223a7b226f7065726174696f6e223a227369676e616c222c2270726f636573735f6964223a2267656e65726174696f6e2d313a6d61696e222c227369676e616c223a227465726d227d7d" + }, + { + "canonical_request": "{\"operation\":\"terminate\",\"process_id\":\"generation-1:main\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000010", + "payload_digest": "d9197927a320eaeecd06e3b83bfbe9b8d1516479b7561d5125b0530e6a1549ad", + "request": { + "operation": "terminate", + "process_id": "generation-1:main" + } + }, + "frame_hex": "000000ce7b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303130222c227061796c6f61645f646967657374223a2264393139373932376133323065616565636430366533623833626662653962386431353136343739623735363164353132356230353330653661313534396164222c2272657175657374223a7b226f7065726174696f6e223a227465726d696e617465222c2270726f636573735f6964223a2267656e65726174696f6e2d313a6d61696e227d7d" + }, + { + "canonical_request": "{\"operation\":\"terminate_boundary\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000011", + "payload_digest": "fcb65e1ddb606a79abbbad448847b833e4135ca9cc2f9f8e455fbfc9c3837cc2", + "request": { + "operation": "terminate_boundary" + } + }, + "frame_hex": "000000b67b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303131222c227061796c6f61645f646967657374223a2266636236356531646462363036613739616262626164343438383437623833336534313335636139636332663966386534353566626663396333383337636332222c2272657175657374223a7b226f7065726174696f6e223a227465726d696e6174655f626f756e64617279227d7d" + }, + { + "canonical_request": "{\"exec_expires_at_unix_ms\":1800000000000,\"operation\":\"exec\",\"spec\":{\"args\":[\"héllo\"],\"env\":[[\"LANG\",\"C.UTF-8\"],[\"SPECIAL\",\"quote\\\" backslash\\\\ newline\\n NUL\\u0000 emoji😀\"]],\"program\":\"/bin/echo\",\"pty\":false,\"runtime_helper\":null,\"shell\":null,\"workdir\":null}}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000012", + "exec_expires_at_unix_ms": 1800000000000, + "payload_digest": "a269e736f12f736c0c17bd19fc8ff5fb0fd700dd3e04379db81ab961c23c0878", + "request": { + "operation": "exec", + "spec": { + "program": "/bin/echo", + "args": [ + "héllo" + ], + "shell": null, + "runtime_helper": null, + "env": [ + [ + "LANG", + "C.UTF-8" + ], + [ + "SPECIAL", + "quote\" backslash\\ newline\n NUL\u0000 emoji😀" + ] + ], + "workdir": null, + "pty": false + } + } + }, + "frame_hex": "0000019a7b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303132222c22657865635f657870697265735f61745f756e69785f6d73223a313830303030303030303030302c227061796c6f61645f646967657374223a2261323639653733366631326637333663306331376264313966633866663566623066643730306464336530343337396462383161623936316332336330383738222c2272657175657374223a7b226f7065726174696f6e223a2265786563222c2273706563223a7b2270726f6772616d223a222f62696e2f6563686f222c2261726773223a5b2268c3a96c6c6f225d2c227368656c6c223a6e756c6c2c2272756e74696d655f68656c706572223a6e756c6c2c22656e76223a5b5b224c414e47222c22432e5554462d38225d2c5b225350454349414c222c2271756f74655c22206261636b736c6173685c5c206e65776c696e655c6e204e554c5c753030303020656d6f6a69f09f9880225d5d2c22776f726b646972223a6e756c6c2c22707479223a66616c73657d7d7d" + }, + { + "canonical_request": "{\"operation\":\"exec_signal\",\"process_id\":\"generation-1:exec\",\"signal\":\"kill\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000013", + "payload_digest": "bc80382030d743852845bc35c200f7c9c51e76c5369acca6024f3d69b6d8bdb7", + "request": { + "operation": "exec_signal", + "process_id": "generation-1:exec", + "signal": "kill" + } + }, + "frame_hex": "000000e07b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303133222c227061796c6f61645f646967657374223a2262633830333832303330643734333835323834356263333563323030663763396335316537366335333639616363613630323466336436396236643862646237222c2272657175657374223a7b226f7065726174696f6e223a22657865635f7369676e616c222c2270726f636573735f6964223a2267656e65726174696f6e2d313a65786563222c227369676e616c223a226b696c6c227d7d" + }, + { + "canonical_request": "{\"cols\":80,\"operation\":\"resize\",\"process_id\":\"generation-1:main\",\"rows\":24}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000014", + "payload_digest": "f51f875abe67b01093312c0536b7686b30a47ce3ea1bcfcd4206052fe44db356", + "request": { + "operation": "resize", + "process_id": "generation-1:main", + "cols": 80, + "rows": 24 + } + }, + "frame_hex": "000000df7b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303134222c227061796c6f61645f646967657374223a2266353166383735616265363762303130393333313263303533366237363836623330613437636533656131626366636434323036303532666534346462333536222c2272657175657374223a7b226f7065726174696f6e223a22726573697a65222c2270726f636573735f6964223a2267656e65726174696f6e2d313a6d61696e222c22636f6c73223a38302c22726f7773223a32347d7d" + }, + { + "canonical_request": "{\"host\":\"127.0.0.1\",\"operation\":\"loopback_connect\",\"port\":8080}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000015", + "payload_digest": "62c8b23330ea3c04bc820acdc50e77286b7439d541f626ae2c35ad3e3dc023d4", + "request": { + "operation": "loopback_connect", + "host": "127.0.0.1", + "port": 8080 + } + }, + "frame_hex": "000000d37b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303135222c227061796c6f61645f646967657374223a2236326338623233333330656133633034626338323061636463353065373732383662373433396435343166363236616532633335616433653364633032336434222c2272657175657374223a7b226f7065726174696f6e223a226c6f6f706261636b5f636f6e6e656374222c22686f7374223a223132372e302e302e31222c22706f7274223a383038307d7d" + }, + { + "canonical_request": "{\"operation\":\"open_mediation\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000016", + "payload_digest": "ddc0eb31d09a4e3f8528f7c9e075d5f252739b6a0ff364995b42f52181249eb4", + "request": { + "operation": "open_mediation" + } + }, + "frame_hex": "000000b27b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303136222c227061796c6f61645f646967657374223a2264646330656233316430396134653366383532386637633965303735643566323532373339623661306666333634393935623432663532313831323439656234222c2272657175657374223a7b226f7065726174696f6e223a226f70656e5f6d6564696174696f6e227d7d" + }, + { + "canonical_request": "{\"operation\":\"accept_network\"}", + "envelope": { + "request_id": "00000000-0000-4000-8000-000000000017", + "payload_digest": "cc7045e9b9efe3b32d8082d881c8d872aabbc6c910edf20f9d3a9bc9e7d31f2e", + "request": { + "operation": "accept_network" + } + }, + "frame_hex": "000000b27b22726571756573745f6964223a2230303030303030302d303030302d343030302d383030302d303030303030303030303137222c227061796c6f61645f646967657374223a2263633730343565396239656665336233326438303832643838316338643837326161626263366339313065646632306639643361396263396537643331663265222c2272657175657374223a7b226f7065726174696f6e223a226163636570745f6e6574776f726b227d7d" + } + ] +}