mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-11-07 08:55:37 +08:00
feat(isolation): publish backend protocol contracts independently of runtime
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
@@ -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(®istration.name)?;
|
||||
if !names.insert(®istration.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(®istration.name)?;
|
||||
registration.endpoint.validate()?;
|
||||
let endpoint = match ®istration.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(®istration.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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user