This is the Phase 0 product contract for the Daytona gap plan. It defines the stable runtime surface that the in-process Rust SDK, future localhost API server, guest daemon transport, and generated SDKs must share.
The contract is intentionally transport-neutral. A direct Rust implementation, HTTP/gRPC server, or guest-daemon bridge should expose the same resources, state names, and error categories.
Fields:
id: stable sandbox identifier.state:created,running,stopped,deleting,error, orunknown.state_dir: local state directory when the runtime is local.image: image reference and resolved image path.cpus,memory_mib,memory_max_mib,memory_min_mib.network_backend:none,passt,gvproxy, orgvproxy-auto.mounts: declared workspace/shared mounts with tag, host path, guest path, and read-only flag.resources: current reservation and optional live RSS when available.
Operations:
create(config) -> Sandboxstart(id) -> Sandboxstop(id) -> Sandboxdelete(id) -> Emptyinspect(id) -> Sandboxlist() -> [Sandbox]
Fields:
id: stable process identifier within a sandbox.argv,cwd,env.state:starting,running,exited,cancelled, orerror.exit: exit code and success flag when complete.
Operations:
process.start(sandbox_id, command) -> Processprocess.stdout(process_id) -> stream<String>process.stderr(process_id) -> stream<String>process.wait(process_id) -> ExitStatusprocess.cancel(process_id) -> Empty
All paths are guest paths rooted at allowed mounts. Implementations may translate paths to host paths only when the runtime executes commands on the host for tests.
Operations:
fs.read(sandbox_id, path) -> bytesfs.write(sandbox_id, path, bytes, mode=create|overwrite|append) -> Emptyfs.stat(sandbox_id, path) -> FileStatfs.list(sandbox_id, path) -> [FileStat]fs.mkdir(sandbox_id, path, parents: bool) -> Emptyfs.delete(sandbox_id, path, recursive: bool) -> Empty
The current in-process Rust SDK covers this surface through exec; the guest
daemon/API server should promote these to first-class operations.
Operations run inside the sandbox using the guest git binary:
git.clone(sandbox_id, url, path, ref?) -> Emptygit.status(sandbox_id, path) -> GitStatusgit.diff(sandbox_id, path, base?) -> Stringgit.commit(sandbox_id, path, message) -> Stringgit.branch(sandbox_id, path, name) -> Emptygit.checkout(sandbox_id, path, ref) -> Emptygit.pull(sandbox_id, path) -> Emptygit.push(sandbox_id, path, remote?, branch?) -> Empty
Operations:
terminal.open(sandbox_id, cwd?, rows?, cols?) -> TerminalSessionterminal.input(session_id, bytes) -> Emptyterminal.output(session_id) -> stream<bytes>terminal.resize(session_id, rows, cols) -> Emptyterminal.close(session_id) -> Empty
Operations:
port.forward(sandbox_id, guest_port, host_port?) -> PortForwardport.list(sandbox_id) -> [PortForward]port.close(sandbox_id, forward_id) -> Empty
Operations:
snapshot.create(sandbox_id, options) -> Snapshotsnapshot.list() -> [Snapshot]snapshot.inspect(id) -> Snapshotsnapshot.restore(id, options) -> Sandboxsnapshot.delete(id) -> Emptysnapshot.prune(delete: bool) -> SnapshotPruneReport
Snapshot kinds are disk-only and full.
Every implementation should map errors to one of:
invalid_argument: malformed input or unsupported option.not_found: sandbox, snapshot, process, file, or forward does not exist.resource_exhausted: host admission budget or quota denied the request.failed_precondition: valid request against the wrong state.unavailable: runner, guest daemon, or transport is unreachable.internal: bug or unexpected runtime failure.io: local filesystem or OS error.
Error payloads should include a human-readable message and, when applicable, the path, sandbox id, process id, or snapshot id that caused the failure.
The localhost API server (tb-sandbox server, src/api.rs) is the first
network transport for this contract. It is JSON over HTTP/1.1, localhost-first,
and authenticated with a bearer token (Authorization: Bearer <token>);
GET /health is unauthenticated.
Route map (all under /v1 unless noted):
GET /health->{status, version}GET /sandboxes->{sandboxes: [{id, state, state_dir}]}POST /sandboxes(body{image, name?, cpus?, memory_mib?, memory_max_mib?, memory_min_mib?, network_backend?, mounts?}) -> SandboxGET /sandboxes/{id}-> Sandbox withmountsPOST /sandboxes/{id}/start|/stop-> SandboxDELETE /sandboxes/{id}->{}POST /sandboxes/{id}/exec(body{argv, cwd?, env?}) -> chunkedapplication/x-ndjsonstream of events:{type:"started",process_id},{type:"stdout",data},{type:"stderr",data},{type:"exit",code,success}.POST /sandboxes/{id}/cancel(body{process_id}) ->{}GET /sandboxes/{id}/fs?path=->{path, content}(text-oriented)PUT /sandboxes/{id}/fs?path=&mode=overwrite|append(raw body) ->{path, bytes}GET /sandboxes/{id}/git/status?path=->{branch, clean, entries:[{status,path}]}GET /sandboxes/{id}/git/diff?path=&base=->{diff}POST /sandboxes/{id}/git/clone(body{url, path, ref?}) ->{path}POST /sandboxes/{id}/ports(body{guest_port}) ->{host, port, guest_port}GET /events-> chunked stream of structured lifecycle/process events.
Errors are returned as {"error":{"category","message"}} with the HTTP status
derived from the category (invalid_argument->400, not_found->404,
resource_exhausted->429, failed_precondition->409, unavailable->503,
internal/io->500). The CLI's API mode (src/client.rs) maps these back into
the same Error categories so local and API behavior match.
The first conformance target is the in-process Rust SDK. The test named
sdk::tests::api_contract_conformance_local_process_runtime exercises the
minimum Phase 0 surface available today: lifecycle, mount inspection, exec with
stdout/stderr/exit, file read/write through exec, cancellation, and deletion.
The HTTP transport runs the same behavioral suite through the real server and
client in api::tests::api_contract_conformance_over_http: it boots the server
over an in-process LocalProcessSandboxRuntime, then drives create, start,
inspect (mounts + state), list, streaming exec (stdout/stderr/exit + file
write), filesystem read/write, live cancellation, and delete-then-not-found over
HTTP — plus auth rejection. api::tests::git_status_and_diff_over_http covers
the Git surface (status/diff over a real repo). Future guest-daemon and runner
transports should run this suite against their transport rather than inventing
parallel expectations.