Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions crates/openshell-supervisor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# OpenShell supervisor

The supervisor loads and reconciles policy, maintains provider credentials, applies network and MCP inspection, and drives the admitted isolation backend through attachment, confirmation, and workload start.

## Backend startup

The public `run_sandbox` entry point selects the OpenShell Sandbox Protocol backend and collects its startup inputs into a private `SandboxRunConfig`. Shared startup receives that config and the trusted backend setup separately. The `backend_setup` module owns that backend's launch-data decoder, workload policy discovery, and client construction. Descriptor contents cannot select an implementation.

Shared startup checks the admitted backend name before passing the opaque payload to its decoder. It then compares the decoded sandbox, session, and runtime generation with the trusted launch inputs before installing credentials or discovering workload policy. A mismatch stops startup.

The built-in decoder also carries the VM driver's fixed workload identity into shared policy validation. Startup and later policy updates must reject selectors that conflict with that identity. Other launch descriptors do not enable this VM-specific check.

The supervisor admits policy and prepares credentials before constructing and attaching the selected client. It uses the isolation contract's `BoundBoundary` and `ConfirmedBoundary` directly: confirm the attached boundary, prepare network mediation, then start the workload. Backend implementations remain responsible for validating their native enforcement evidence through the isolation contract.

The client receives the supervisor's live provider state, bearer-token slot, and CA-path slot. Provider refresh, token rotation, and later CA publication must remain visible through those shared handles. Startup does not create independent copies of their current values.

The setup interface stays private to the supervisor. It adds no runtime backend registration, endpoint configuration, or public factory API. The public `run_sandbox` signature and standard backend selection remain unchanged.
250 changes: 250 additions & 0 deletions crates/openshell-supervisor/src/backend_setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Backend-owned launch decoding and client construction for supervisor startup.

use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use miette::Result;
use openshell_core::SandboxSessionId;
use openshell_core::jwt::{SessionBearerTokenSlot, SupervisorAuthBundle};
use openshell_core::provider_credentials::ProviderCredentialState;
use openshell_isolation_interface::AgentSpec;
use openshell_isolation_interface::contract::{
BackendDescriptor, BackendError, BackendRegistry, BoundBoundary, IsolationBackend,
ResolvedWorkloadIdentity, SandboxContext, SandboxPolicy,
};

/// Coordinates decoded by the selected trusted backend. Shared startup checks
/// these against admission before credentials or discovery reach that backend.
pub struct LaunchIdentity {
pub sandbox_id: String,
pub generation: String,
pub session_id: SandboxSessionId,
pub workload_identity: ResolvedWorkloadIdentity,
/// Preserve driver-fixed VM selectors during shared startup and reload.
pub vm_policy_identity: Option<super::VmPolicyIdentity>,
}

/// Runtime state remains owned by the supervisor. Backends retain these same
/// handles so CA publication, provider reload, and bearer rotation stay visible
/// after attachment; copying their current contents would lose later updates.
pub struct BackendServices {
pub ca_file_paths: Arc<Mutex<Option<(PathBuf, PathBuf)>>>,
pub provider_credentials: ProviderCredentialState,
pub sandbox_bearer: SessionBearerTokenSlot,
}

/// Selected by trusted composition, never by payload contents. Decoding and
/// construction may prepare a client but must not launch workload code.
pub trait BackendSetup: Sync {
/// Name chosen by trusted composition and checked against admission before decoding.
fn backend_name(&self) -> &str;

/// Decode native launch data without starting workload code. The returned
/// identity must describe the same resource retained by the prepared backend.
fn decode(
&self,
payload: &[u8],
) -> std::result::Result<(LaunchIdentity, Box<dyn PreparedBackend>), BackendError>;
}

/// Native launch data stays with its backend until client construction. Shared
/// startup admits policy and credentials between discovery and attachment.
#[tonic::async_trait]
pub trait PreparedBackend: Send + Sync {
/// Read image policy through the backend's authenticated client. An invalid
/// or unreadable policy must return the invalid flag or an error, never missing.
async fn discover_policy(
&self,
bearer: SessionBearerTokenSlot,
) -> std::result::Result<(Option<String>, bool), BackendError>;

/// Consume prepared launch data and retain the supervisor's live handles.
/// Construction does not attach the resource or start workload code.
fn build(
self: Box<Self>,
services: BackendServices,
) -> std::result::Result<Arc<dyn IsolationBackend>, BackendError>;
}

/// Created only after name and launch identity checks. Consuming attachment
/// prevents reusing one prepared startup to build or attach a second client.
pub struct SelectedBackend {
descriptor: BackendDescriptor,
identity: LaunchIdentity,
prepared: Box<dyn PreparedBackend>,
}

impl SelectedBackend {
/// Reject absent or mismatched admission before decoding, then bind the
/// decoded sandbox, session, and generation to trusted launch inputs.
pub fn select(
setup: &dyn BackendSetup,
descriptor: BackendDescriptor,
admitted_backend: Option<&str>,
sandbox_id: Option<&str>,
auth: &SupervisorAuthBundle,
) -> Result<Self> {
let admitted_backend = admitted_backend.ok_or_else(|| {
miette::miette!("runtime descriptor supplied without an admitted isolation backend")
})?;
if descriptor.backend_name != admitted_backend {
return Err(miette::miette!(
"descriptor backend {:?} does not match admitted backend {admitted_backend:?}",
descriptor.backend_name
));
}
if setup.backend_name() != admitted_backend {
return Err(miette::miette!(
"selected backend {:?} does not match admitted backend {admitted_backend:?}",
setup.backend_name()
));
}

let (identity, prepared) = setup
.decode(&descriptor.payload)
.map_err(|error| miette::miette!(error.to_string()))?;
if sandbox_id.is_none_or(|id| id.is_empty() || identity.sandbox_id != id) {
return Err(miette::miette!(
"runtime descriptor does not match admitted sandbox"
));
}
if identity.session_id != auth.session_id {
return Err(miette::miette!(
"supervisor authentication bundle does not match runtime session"
));
}
if identity.generation != auth.runtime_generation.as_str() {
return Err(miette::miette!(
"supervisor authentication bundle does not match runtime generation"
));
}
Ok(Self {
descriptor,
identity,
prepared,
})
}

/// Return the backend name already matched to the admitted selection.
pub fn backend_name(&self) -> &str {
&self.descriptor.backend_name
}

/// Carry the selected backend's VM identity constraint into policy handling.
pub fn vm_policy_identity(&self) -> Option<super::VmPolicyIdentity> {
self.identity.vm_policy_identity
}

/// Delegate image discovery only after shared name and identity checks.
/// Callers may retry this read; discovery cannot authorize workload launch.
pub async fn discover_policy(
&self,
bearer: SessionBearerTokenSlot,
) -> Result<(Option<String>, bool)> {
self.prepared
.discover_policy(bearer)
.await
.map_err(|error| miette::miette!("discover workload image policy: {error}"))
}

/// Construct and attach the selected client using the admitted policy and
/// shared services. Registry verification rejects a differently named client.
pub async fn attach(
self,
services: BackendServices,
policy: SandboxPolicy,
agent: AgentSpec,
) -> Result<Box<dyn BoundBoundary>> {
let backend = self
.prepared
.build(services)
.map_err(|error| miette::miette!(error.to_string()))?;
let mut registry = BackendRegistry::new();
registry
.register(backend)
.map_err(|error| miette::miette!(error.to_string()))?;
let admitted_backend = self.descriptor.backend_name.clone();
let (backend, verified) = registry
.resolve(self.descriptor, &admitted_backend)
.map_err(|error| miette::miette!(error.to_string()))?;
backend
.attach(
verified,
SandboxContext {
sandbox_id: self.identity.sandbox_id,
session_id: self.identity.session_id,
policy,
agent,
identity: self.identity.workload_identity,
},
)
.await
.map_err(|error| miette::miette!(error.to_string()))
}
}

/// The standard binary selects the `OpenShell` Sandbox Protocol. Its wire schema
/// and concrete client stay here rather than in the shared startup sequence.
pub struct OpenShellBackendSetup;

impl BackendSetup for OpenShellBackendSetup {
fn backend_name(&self) -> &str {
openshell_sandbox_backend::BACKEND_NAME
}

fn decode(
&self,
payload: &[u8],
) -> std::result::Result<(LaunchIdentity, Box<dyn PreparedBackend>), BackendError> {
let descriptor: openshell_sandbox_backend::boundary_protocol::SandboxRuntimeDescriptor =
serde_json::from_slice(payload).map_err(|error| {
BackendError::Descriptor(format!("decode sandbox runtime descriptor: {error}"))
})?;
let identity = LaunchIdentity {
sandbox_id: descriptor.boundary_id.clone(),
generation: descriptor.generation.clone(),
session_id: descriptor.session_id,
workload_identity: descriptor.workload_identity.clone(),
vm_policy_identity: descriptor
.resource_claims
.contains_key("vm.generation")
.then_some(super::VmPolicyIdentity {
uid: descriptor.workload_identity.uid,
gid: descriptor.workload_identity.gid,
}),
};
Ok((identity, Box::new(OpenShellLaunch(descriptor))))
}
}

struct OpenShellLaunch(openshell_sandbox_backend::boundary_protocol::SandboxRuntimeDescriptor);

#[tonic::async_trait]
impl PreparedBackend for OpenShellLaunch {
async fn discover_policy(
&self,
bearer: SessionBearerTokenSlot,
) -> std::result::Result<(Option<String>, bool), BackendError> {
openshell_sandbox_backend::OpenShellRuntimeBackend::discover_policy(self.0.clone(), bearer)
.await
}

fn build(
self: Box<Self>,
services: BackendServices,
) -> std::result::Result<Arc<dyn IsolationBackend>, BackendError> {
Ok(Arc::new(
openshell_sandbox_backend::OpenShellRuntimeBackend::new(
services.ca_file_paths,
services.provider_credentials,
services.sandbox_bearer,
),
))
}
}

#[cfg(test)]
mod tests;
Loading
Loading