Define a minimal, functional control API for creating and managing Cleanroom sandboxes with durable execution streaming and interactive execution bootstrap.
This document is intentionally small-scope:
- management plane for sandbox lifecycle
- execution plane for command runs and streaming output
Sandbox: A policy-bound execution environment created from an immutableCompiledPolicy.Execution: A single command invocation inside a sandbox.Event: Structured runtime and policy events emitted during sandbox/execution lifecycle.
sandbox is the top-level lifecycle noun. execution_id is the canonical identifier for a command invocation.
Use ConnectRPC with HTTP/2 enabled in server deployments.
Why:
- unary RPCs for lifecycle operations
- server-streaming for durable batch execution output and event tails
- unary interactive bootstrap via
AttachExecution, with QUIC carrying interactive PTY bytes and control frames after bootstrap
Non-goal for v1:
- REST endpoint support.
- ConnectRPC bidirectional streaming for interactive terminal traffic.
Cleanroom uses a client/server architecture for all user-facing operations.
cleanroomCLI is always a client.- The server process is authoritative for sandbox lifecycle and execution state.
- "Local" behavior means "client talking to local server endpoint", not a separate direct execution path.
Use a single binary in v1:
- primary executable:
cleanroom - server mode:
cleanroom serve
This keeps one code path while still supporting systemd/launchd unit ergonomics via a single executable.
Default endpoint resolution:
- macOS: user socket
unix://$XDG_RUNTIME_DIR/cleanroom/cleanroom.sock - Linux: system socket when present
unix:///var/run/cleanroom/cleanroom.sock, otherwise user socket
Fallbacks:
--hostflagCLEANROOM_HOST- runtime config
control_host - default endpoint resolution
HTTP endpoints are also supported:
http://host:portfor direct HTTPhttps://host:portfor TLS transport
Two services are sufficient.
SandboxServiceExecutionService
CreateSandbox(CreateSandboxRequest) returns (CreateSandboxResponse)(unary)GetSandbox(GetSandboxRequest) returns (GetSandboxResponse)(unary)ListSandboxes(ListSandboxesRequest) returns (ListSandboxesResponse)(unary)DownloadSandboxFile(DownloadSandboxFileRequest) returns (DownloadSandboxFileResponse)(unary)TerminateSandbox(TerminateSandboxRequest) returns (TerminateSandboxResponse)(unary)StreamSandboxEvents(StreamSandboxEventsRequest) returns (stream SandboxEvent)(server-streaming)
CreateExecution(CreateExecutionRequest) returns (CreateExecutionResponse)(unary)AttachExecution(AttachExecutionRequest) returns (AttachExecutionResponse)(unary)GetExecution(GetExecutionRequest) returns (GetExecutionResponse)(unary)InspectExecution(InspectExecutionRequest) returns (InspectExecutionResponse)(unary)CancelExecution(CancelExecutionRequest) returns (CancelExecutionResponse)(unary)WriteExecutionStdin(WriteExecutionStdinRequest) returns (WriteExecutionStdinResponse)(unary)CloseExecutionStdin(CloseExecutionStdinRequest) returns (CloseExecutionStdinResponse)(unary)StreamExecution(StreamExecutionRequest) returns (stream ExecutionStreamEvent)(server-streaming)
AttachExecution is a bootstrap RPC for interactive executions. It returns a session token plus QUIC connection parameters. Interactive PTY bytes and control frames do not flow through ConnectRPC after bootstrap.
StreamExecution is the durable batch-oriented output stream. InspectExecution is the richer diagnostics surface for retained stdout/stderr, message, image metadata, artifacts dir, and observability payload.
SANDBOX_STATUS_PROVISIONINGSANDBOX_STATUS_READYSANDBOX_STATUS_STOPPINGSANDBOX_STATUS_STOPPEDSANDBOX_STATUS_FAILED
EXECUTION_STATUS_QUEUEDEXECUTION_STATUS_RUNNINGEXECUTION_STATUS_SUCCEEDEDEXECUTION_STATUS_FAILEDEXECUTION_STATUS_CANCELEDEXECUTION_STATUS_TIMED_OUT
CreateSandboxcompiles policy once and persists:compiled_policypolicy_hashbackend
- Active sandbox policy is immutable for sandbox lifetime.
- Backend capability validation occurs before provisioning.
- Launch fails closed on capability mismatch.
- Secret values are never returned by API responses/streams.
These rules align with SPEC.md requirements for policy immutability and capability gating.
All RPC errors must include:
- stable machine code (
error.code) - human-readable message (
error.message) - optional details (
error.details)
Minimum v1 codes:
policy_invalidpolicy_conflictbackend_unavailablebackend_capability_mismatchhost_not_allowedregistry_not_allowedlockfile_violationsecret_scope_violationruntime_launch_failed
For ConnectRPC, map these into typed error details and a stable application code field.
syntax = "proto3";
package cleanroom.v1;
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
service SandboxService {
rpc CreateSandbox(CreateSandboxRequest) returns (CreateSandboxResponse);
rpc GetSandbox(GetSandboxRequest) returns (GetSandboxResponse);
rpc ListSandboxes(ListSandboxesRequest) returns (ListSandboxesResponse);
rpc DownloadSandboxFile(DownloadSandboxFileRequest) returns (DownloadSandboxFileResponse);
rpc TerminateSandbox(TerminateSandboxRequest) returns (TerminateSandboxResponse);
rpc StreamSandboxEvents(StreamSandboxEventsRequest) returns (stream SandboxEvent);
}
service ExecutionService {
rpc CreateExecution(CreateExecutionRequest) returns (CreateExecutionResponse);
rpc AttachExecution(AttachExecutionRequest) returns (AttachExecutionResponse);
rpc GetExecution(GetExecutionRequest) returns (GetExecutionResponse);
rpc InspectExecution(InspectExecutionRequest) returns (InspectExecutionResponse);
rpc CancelExecution(CancelExecutionRequest) returns (CancelExecutionResponse);
rpc WriteExecutionStdin(WriteExecutionStdinRequest) returns (WriteExecutionStdinResponse);
rpc CloseExecutionStdin(CloseExecutionStdinRequest) returns (CloseExecutionStdinResponse);
rpc StreamExecution(StreamExecutionRequest) returns (stream ExecutionStreamEvent);
}
message Sandbox {
string sandbox_id = 1;
SandboxStatus status = 2;
string backend = 3;
string policy_hash = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
string last_execution_id = 7;
string active_execution_id = 8;
}
enum SandboxStatus {
SANDBOX_STATUS_UNSPECIFIED = 0;
SANDBOX_STATUS_PROVISIONING = 1;
SANDBOX_STATUS_READY = 2;
SANDBOX_STATUS_STOPPING = 3;
SANDBOX_STATUS_STOPPED = 4;
SANDBOX_STATUS_FAILED = 5;
}
message Execution {
string execution_id = 1;
string sandbox_id = 2;
ExecutionStatus status = 3;
repeated string command = 4;
int32 exit_code = 5;
google.protobuf.Timestamp started_at = 6;
google.protobuf.Timestamp finished_at = 7;
bool tty = 8;
ExecutionKind kind = 10;
}
message AttachExecutionRequest {
string sandbox_id = 1;
string execution_id = 2;
uint32 initial_cols = 3;
uint32 initial_rows = 4;
}
message AttachExecutionResponse {
string session_id = 1;
string session_token = 2;
google.protobuf.Timestamp expires_at = 3;
string quic_endpoint = 4;
string alpn = 5;
string server_cert_pin_sha256 = 6;
}
message InspectExecutionRequest {
string sandbox_id = 1;
string execution_id = 2;
}
message InspectExecutionResponse {
Execution execution = 1;
string message = 2;
string stdout = 3;
string stderr = 4;
string image_ref = 5;
string image_digest = 6;
string artifacts_dir = 7;
string plan_path = 8;
bool launched_vm = 9;
google.protobuf.Struct observability = 10;
string trace_id = 11;
string trace_url = 12;
}
enum ExecutionStatus {
EXECUTION_STATUS_UNSPECIFIED = 0;
EXECUTION_STATUS_QUEUED = 1;
EXECUTION_STATUS_RUNNING = 2;
EXECUTION_STATUS_SUCCEEDED = 3;
EXECUTION_STATUS_FAILED = 4;
EXECUTION_STATUS_CANCELED = 5;
EXECUTION_STATUS_TIMED_OUT = 6;
}
enum ExecutionKind {
EXECUTION_KIND_UNSPECIFIED = 0;
EXECUTION_KIND_BATCH = 1;
EXECUTION_KIND_INTERACTIVE = 2;
}Expose a server mode and API-driven commands in the same binary.
cleanroom serve --listen unix:///run/user/1000/cleanroom/cleanroom.sockcleanroom serve --listen http://127.0.0.1:7777cleanroom serve --listen https://127.0.0.1:7777 --tls-cert /path/to/server.pem --tls-key /path/to/server.key
cleanroom sandbox createcleanroom sandbox create --from <snapshot-id>cleanroom sandbox inspect <sandbox-id>cleanroom sandbox lscleanroom sandbox rm <sandbox-id>
sandbox inspect is the primary way to discover a sandbox's last_execution_id and active_execution_id.
cleanroom sandbox create is repo-agnostic. Without --from, it does not
inspect the local git repository or read cleanroom.yaml; it synthesizes a
built-in policy from the selected image ref, uses deny-by-default networking by
default, enables the guest Docker service with --docker, and disables egress
filtering only when --dangerously-allow-all is set. --image, --docker,
and --dangerously-allow-all cannot be combined with --from.
cleanroom inspect <typeid>cleanroom execution inspect <execution-id>cleanroom execution inspect --sandbox-id <sandbox-id> --lastcleanroom status --execution-id <execution-id>cleanroom status --last
Notes:
sandbox inspectis the lookup surface when you only know a sandbox ID.execution inspectis the live control-plane inspection surface and includes trace metadata when available.statusis the local retained-artifacts browser under$XDG_STATE_HOME/cleanroom/executions.- There are no first-class low-level
execution create/get/cancel/streamCLI verbs; those are API/SDK operations.
cleanroom exec remains the primary developer UX, but it is implemented as client/server RPC.
Default command:
cleanroom exec -- npm test
Additional command forms:
cleanroom createcleanroom console -- bashcleanroom exec --in <sandbox-id> -- npm testcleanroom exec --from <snapshot-id> -- npm test
Behavior contract:
- Resolve server endpoint (
--host,CLEANROOM_HOST, context, default unix socket). - Select sandbox mode:
--in <id>reuses an existing sandbox--from <snapshot-id>creates a sandbox from a snapshot- otherwise create a sandbox from policy
- When creating a sandbox from policy, resolve and compile repository policy.
- If repo-aware bootstrap is enabled for that policy-created sandbox, resolve
the current git remote and committed
HEAD. - For repo-aware top-level commands, materialize that checkout in the sandbox
and default the guest working directory to
repository.path.- Cleanroom does not auto-detect or auto-wrap
mise; call it explicitly insandbox.dependencies.commandor the workload command when needed
- Cleanroom does not auto-detect or auto-wrap
- Create execution with explicit kind, env, and TTY options.
- Attach stdio according to command mode:
cleanroom execdefaults to attached stdin plus separate stdout/stderr usingStreamExecution,WriteExecutionStdin, andCloseExecutionStdincleanroom exec --env KEYinheritsKEYfrom the local clientcleanroom exec --env KEY=VALUEsends an explicit guest env assignmentcleanroom exec -ncloses stdin immediately so the command observes EOFcleanroom consoleusesAttachExecutionfor PTY-oriented interactive sessions- attached execution streams may emit warning events; the CLI renders them as warning notices on stderr
- Return the command exit code.
- Newly created
execandconsolesandboxes are terminated after the command unless--keepis set. Reused sandboxes (--in) remainREADY.
Notes:
cleanroom create,cleanroom exec, andcleanroom consoleare the repo-aware UX layer by default.cleanroom create --from <snapshot-id>follows the snapshot path and does not read repository policy.cleanroom sandbox createstays generic and does not inspect the local git repository or readcleanroom.yaml.- Dirty working trees warn and use committed
HEAD; uncommitted changes are not copied into the sandbox.
Signal behavior:
- First
Ctrl-C:CancelExecution. - Second
Ctrl-C: force detach client stream and return immediately.
Failure UX:
execandconsolefailures preserve streamed guest stdout/stderr and return the guest exit code without appending a diagnostic footer.cleanroom exec --print-sandbox-idremains the explicit opt-in for printing the resolved sandbox ID before streaming begins.cleanroom exec --print-trace-idremains the explicit opt-in for printingtrace_idafter a successful execution.- On policy/runtime deny, print stable reason code and a follow-up command to inspect sandbox or execution state.
Debugging flow:
- Start with
cleanroom status --lastwhen you only need the newest retained execution, orcleanroom sandbox inspect <sandbox-id>when you already know the sandbox ID. - Use
cleanroom execution inspect <execution-id>for the canonical execution view, includingtrace_id, optionaltrace_url, and retained observability payload. - Use
cleanroom status --execution-id <execution-id>for retained local artifacts and raw observability files.
- Define protobufs and generate ConnectRPC stubs.
- Implement
cleanroom servewith in-process adapter wiring. - Implement unary RPCs first (
Create/Get/List/Terminate,Create/Get/Cancel). - Add
StreamExecutionandStreamSandboxEvents. - Add
AttachExecution,InspectExecution, and stdin attach/close RPCs. - Implement
cleanroom execas an RPC wrapper (CreateSandbox->CreateExecution-> optional stdin attach/close -> stream -> optionalTerminateSandbox). - Wire remaining CLI subcommands to RPC client.
- Add conformance tests for status transitions, error codes, signal handling, and stream termination behavior.
- Artifact upload API
- Multi-tenant org/authz policy model
- Cross-sandbox workflow orchestration