feat(isolation): publish backend protocol contracts independently of runtime

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
Drew Newberry
2026-10-09 08:36:31 -07:00
parent fe2dd3e9d0
commit 1a65cdc9fa
16 changed files with 1158 additions and 21 deletions
+7
View File
@@ -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
@@ -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;
@@ -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<SocketAddr>,
},
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(&registration.name)?;
if !names.insert(&registration.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<Self, Self::Error> {
use crate::proto::compute::v1::{
IsolationBackendTcpEndpoint, IsolationBackendVsockEndpoint,
isolation_backend_registration::Endpoint,
};
validate_backend_name(&registration.name)?;
registration.endpoint.validate()?;
let endpoint = match &registration.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<Self, Self::Error> {
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::<Result<_, _>>()?,
},
Endpoint::Vsock(vsock) => SandboxTransport::Vsock {
guest_cid: vsock.guest_cid,
port: vsock.port,
},
};
validate_backend_name(&registration.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::<BackendRegistration>(&json).unwrap(),
original
);
}
}
}
+1
View File
@@ -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;
+9
View File
@@ -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,
@@ -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(),
@@ -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<std::net::SocketAddr>,
},
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<u8> = (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 {
+1 -1
View File
@@ -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";
@@ -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<DriverSandboxSpec, Box<Status>> {
Ok(DriverSandboxSpec {
isolation_backend: None,
log_level: spec.log_level.clone(),
environment: spec.environment.clone(),
template: spec
+37 -1
View File
@@ -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!(
+8
View File
@@ -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.
+36
View File
@@ -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 {
+126
View File
@@ -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;
}
+26
View File
@@ -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;
}
}
+375
View File
@@ -0,0 +1,375 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->
# 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.<name>]` 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 <sandbox-token>`. 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:<gateway ID>"`,
`sub: "spiffe://openshell/sandbox/<sandbox ID>"`, `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": "<SHA-256 described below>",
"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":"<same 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<string,string>` | `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<string,string>`, `provider_files: map<string,string>` (default `{}`) | `started`: `process_id: string`, `provider_env_revision: u64`, `provider_env_generation: u64` |
| `update_provider_environment` | `generation: u64`, `revision: u64`, `provider_env: map<string,string>`, `provider_files: map<string,string>` (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<string,string>`, `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.
+282
View File
@@ -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"
}
]
}