Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
hex = "0.4"
ed25519-compact = { version = "2.3", default-features = false, features = ["std"] }
httpmock = { version = "0.7" }
mockall = { version = "0.15.0" }
hcl-rs = "0.19"
Expand Down
181 changes: 181 additions & 0 deletions crates/alien-bindings/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,185 @@ pub trait Container: Binding {
fn as_any(&self) -> &dyn std::any::Any;
}

/// A request to create a sandbox session.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionRequest {
/// Caller-chosen session id. Omitted means the provider allocates one.
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
/// Opaque tenant key. Never sent to a provider verbatim — the binding derives a
/// fixed-length identifier from it with a deployment-scoped HMAC.
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant_key: Option<String>,
/// Environment variables to place in the session.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub env: BTreeMap<String, String>,
}

/// A live sandbox session.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct SandboxSession {
/// Provider-scoped session identifier
pub session_id: String,
/// Current lifecycle state
pub state: SandboxSessionState,
/// Lifecycle generation. A capability from another generation is rejected, which is how
/// terminate revokes without distributing a revocation list.
pub generation: u64,
}

/// Lifecycle state of a sandbox session.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub enum SandboxSessionState {
/// Created but not yet able to run a command.
///
/// A real state, not a placeholder: a MicroVM takes seconds to reach `RUNNING`, and calling
/// that Running would tell a caller to send commands to something that cannot answer.
Starting,
/// Executing, consuming CPU and memory
Running,
/// Suspended with state preserved
Suspended,
/// Terminated; the id will not run again
Terminated,
}

/// A command to run inside a sandbox.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct RunCommandRequest {
/// Command and arguments
pub command: Vec<String>,
/// Working directory inside the sandbox
#[serde(skip_serializing_if = "Option::is_none")]
pub working_directory: Option<String>,
/// Environment overlaid on the session's own
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub env: BTreeMap<String, String>,
/// Wall-clock ceiling. Required — a defaulted deadline is a hang waiting for a slow day.
pub deadline: Duration,
}

/// One frame of a running command's output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CommandOutput {
/// Bytes written to stdout
#[serde(rename_all = "camelCase")]
Stdout {
/// Monotonic across both streams, so a caller can interleave them in production order
seq: u64,
/// Raw bytes; command output is not necessarily UTF-8
data: Vec<u8>,
},
/// Bytes written to stderr
#[serde(rename_all = "camelCase")]
Stderr {
/// Monotonic across both streams
seq: u64,
/// Raw bytes
data: Vec<u8>,
},
/// The command finished. Exactly one terminal frame is emitted, always last.
#[serde(rename_all = "camelCase")]
Exit {
/// Process exit code
code: i32,
/// Set when output was cut short by a bound rather than by the command finishing
#[serde(default)]
truncated: bool,
},
}

/// An authenticated, port-scoped capability to reach a service inside a sandbox.
///
/// Not a URL string: AWS needs a JWE and a port header, Azure an Entra token, and a bare
/// string cannot carry either. Returning one would push callers into building the request
/// themselves and getting the auth wrong.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct PreviewCapability {
/// Endpoint the request must be sent to
pub endpoint: String,
/// Headers that must accompany every request
pub headers: BTreeMap<String, String>,
/// Ports this capability admits. A request to any other port is refused upstream.
pub allowed_ports: Vec<u16>,
/// Seconds until the capability expires
pub expires_in_seconds: u64,
}

/// A sandbox binding: create sessions, run untrusted code in them, and tear them down.
///
/// Capabilities differ per platform. Call `capabilities()` and branch, or call and handle the
/// typed error — an unsupported capability is never a silent no-op.
#[async_trait]
pub trait Sandbox: Binding {
/// What this platform's backend supports.
fn capabilities(&self) -> alien_core::SandboxCapabilities;

/// Creates a session.
async fn create(&self, request: CreateSessionRequest) -> Result<SandboxSession>;

/// Fetches a session by id, or `None` if it does not exist.
///
/// Requires `reconnect`. A GCP session id is scoped to one Cloud Run instance, so GCP
/// returns the typed error rather than a `None` a caller would read as "expired".
async fn get(&self, session_id: &str) -> Result<Option<SandboxSession>>;

/// Fetches a session, creating it if absent.
async fn get_or_create(&self, request: CreateSessionRequest) -> Result<SandboxSession>;

/// Lists sessions belonging to this sandbox's parent.
async fn list(&self) -> Result<Vec<SandboxSession>>;

/// Runs a command, streaming output frames until exactly one terminal frame.
///
/// The stream carries backpressure: a consumer that stops reading stops the sandbox's
/// writer, rather than buffering without bound.
async fn run_command(
&self,
session_id: &str,
request: RunCommandRequest,
) -> Result<futures::stream::BoxStream<'static, Result<CommandOutput>>>;

/// Reads a file out of the sandbox. Paths are normalised and may not escape the root.
async fn read_file(&self, session_id: &str, path: &str) -> Result<Vec<u8>>;

/// Writes files into the sandbox.
async fn write_files(&self, session_id: &str, files: BTreeMap<String, Vec<u8>>) -> Result<()>;

/// Creates a directory inside the sandbox.
async fn mkdir(&self, session_id: &str, path: &str) -> Result<()>;

/// Mints a capability to reach a declared port. Requires `preview`.
async fn preview(&self, session_id: &str, port: u16) -> Result<PreviewCapability>;

/// Suspends a session, preserving state. Requires `suspendResume`.
async fn suspend(&self, session_id: &str) -> Result<()>;

/// Resumes a suspended session. Requires `suspendResume`.
async fn resume(&self, session_id: &str) -> Result<()>;

/// Captures full session state and returns its identifier. Requires `snapshot`.
async fn snapshot(&self, session_id: &str) -> Result<String>;

/// Terminates a session. Idempotent: terminating an absent session succeeds.
async fn terminate(&self, session_id: &str) -> Result<()>;

/// Get a reference to this object as `Any` for dynamic casting
fn as_any(&self) -> &dyn std::any::Any;
}

/// A provider must implement methods to load the various types of bindings
/// based on environment variables or other configuration sources.
#[async_trait]
Expand Down Expand Up @@ -988,6 +1167,8 @@ pub trait BindingsProviderApi: Send + Sync + std::fmt::Debug {
/// Given a binding identifier, builds a ServiceAccount implementation.
async fn load_service_account(&self, binding_name: &str) -> Result<Arc<dyn ServiceAccount>>;



/// Runtime-only binding env vars (a local Postgres connection with its password, a local
/// BYO-key AI binding) for the given resource — re-resolved on every (re)start so the secret
/// reaches the worker process but is never written to persisted worker metadata. The resource
Expand Down
8 changes: 8 additions & 0 deletions crates/alien-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ openapi = ["dep:utoipa"]
jsonschema = ["dep:schemars"]
test-utils = []
local = ["tokio/fs"]
# Signing and verifying sandbox capability tokens. Gated because alien-core is also built for
# targets that have no business carrying a crypto implementation.
sandbox-capability = ["dep:ed25519-compact"]

[dependencies]
serde = { workspace = true, features = ["derive"] }
Expand All @@ -31,6 +34,7 @@ async-trait = { workspace = true }
bytes = { workspace = true }
reqwest = { workspace = true }
base64 = { workspace = true }
ed25519-compact = { workspace = true, optional = true }
alien-macros = { workspace = true }
alien-error = { workspace = true, features = ["openapi"] }
thiserror = { workspace = true }
Expand All @@ -47,3 +51,7 @@ futures = { workspace = true }
name = "schema_exporter"
path = "src/bin/schema_exporter.rs"
required-features = ["clap", "openapi"]

# Killing a process group needs `kill(2)` with a negative pid, which std does not expose.
[target.'cfg(unix)'.dependencies]
libc = "0.2"
8 changes: 8 additions & 0 deletions crates/alien-core/src/bin/schema_exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ use utoipa::OpenApi;
AiOutputs,
Queue,
QueueOutputs,
Sandbox,
SandboxOutputs,
SandboxCode,
SandboxLimits,
SandboxEgress,
SandboxSessionPolicy,
SandboxCapabilities,
SandboxCapability,
Email,
EmailInbound,
EmailEvents,
Expand Down
5 changes: 5 additions & 0 deletions crates/alien-core/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod container_apps_environment;
mod kv;
mod postgres;
mod queue;
mod sandbox;
mod service_account;
mod storage;
mod vault;
Expand Down Expand Up @@ -52,6 +53,10 @@ pub use postgres::{
pub use queue::{
LocalQueueBinding, PubSubQueueBinding, QueueBinding, ServiceBusQueueBinding, SqsQueueBinding,
};
pub use sandbox::{
AwsSandboxBinding, AzureSandboxBinding, GcpSandboxBinding, KubernetesSandboxBinding,
LocalSandboxBinding, SandboxBinding,
};
pub use service_account::{
AwsServiceAccountBinding, AzureServiceAccountBinding, GcpServiceAccountBinding,
ServiceAccountBinding,
Expand Down
Loading
Loading