Skip to content

Commit ccfdfbb

Browse files
committed
refactor(supervisor): name hosted lifetimes sandbox instances
A sandbox instance is one running generation of a stable Sandbox resource. Gateway transport connections and the gateway supervisor sessions they carry can reconnect within one instance, so the hosted object, its configuration, controls, host, and per-instance network, denial, and token-grant state now use the sandbox instance name. Rename session_host to sandbox_instance_host, run_session to run_sandbox_instance, and SessionConfig, SessionControls, SessionHost, and related hosted types to their SandboxInstance equivalents. Gateway supervisor sessions, SSH sessions, and authentication session types keep their names. No wire, configuration, or lifecycle behavior changes. Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent bce3e09 commit ccfdfbb

15 files changed

Lines changed: 422 additions & 367 deletions

File tree

‎crates/openshell-core/src/grpc_client.rs‎

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! `OpenShell` server.
66
//!
77
//! Every request carries a sandbox bearer credential in the `Authorization`
8-
//! header. Each supervisor session owns its [`GatewayCredentials`], usually
8+
//! header. Each sandbox instance owns its [`GatewayCredentials`], usually
99
//! built from the trusted launch bundle with
1010
//! [`GatewayCredentials::from_auth_bundle`]. Diagnostic tools can instead
1111
//! resolve a token with [`GatewayCredentials::from_environment`] from one of:
@@ -16,8 +16,8 @@
1616
//! caller exchanges it for a gateway JWT via `IssueSandboxToken`.
1717
//!
1818
//! Callers reach the gateway through a [`GatewayEndpoint`], which pairs the
19-
//! URL with one session's credentials. No credential is installed
20-
//! process-wide, so sessions hosted by one supervisor process cannot use or
19+
//! URL with one sandbox instance's credentials. No credential is installed
20+
//! process-wide, so instances hosted by one supervisor process cannot use or
2121
//! rotate each other's identity.
2222
2323
use std::collections::HashMap;
@@ -65,7 +65,7 @@ pub const MAX_SUPERVISOR_STREAM_MESSAGE_BYTES: usize = 9 * 1024 * 1024;
6565

6666
/// Shared, refreshable Bearer header. All [`AuthInterceptor`] clones read
6767
/// the same slot, so the renewal task can replace the token in place without
68-
/// rebuilding the channel. `None` means the owning session revoked it.
68+
/// rebuilding the channel. `None` means the owning sandbox instance revoked it.
6969
type TokenSlot = Arc<RwLock<Option<AsciiMetadataValue>>>;
7070

7171
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -75,7 +75,7 @@ enum TokenSource {
7575
K8sServiceAccount,
7676
}
7777

78-
/// Gateway credentials owned by one supervisor session.
78+
/// Gateway credentials owned by one sandbox instance.
7979
///
8080
/// Clients built from a [`GatewayEndpoint`] read only these slots, and the
8181
/// renewal task started by the first connection updates only them. Dropping
@@ -200,7 +200,7 @@ impl GatewayCredentials {
200200
}
201201

202202
/// Start the renewal task once for these credentials, on the caller's
203-
/// runtime, so a hosted session's renewal stops with that session.
203+
/// runtime, so a hosted sandbox instance's renewal stops with that instance.
204204
fn start_renewal(&self, channel: &AuthedChannel, plain_channel: &Channel, endpoint: &str) {
205205
let mut renewal = self
206206
.inner
@@ -239,14 +239,16 @@ impl GatewayCredentials {
239239
.bearer
240240
.write()
241241
.unwrap_or_else(std::sync::PoisonError::into_inner);
242-
// A revoked session must not be revived by an in-flight response.
242+
// Revoked instance credentials must not be revived by an in-flight
243+
// response.
243244
if slot.is_some() {
244245
*slot = Some(bearer);
245246
}
246247
}
247248
}
248249

249-
/// The gateway URL a session uses, paired with that session's credentials.
250+
/// The gateway URL a sandbox instance uses, paired with that instance's
251+
/// credentials.
250252
#[derive(Clone)]
251253
pub struct GatewayEndpoint {
252254
url: Arc<str>,
@@ -271,7 +273,7 @@ impl GatewayEndpoint {
271273
&self.credentials
272274
}
273275

274-
/// Reach another gateway address with the same session credentials.
276+
/// Reach another gateway address with the same sandbox instance credentials.
275277
#[must_use]
276278
pub fn with_url(&self, url: impl Into<Arc<str>>) -> Self {
277279
Self::new(url, self.credentials.clone())
@@ -357,7 +359,9 @@ impl tonic::service::Interceptor for AuthInterceptor {
357359
.read()
358360
.unwrap_or_else(std::sync::PoisonError::into_inner)
359361
.clone()
360-
.ok_or_else(|| Status::unauthenticated("gateway session credentials were revoked"))?;
362+
.ok_or_else(|| {
363+
Status::unauthenticated("sandbox instance gateway credentials were revoked")
364+
})?;
361365
req.metadata_mut().insert("authorization", bearer);
362366
#[cfg(feature = "trace-context")]
363367
let req =
@@ -458,11 +462,14 @@ impl Drop for ClientSpanStatus {
458462
}
459463
}
460464

461-
/// Build a Bearer-authenticated channel to the gateway with one session's
462-
/// credentials. The first connection starts that session's renewal task.
465+
/// Build a Bearer-authenticated channel to the gateway with one sandbox
466+
/// instance's credentials. The first connection starts that instance's
467+
/// renewal task.
463468
async fn connect_channel(gateway: &GatewayEndpoint) -> Result<AuthedChannel> {
464469
if gateway.credentials.is_revoked() {
465-
return Err(miette::miette!("gateway session credentials were revoked"));
470+
return Err(miette::miette!(
471+
"sandbox instance gateway credentials were revoked"
472+
));
466473
}
467474
let channel = build_plain_channel(gateway.url()).await?;
468475
let intercepted = InterceptedService::new(
@@ -577,8 +584,9 @@ pub async fn report_provider_readiness(
577584
.into_diagnostic()
578585
}
579586

580-
/// Slots and transport retained by one session's renewal task. It holds no
581-
/// reference to [`GatewayCredentials`], so dropping them still stops it.
587+
/// Slots and transport retained by one sandbox instance's renewal task. It
588+
/// holds no reference to [`GatewayCredentials`], so dropping them still stops
589+
/// it.
582590
struct RenewalTarget {
583591
channel: AuthedChannel,
584592
bearer: TokenSlot,
@@ -590,10 +598,10 @@ struct RenewalTarget {
590598
}
591599

592600
/// Background task that renews the sandbox JWT at ~80% of its remaining
593-
/// lifetime. The new token replaces the value in the session's slot, so all
594-
/// in-flight and future clients of that session pick it up on their next
595-
/// request. The loop never panics: every failure is logged and re-attempted
596-
/// after a bounded backoff.
601+
/// lifetime. The new token replaces the value in the sandbox instance's slot,
602+
/// so all in-flight and future clients of that instance pick it up on their
603+
/// next request. The loop never panics: every failure is logged and
604+
/// re-attempted after a bounded backoff.
597605
async fn refresh_token_loop(target: RenewalTarget) {
598606
let RenewalTarget {
599607
channel,
@@ -832,7 +840,7 @@ fn compute_refresh_delay(slot: &TokenSlot) -> Option<Duration> {
832840
let current = slot
833841
.read()
834842
.unwrap_or_else(std::sync::PoisonError::into_inner);
835-
// A revoked session has nothing to renew.
843+
// Revoked instance credentials have nothing to renew.
836844
current.as_ref()?.to_str().ok().map(str::to_string)
837845
}
838846
.unwrap_or_default();
@@ -1058,7 +1066,7 @@ mod workspace_tests {
10581066
}
10591067
}
10601068

1061-
/// Connect to the `OpenShell` server with one session's credentials.
1069+
/// Connect to the `OpenShell` server with one sandbox instance's credentials.
10621070
async fn connect(gateway: &GatewayEndpoint) -> Result<OpenShellClient<AuthedChannel>> {
10631071
let channel = connect_channel(gateway).await?;
10641072
Ok(OpenShellClient::new(channel))
@@ -1711,7 +1719,8 @@ mod token_grant_exchange_tests {
17111719
#[derive(Clone)]
17121720
pub struct CachedOpenShellClient {
17131721
client: OpenShellClient<AuthedChannel>,
1714-
/// Session credentials rotated alongside extension credentials.
1722+
/// Sandbox instance gateway credentials rotated alongside extension
1723+
/// credentials.
17151724
credentials: GatewayCredentials,
17161725
workspace: Arc<tokio::sync::OnceCell<String>>,
17171726
/// Extension credentials for this supervisor. Cloning the client shares

‎crates/openshell-ocsf/src/builders/mod.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,11 @@ pub struct EventContext {
200200
pub origin: EventOrigin,
201201
}
202202

203-
/// Event context borrowed from the caller or shared by a hosted session.
203+
/// Event context borrowed from the caller or shared by a hosted sandbox
204+
/// instance.
204205
///
205-
/// Builders accept either form, so events attributed to a hosted supervisor
206-
/// session need not leak a `'static` context for each session.
206+
/// Builders accept either form, so events attributed to a hosted sandbox
207+
/// instance need not leak a `'static` context for each instance.
207208
#[derive(Debug, Clone)]
208209
pub enum ContextRef<'a> {
209210
Borrowed(&'a EventContext),

‎crates/openshell-ocsf/src/ctx.rs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
//! Process-wide and session-bound [`EventContext`].
4+
//! Process-wide and sandbox-instance-bound [`EventContext`].
55
//!
66
//! A dedicated supervisor initialises the process-wide context once via
7-
//! [`set_ctx`]. A process hosting several supervisor sessions instead binds
8-
//! each session's context to the threads of that session's runtime with
7+
//! [`set_ctx`]. A process hosting several sandbox instances instead binds
8+
//! each instance's context to the threads of that instance's runtime with
99
//! [`bind_thread_ctx`]; a bound context takes precedence. [`ctx`] falls back
1010
//! to a default context when neither is set (e.g. unit tests that exercise
1111
//! builders without booting the sandbox).
@@ -28,7 +28,7 @@ static OCSF_CTX_FALLBACK: LazyLock<EventContext> = LazyLock::new(|| EventContext
2828
});
2929

3030
std::thread_local! {
31-
/// Context of the hosted session whose runtime owns this thread.
31+
/// Context of the hosted sandbox instance whose runtime owns this thread.
3232
static THREAD_CTX: RefCell<Option<Arc<EventContext>>> = const { RefCell::new(None) };
3333
}
3434

@@ -40,16 +40,17 @@ pub fn set_ctx(ctx: EventContext) -> bool {
4040
OCSF_CTX.set(ctx).is_ok()
4141
}
4242

43-
/// Attribute events emitted on the calling thread to one hosted session.
43+
/// Attribute events emitted on the calling thread to one hosted sandbox
44+
/// instance.
4445
///
45-
/// Session runtimes bind every thread they start; `None` unbinds it.
46+
/// Instance runtimes bind every thread they start; `None` unbinds it.
4647
pub fn bind_thread_ctx(ctx: Option<Arc<EventContext>>) {
4748
THREAD_CTX.with(|current| *current.borrow_mut() = ctx);
4849
}
4950

5051
/// Return the context for events emitted on the calling thread.
5152
///
52-
/// A session-bound context wins over the process-wide one. Falls back to a
53+
/// An instance-bound context wins over the process-wide one. Falls back to a
5354
/// default context if neither is set.
5455
#[must_use]
5556
pub fn ctx() -> ContextRef<'static> {

‎crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ use tracing_subscriber::layer::Context;
1616
use crate::format::downgrade::downgrade_event;
1717
use crate::tracing_layers::event_bridge::{OCSF_TARGET, clone_current_event};
1818

19-
/// OCSF JSON output settings owned by one hosted supervisor session.
19+
/// OCSF JSON output settings owned by one hosted sandbox instance.
2020
///
21-
/// Sessions apply their own `ocsf_json_enabled` and schema-version settings
22-
/// to these values. Binding them to the session's runtime threads makes every
21+
/// Instances apply their own `ocsf_json_enabled` and schema-version settings
22+
/// to these values. Binding them to the instance's runtime threads makes every
2323
/// [`OcsfJsonlLayer`] use them for events those threads emit.
2424
#[derive(Clone, Debug, Default)]
2525
pub struct OcsfJsonSettings {
@@ -32,7 +32,8 @@ std::thread_local! {
3232
const { std::cell::RefCell::new(None) };
3333
}
3434

35-
/// Apply one hosted session's JSON settings to events emitted on this thread.
35+
/// Apply one hosted sandbox instance's JSON settings to events emitted on this
36+
/// thread.
3637
pub fn bind_thread_json_settings(settings: Option<OcsfJsonSettings>) {
3738
THREAD_JSON_SETTINGS.with(|current| *current.borrow_mut() = settings);
3839
}
@@ -111,9 +112,9 @@ where
111112
return;
112113
}
113114

114-
// A hosted session's settings replace the process-wide ones.
115-
let session = THREAD_JSON_SETTINGS.with(|current| current.borrow().clone());
116-
let (enabled, target_version) = session.as_ref().map_or(
115+
// A hosted sandbox instance's settings replace the process-wide ones.
116+
let instance = THREAD_JSON_SETTINGS.with(|current| current.borrow().clone());
117+
let (enabled, target_version) = instance.as_ref().map_or(
117118
(self.enabled.as_ref(), self.target_version.as_ref()),
118119
|settings| (Some(&settings.enabled), Some(&settings.target_version)),
119120
);

‎crates/openshell-otel/src/propagation.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ pub fn set_parent_from_environment(span: &tracing::Span) {
124124
/// Parent `span` under an explicit W3C `traceparent` and `tracestate`.
125125
///
126126
/// A process supervising several sandboxes receives each launch's context
127-
/// with its session instead of through process environment. Leaves `span`
128-
/// unchanged when the values carry no valid context.
127+
/// with its sandbox instance instead of through process environment. Leaves
128+
/// `span` unchanged when the values carry no valid context.
129129
pub fn set_parent_from_trace_context(
130130
span: &tracing::Span,
131131
traceparent: Option<&str>,

‎crates/openshell-supervisor-network/README.md‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ default features, so this instrumentation does not
1515
require the compute runtime or Unix-only sandbox library. It is a development
1616
dependency only.
1717

18-
## Session inputs
18+
## Sandbox instance inputs
1919

20-
`run_networking` takes a `NetworkSession` with the sandbox identity, the
21-
session's `GatewayEndpoint`, the SPIFFE Workload API socket for provider token
22-
grants, the `policy.local` denial source, and proxy CA files. None of these
23-
are read from the process environment, so one process can run several
24-
sessions. Each session owns its provider token cache and HTTP client; a token
25-
acquired for one sandbox is never served to another. Without a Workload API
26-
socket, token grants fail closed. A process hosting several sessions serves
27-
`/v1/denials` from a per-session `SessionDenialLog` instead of the shared
28-
shorthand log files.
20+
`run_networking` takes a `SandboxInstanceNetwork` with the sandbox identity,
21+
the sandbox instance's `GatewayEndpoint`, the SPIFFE Workload API socket for
22+
provider token grants, the `policy.local` denial source, and proxy CA files.
23+
None of these are read from the process environment, so one process can run
24+
several sandbox instances. Each instance owns its provider token cache and
25+
HTTP client; a token acquired for one sandbox is never served to another.
26+
Without a Workload API socket, token grants fail closed. A process hosting
27+
several instances serves `/v1/denials` from a per-instance
28+
`SandboxInstanceDenialLog` instead of the shared shorthand log files.

‎crates/openshell-supervisor-network/src/l7/token_grant_injection.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ pub trait TokenGrantResolver: Send + Sync {
5353
) -> Pin<Box<dyn Future<Output = Result<String, TokenGrantFailure>> + Send + 'a>>;
5454
}
5555

56-
/// Resolves provider tokens through one session's authenticated grant flow.
56+
/// Resolves provider tokens through one sandbox instance's authenticated grant flow.
5757
pub struct SpiffeTokenGrantResolver {
58-
grants: crate::token_grant::SessionTokenGrants,
58+
grants: crate::token_grant::SandboxInstanceTokenGrants,
5959
}
6060

6161
impl SpiffeTokenGrantResolver {
6262
#[must_use]
63-
pub fn new(grants: crate::token_grant::SessionTokenGrants) -> Self {
63+
pub fn new(grants: crate::token_grant::SandboxInstanceTokenGrants) -> Self {
6464
Self { grants }
6565
}
6666
}

‎crates/openshell-supervisor-network/src/policy_local.rs‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,18 @@ const SHORTHAND_LOG_PREFIX: &str = "openshell.";
8484
/// log entry (very long URL path, etc.) cannot blow up the response.
8585
const MAX_DENIAL_LINE_BYTES: usize = 4096;
8686

87-
/// Most recent denial lines retained for one hosted session.
88-
const MAX_SESSION_DENIAL_LINES: usize = 256;
87+
/// Most recent denial lines retained for one hosted sandbox instance.
88+
const MAX_INSTANCE_DENIAL_LINES: usize = 256;
8989

9090
/// Where `/v1/denials` reads this sandbox's recent denials.
9191
#[derive(Clone, Debug)]
9292
pub enum DenialLogSource {
9393
/// Shorthand log files written by a supervisor process that serves one
9494
/// sandbox.
9595
ShorthandFiles(PathBuf),
96-
/// Denials recorded for this session alone. A process hosting several
96+
/// Denials recorded for this sandbox instance alone. A process hosting several
9797
/// sandboxes cannot expose its shared log files to any one of them.
98-
Session(SessionDenialLog),
98+
SandboxInstance(SandboxInstanceDenialLog),
9999
}
100100

101101
impl Default for DenialLogSource {
@@ -104,27 +104,27 @@ impl Default for DenialLogSource {
104104
}
105105
}
106106

107-
/// Bounded recent OCSF denial lines recorded for one hosted session.
107+
/// Bounded recent OCSF denial lines recorded for one hosted sandbox instance.
108108
#[derive(Clone, Default)]
109-
pub struct SessionDenialLog {
109+
pub struct SandboxInstanceDenialLog {
110110
lines: Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
111111
}
112112

113-
impl std::fmt::Debug for SessionDenialLog {
113+
impl std::fmt::Debug for SandboxInstanceDenialLog {
114114
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115115
let retained = self
116116
.lines
117117
.lock()
118118
.unwrap_or_else(std::sync::PoisonError::into_inner)
119119
.len();
120120
formatter
121-
.debug_struct("SessionDenialLog")
121+
.debug_struct("SandboxInstanceDenialLog")
122122
.field("retained", &retained)
123123
.finish()
124124
}
125125
}
126126

127-
impl SessionDenialLog {
127+
impl SandboxInstanceDenialLog {
128128
/// Retain `line` when it is a rendered OCSF denial; ignore anything else.
129129
pub fn record(&self, line: &str) {
130130
let line = line.trim_end();
@@ -135,7 +135,7 @@ impl SessionDenialLog {
135135
.lines
136136
.lock()
137137
.unwrap_or_else(std::sync::PoisonError::into_inner);
138-
if lines.len() == MAX_SESSION_DENIAL_LINES {
138+
if lines.len() == MAX_INSTANCE_DENIAL_LINES {
139139
lines.pop_front();
140140
}
141141
lines.push_back(line.to_string());
@@ -227,7 +227,7 @@ impl PolicyLocalContext {
227227
self
228228
}
229229

230-
/// The session's provider token grant resolver, absent when grants are
230+
/// The sandbox instance's provider token grant resolver, absent when grants are
231231
/// unavailable and must fail closed.
232232
pub(crate) fn token_grant_resolver(&self) -> Option<Arc<dyn TokenGrantResolver>> {
233233
self.token_grants.clone()
@@ -425,7 +425,7 @@ async fn recent_denials_response(
425425
let limit = parse_last_query(query).unwrap_or(DEFAULT_DENIALS_LIMIT);
426426
let log_dir = match &ctx.denials {
427427
DenialLogSource::ShorthandFiles(log_dir) => log_dir.clone(),
428-
DenialLogSource::Session(log) => {
428+
DenialLogSource::SandboxInstance(log) => {
429429
return (
430430
200,
431431
serde_json::json!({

0 commit comments

Comments
 (0)