Skip to content

Commit fc7caf2

Browse files
committed
refactor(supervisor): launch sandbox instances from one config and prepared bindings
Replace SandboxRunConfig and its field-by-field copy into SandboxInstanceConfig with a single launch configuration. Live per-instance state (gateway credentials, OCSF context and JSON settings, log sink, and denial log) is now an explicit SandboxInstanceBindings value that the owner prepares once, instead of placeholders the runner or host overwrote. The dedicated binary loads launch inputs from its arguments and environment and shares one GatewayCredentials with log push; run_sandbox_with_backend keeps the process-wide OCSF context, health endpoints, and signal handling. The instance host binds the same SandboxInstanceBindings to its runtime threads and revokes their credentials when the instance ends. Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 408de7d commit fc7caf2

6 files changed

Lines changed: 337 additions & 391 deletions

File tree

‎crates/openshell-supervisor/README.md‎

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The supervisor loads and reconciles policy, maintains provider credentials, appl
44

55
## Backend startup
66

7-
The public `run_sandbox` entry point selects the OpenShell Sandbox Protocol backend and collects its startup inputs into a public `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.
7+
The supervisor binary selects a backend setup and collects its launch inputs into a public `SandboxInstanceConfig`. 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.
88

99
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.
1010

@@ -14,7 +14,7 @@ The supervisor admits policy and prepares credentials before constructing and at
1414

1515
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.
1616

17-
`backend_setup`, `SandboxRunConfig`, and `run_sandbox_with_backend` are public so the supervisor binary can select a registered backend. External backends should implement the `IsolationBackend` and `SandboxProtocol` services rather than link this crate.
17+
`backend_setup`, `SandboxInstanceConfig`, `SandboxInstanceBindings`, and `run_sandbox_with_backend` are public so the supervisor binary can select a registered backend. External backends should implement the `IsolationBackend` and `SandboxProtocol` services rather than link this crate.
1818

1919
The startup result separates the backend transport payload from optional
2020
authenticated CONNECT listener settings. Shared networking starts the listener
@@ -29,27 +29,32 @@ Gateway connections, and the gateway supervisor sessions carried over them,
2929
can reconnect within an instance without creating another one; the generation
3030
remains the fencing identifier.
3131

32-
`run_sandbox_instance` supervises one sandbox instance from a
33-
`SandboxInstanceConfig` and `SandboxInstanceControls`. Everything that belongs
34-
to that instance is an input or is created by the instance: gateway
35-
credentials and their renewal, the Sandbox Protocol credential, policy,
36-
provider credentials and token grants, middleware credentials, OCSF JSON
37-
settings, proxy trust files, the denial history served by `policy.local`, the
38-
backend connection, access plane, readiness, and stop signal. Gateway clients
39-
receive a `GatewayEndpoint`, which pairs the URL with that instance's
40-
`GatewayCredentials`; nothing is installed process-wide.
41-
42-
`run_sandbox_with_backend` is the dedicated wrapper. It keeps the existing
43-
process contract: it sets the process-wide OCSF context, reads launch inputs
44-
from the environment, binds the readiness socket and port to instance
45-
readiness, and stops the instance on SIGTERM or SIGINT. The binary shares one
46-
`GatewayCredentials` between log push and supervision, so renewal by either
47-
keeps both current.
32+
`run_sandbox_instance` supervises one sandbox instance from three inputs.
33+
`SandboxInstanceConfig` is the single launch configuration: identity, command,
34+
policy inputs, auth bundle, backend descriptor, and file locations.
35+
`SandboxInstanceBindings` is the live state its owner prepares once: the
36+
`GatewayCredentials` issued from the auth bundle, the OCSF context and JSON
37+
settings, the log sink, and the denial history served by `policy.local`.
38+
`SandboxInstanceControls` carries the owner's stop signal and readiness.
39+
Everything else, including credential renewal, policy, provider credentials and
40+
token grants, middleware credentials, the backend connection, and the access
41+
plane, is created by the instance. Gateway clients receive a `GatewayEndpoint`,
42+
which pairs the URL with that instance's `GatewayCredentials`; nothing is
43+
installed process-wide.
44+
45+
The dedicated process wrapper keeps the existing process contract. The binary
46+
loads the launch configuration from its arguments and environment and prepares
47+
`SandboxInstanceBindings::dedicated`, sharing one `GatewayCredentials` with log
48+
push and the OCSF JSON settings with its logging layers, so renewal by either
49+
gateway client keeps both current. `run_sandbox_with_backend` sets the
50+
process-wide OCSF context, binds the readiness socket and port to instance
51+
readiness, and stops the instance on SIGTERM or SIGINT.
4852

4953
`sandbox_instance_host::SandboxInstanceHost` runs several sandbox instances in
5054
one process. Each instance gets its own Tokio runtime; every thread of that
51-
runtime, and the thread driving it, is bound to the instance's OCSF context,
52-
OCSF JSON settings, log sink, and denial log. Spawned tasks, blocking work,
55+
runtime, and the thread driving it, is bound to the instance's
56+
`SandboxInstanceBindings`: OCSF context, OCSF JSON settings, log sink, and
57+
denial log. Spawned tasks, blocking work,
5358
callbacks, reconnects, and renewal therefore inherit the instance's identity,
5459
and threads outside an instance carry none and push no logs. The OCSF context
5560
also supplies the sandbox identity in middleware requests.

‎crates/openshell-supervisor/src/backend_setup/tests.rs‎

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -680,32 +680,42 @@ async fn shared_startup_uses_selected_backend_through_readiness_and_shutdown() {
680680
.require_networking
681681
.store(true, Ordering::SeqCst);
682682
let startup = |await_main_process_attachment| {
683+
let config = crate::SandboxInstanceConfig {
684+
command: vec!["test-agent".into(), "test-argument".into()],
685+
workdir: Some("/test-workspace".into()),
686+
timeout_secs: 15,
687+
interactive: false,
688+
await_main_process_attachment,
689+
sandbox_id: Some(setup.sandbox_id.clone()),
690+
sandbox: None,
691+
container_image: String::new(),
692+
openshell_endpoint: None,
693+
policy_rules: Some(rules.to_string_lossy().into_owned()),
694+
policy_data: Some(data.to_string_lossy().into_owned()),
695+
ssh_socket_path: None,
696+
shared_ssh_socket: false,
697+
upstream_proxy_args: UpstreamProxyArgs::default(),
698+
additional_ca_bundle: None,
699+
backend_descriptor: TestSetup::descriptor(),
700+
auth_bundle: setup.auth.clone(),
701+
admitted_isolation_backend: Some(TEST_BACKEND.into()),
702+
main_exit_marker: Some(marker.clone()),
703+
trace_context: None,
704+
provider_workload_api_socket: None,
705+
// The binary loads the proxy trust directory from its environment.
706+
proxy_tls: openshell_supervisor_network::run::ProxyTlsFiles::from_environment(),
707+
};
708+
let bindings = crate::SandboxInstanceBindings::dedicated(
709+
&config,
710+
openshell_core::grpc_client::GatewayCredentials::from_auth_bundle(&setup.auth).unwrap(),
711+
openshell_ocsf::OcsfJsonSettings::default(),
712+
);
683713
Box::pin(crate::run_sandbox_with_backend(
684714
&setup,
685-
crate::SandboxRunConfig {
686-
command: vec!["test-agent".into(), "test-argument".into()],
687-
workdir: Some("/test-workspace".into()),
688-
timeout_secs: 15,
689-
interactive: false,
690-
await_main_process_attachment,
691-
sandbox_id: Some(setup.sandbox_id.clone()),
692-
sandbox: None,
693-
openshell_endpoint: None,
694-
policy_rules: Some(rules.to_string_lossy().into_owned()),
695-
policy_data: Some(data.to_string_lossy().into_owned()),
696-
ssh_socket_path: None,
697-
health_socket_path: Some(readiness.clone()),
698-
health_port: Some(health_port),
699-
ocsf_enabled: Arc::new(AtomicBool::new(false)),
700-
ocsf_schema_version: Arc::new(Mutex::new(String::new())),
701-
upstream_proxy_args: UpstreamProxyArgs::default(),
702-
additional_ca_bundle: None,
703-
backend_descriptor: TestSetup::descriptor(),
704-
auth_bundle: setup.auth.clone(),
705-
gateway_credentials: None,
706-
admitted_isolation_backend: Some(TEST_BACKEND.into()),
707-
main_exit_marker: Some(marker.clone()),
708-
},
715+
config,
716+
bindings,
717+
Some(readiness.clone()),
718+
Some(health_port),
709719
))
710720
};
711721

‎crates/openshell-supervisor/src/legacy_startup_regression_tests.rs‎

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use openshell_core::proto::{
1111
UpdateConfigResponse, gateway_message, supervisor_message,
1212
};
1313
use openshell_isolation_interface::contract::{BackendError, ResolvedWorkloadIdentity};
14-
use std::sync::{Mutex, atomic::Ordering};
14+
use std::sync::atomic::Ordering;
1515
use tonic::codegen::tokio_stream::{StreamExt as _, wrappers::TcpListenerStream};
1616
use tonic::codegen::{Body, BoxFuture, BoxStream, Bytes, Service, StdError, http};
1717

@@ -277,37 +277,41 @@ async fn config_push_regression_legacy_gateway_never_observes_ready_before_workl
277277
session_id,
278278
probe: probe.clone(),
279279
};
280-
let startup = run_sandbox_with_backend(
281-
&setup,
282-
SandboxRunConfig {
283-
command: vec!["true".into()],
284-
workdir: None,
285-
timeout_secs: 0,
286-
interactive: false,
287-
await_main_process_attachment: false,
288-
sandbox_id: Some("sb-legacy-restart".into()),
289-
sandbox: Some("legacy-restart".into()),
290-
openshell_endpoint: Some(endpoint),
291-
policy_rules: None,
292-
policy_data: None,
293-
ssh_socket_path: None,
294-
health_socket_path: None,
295-
health_port: None,
296-
ocsf_enabled: Arc::new(AtomicBool::new(false)),
297-
ocsf_schema_version: Arc::new(Mutex::new(String::new())),
298-
upstream_proxy_args:
299-
openshell_supervisor_network::upstream_proxy::UpstreamProxyArgs::default(),
300-
additional_ca_bundle: None,
301-
backend_descriptor: openshell_isolation_interface::contract::BackendDescriptor {
302-
backend_name: "test-held".into(),
303-
payload: Vec::new(),
304-
},
305-
auth_bundle,
306-
gateway_credentials: None,
307-
admitted_isolation_backend: Some("test-held".into()),
308-
main_exit_marker: None,
280+
let config = SandboxInstanceConfig {
281+
command: vec!["true".into()],
282+
workdir: None,
283+
timeout_secs: 0,
284+
interactive: false,
285+
await_main_process_attachment: false,
286+
sandbox_id: Some("sb-legacy-restart".into()),
287+
sandbox: Some("legacy-restart".into()),
288+
container_image: String::new(),
289+
openshell_endpoint: Some(endpoint),
290+
policy_rules: None,
291+
policy_data: None,
292+
ssh_socket_path: None,
293+
shared_ssh_socket: false,
294+
upstream_proxy_args:
295+
openshell_supervisor_network::upstream_proxy::UpstreamProxyArgs::default(),
296+
additional_ca_bundle: None,
297+
backend_descriptor: openshell_isolation_interface::contract::BackendDescriptor {
298+
backend_name: "test-held".into(),
299+
payload: Vec::new(),
309300
},
301+
auth_bundle,
302+
admitted_isolation_backend: Some("test-held".into()),
303+
main_exit_marker: None,
304+
trace_context: None,
305+
provider_workload_api_socket: None,
306+
proxy_tls: openshell_supervisor_network::run::ProxyTlsFiles::default(),
307+
};
308+
let bindings = SandboxInstanceBindings::dedicated(
309+
&config,
310+
openshell_core::grpc_client::GatewayCredentials::from_auth_bundle(&config.auth_bundle)
311+
.unwrap(),
312+
openshell_ocsf::OcsfJsonSettings::default(),
310313
);
314+
let startup = run_sandbox_with_backend(&setup, config, bindings, None, None);
311315
tokio::pin!(startup);
312316
let result = tokio::select! {
313317
() = probe.ready_event.notified() => None,

0 commit comments

Comments
 (0)