Skip to content
Open
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
1 change: 1 addition & 0 deletions crates/openshell-core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut proto_config = tonic_prost_build::Config::new();
// Keep optional grant evidence off sandbox lifecycle futures' stacks.
proto_config.boxed(".openshell.v1.SandboxStatus.last_token_grant_failure");
proto_config.boxed(".openshell.v1.SandboxStatus.launch_policy");
let proto_include = if use_vendored_protoc {
proto_config.protoc_executable(protoc_bin_vendored::protoc_bin_path()?);
Some(protoc_bin_vendored::include_path()?)
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-core/src/extension_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub const COMPUTE_SUSPEND_RESUME: &str = "openshell.compute.suspend-resume";
/// Optional stopped-state workload configuration updates retaining the resource and storage.
pub const COMPUTE_UPDATE_SANDBOX_CONFIGURATION: &str =
"openshell.compute.update-sandbox-configuration";
/// Immutable policy input bound to each compute launch generation.
pub const COMPUTE_LAUNCH_POLICY: &str = "openshell.compute.launch-policy";

const MAX_IMPLEMENTATION_NAME_BYTES: usize = 128;
const MAX_IMPLEMENTATION_VERSION_BYTES: usize = 128;
Expand Down Expand Up @@ -133,6 +135,7 @@ pub fn gateway_metadata(family: ExtensionFamily) -> PeerMetadata {
supported_capabilities.push(COMPUTE_LAUNCH_AUTHENTICATION.to_string());
supported_capabilities.push(COMPUTE_ISOLATION_BACKEND.to_string());
supported_capabilities.push(COMPUTE_UPDATE_SANDBOX_CONFIGURATION.to_string());
supported_capabilities.push(COMPUTE_LAUNCH_POLICY.to_string());
}
PeerMetadata {
protocol_version: Some(ProtocolVersion {
Expand Down
74 changes: 74 additions & 0 deletions crates/openshell-core/src/launch_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Compute launch input identity. Policy safety remains the admission owner's duty.

use crate::policy_identity::deterministic_policy_hash;
use crate::proto::SandboxLaunchPolicy;
use prost::Message;

/// Check the complete bounded input before a driver changes retained resources.
/// This does not authorize a policy or replace backend-specific admission.
pub fn validate_launch_policy(
input: &SandboxLaunchPolicy,
generation: &str,
) -> Result<(), &'static str> {
if crate::sandbox_generation::SandboxGenerationId::parse(generation).is_err()
|| input.generation_id != generation
|| input.generation_id.is_empty()
{
return Err("launch policy generation does not match the compute launch");
}
let policy = input
.policy
.as_ref()
.ok_or("launch policy document is missing")?;
if policy.encoded_len() > 262_144 {
return Err("launch policy exceeds the policy size limit");
}
if deterministic_policy_hash(policy) != input.policy_hash {
return Err("launch policy hash does not match its document");
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn launch_input_requires_matching_generation_document_hash_and_size() {
let generation = uuid::Uuid::new_v4().to_string();
let policy = crate::proto::SandboxPolicy::default();
let valid = SandboxLaunchPolicy {
generation_id: generation.clone(),
policy_hash: deterministic_policy_hash(&policy),
policy: Some(policy),
};
assert!(validate_launch_policy(&valid, &generation).is_ok());
for mutation in 0..5 {
let mut input = valid.clone();
match mutation {
0 => input.generation_id = uuid::Uuid::new_v4().to_string(),
1 => input.policy = None,
2 => input.policy_hash = "wrong".into(),
3 => {
let policy = input.policy.as_mut().unwrap();
policy.filesystem = Some(crate::proto::FilesystemPolicy {
read_write: vec!["/".repeat(262_145)],
..Default::default()
});
input.policy_hash = deterministic_policy_hash(policy);
}
_ => input.generation_id.clear(),
}
assert!(
validate_launch_policy(&input, &generation).is_err(),
"mutation {mutation}"
);
}
let mut invalid = valid;
invalid.generation_id = "invalid/generation".into();
assert!(validate_launch_policy(&invalid, "invalid/generation").is_err());
}
}
1 change: 1 addition & 0 deletions crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod host_pattern;
pub mod image;
pub mod isolation_registration;
pub mod jwt;
pub mod launch_policy;
pub mod local_api_socket;
pub mod mcp;
pub mod metadata;
Expand Down
8 changes: 8 additions & 0 deletions crates/openshell-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,11 @@ These helpers use the raw client's authentication slot. They do not perform OIDC
- Async-only. Tonic is async-native; callers needing a blocking call can wrap
with their own runtime.
- The curated surface will grow as more RPCs graduate from `raw`.

## Next minor API migration

This source-breaking API change targets the next minor release and must not be backported into the `0.1.x` patch series.

Raw `SandboxStatus` adds optional `launch_policy`, exposing the retained generation, policy document and digest under existing policy-read authorization. Provider credential values and environment data are not included. Complete generated Rust status literals must add `launch_policy: None` or use `..Default::default()`.

Legacy wire payloads retain their field identities and decode the new field as absent. Upgrade gateway writers together because older writers can drop retained launch state. A negotiated launch without retained input fails closed. Stop and explicitly start the sandbox to claim a new generation; retries and recovery do not replace an already claimed generation's input. Frozen sandboxes keep their original accepted policy.
13 changes: 9 additions & 4 deletions crates/openshell-sdk/tests/sandbox_status_compatibility.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Compile-time source-compatibility fixture for the v0.1.5 public status shape.
//! Exhaustive raw status fixture for the documented next-minor API transition.
//!
//! This intentionally advances the v0.1.5 source baseline. Legacy wire payloads
//! still decode; complete raw Rust literals need the new optional field.
//! See the SDK README migration notice. Do not backport into a 0.1.x patch train.

use openshell_sdk::raw::proto::SandboxStatus;

#[test]
fn baseline_exhaustive_sandbox_status_literal_still_compiles() {
// Intentionally no `..Default::default()`: adding even an optional public
// field breaks existing downstream Rust exhaustive struct literals.
fn next_minor_exhaustive_sandbox_status_literal_compiles() {
// Intentionally no `..Default::default()`: future shape changes must be
// reviewed against this declared minor baseline, never silently accepted.
let status = SandboxStatus {
agent_pod: String::new(),
agent_fd: String::new(),
Expand All @@ -26,6 +30,7 @@ fn baseline_exhaustive_sandbox_status_literal_still_compiles() {
next_restart_time: None,
main_process_started_time: None,
last_token_grant_failure: None,
launch_policy: None,
};
assert_eq!(status, SandboxStatus::default());
}
Loading
Loading