rath.backend#

Backend abstractions, sandbox handles, backend tool payloads, execution results, registry, and stream.

Source#

Module

Source

rath.backend.abc

src/rath/backend/abc.py

rath.backend.tool_types

src/rath/backend/tool_types.py

rath.backend.results

src/rath/backend/results.py

rath.backend.registry

src/rath/backend/registry.py

rath.backend.local

src/rath/backend/local.py

rath.backend.opensandbox

src/rath/backend/opensandbox.py

rath.backend.stream

src/rath/backend/stream.py

rath.backend.persistence

src/rath/backend/persistence/

Public contract#

Backend interface#

API

Returns

Description

Backend.is_available()

bool

Static availability check.

Backend.capabilities()

Capabilities

Backend class-level capabilities.

Backend.supported_calls()

frozenset[type[BackendTool]]

Supported payload types.

backend.open(spec=None)

BackendSandbox

Opens a sandbox handle with refcount == 0; bind it to a session or enter its context before holding it.

backend.close(sandbox)

None

Closes and releases resources.

backend.dispatch(sandbox, call)

ToolResult | bool

Executes the payload.

Sandbox handle lifecycle#

Refcounted sandbox lifecycle

Each Session.sandbox slot owns one sandbox reference; the backend closes the handle only after the final reference is released.#

API

Behavior

sandbox.refcount

Read-only live reference count.

sandbox.acquire()

Adds one reference; raises BackendSandboxClosed if closed.

sandbox.release()

Drops one reference; closes through the backend when the count reaches zero.

with sandbox:

Acquires on enter and releases on exit.

sandbox.dispatch(call)

Runs a backend payload unless the sandbox is closed.

sandbox.stream(buffer=0)

Creates a FIFO worker stream bound to this sandbox.

Every Session.sandbox slot owns one reference. Session.bind_sandbox(...), Session.require_sandbox(), run_session_loop(...), run_session_compress(...), fork(), and detach() all preserve this ownership rule. Refcount acquire/release is lock-protected in v1.2, and the backend close happens outside the lock to avoid double-close races.

Sandbox spec#

Field

Type

Description

image

str | None

Image name that the backend may use.

entrypoint

Sequence[str] | None

Entrypoint that the backend may use.

env

Mapping[str, str] | None

Sandbox environment variables.

timeout

timedelta | None

Sandbox lifetime or creation-timeout semantics.

working_dir

str | None

Local working directory or OpenSandbox host bind source.

Backend tool payloads#

Payload

Fields

Returns

BackendToolCommandRun

cmd, env, cwd, stdin, timeout

CommandResult or ToolExecutionFailure

BackendToolFilesRead

path, encoding

FileContent or ToolExecutionFailure

BackendToolFilesWrite

path, data, mode

FileWriteResult

BackendToolFilesList

path

FileEntries or ToolExecutionFailure

BackendToolFilesExists

path

bool

BackendToolCodeRun

code, language, timeout

CodeResult or ToolExecutionFailure

Registry#

Function

Behavior

register(name)

Decorator that registers a backend class.

list_names()

Returns registered backend names.

get(name)

Returns the per-process singleton backend instance.

get_class(name)

Returns the backend class.

is_available(name)

Returns true when the backend is registered and class availability is true.

preferred(names)

Returns the first available backend instance.

set_default(name) / current()

Sets and gets the default backend.

Exceptions#

Exception

Trigger

BackendNotFound

get(...) / get_class(...) cannot find the backend.

BackendSandboxClosed

BackendSandbox.dispatch(...) is called on a closed sandbox.

UnsupportedBackendTool

The backend implementation does not support a payload type.

RuntimeError

The opensandbox backend is opened directly when the OpenSandbox SDK is missing.

Built-in backends#

Backend

Behavior

local

Host-side subprocess plus filesystem workspace. Automatically registered after importing rath.backend.

opensandbox

Optional SDK backend. The container root is /workspace; working_dir requests a host bind.

OpenSandbox uses opensandbox/code-interpreter:v1.0.2 by default. If a requested host bind is rejected by the server allowlist, OpenRath retries once with an empty workspace unless RATH_OPENSANDBOX_STRICT_WORKSPACE_BIND=1 is set.

Persistent sandbox identities#

PersistentSandboxRegistry stores reusable sandbox identities under .openrath/sandboxes/; it does not hold live BackendSandbox handles.

Persistent sandbox identity registry

Persistent sandbox records make local working directories and OpenSandbox remote ids discoverable across process boundaries without keeping live handles open.#

Backend

Storage

local

.openrath/sandboxes/local/<uuid>/ working directory.

opensandbox

.openrath/sandboxes/opensandbox/<uuid>.json remote sandbox record.

API

Behavior

alloc_local_id()

Creates a fresh local sandbox directory.

ensure_local(id)

Creates or reuses a local sandbox directory.

list_local() / delete_local(id) / prune_local(older_than=...)

Local sandbox cleanup helpers.

record_remote(backend, remote_id, spec=None)

Stores a remote sandbox identity.

load_remote(id) / list_remote() / touch_remote(id) / delete_remote(id)

Remote record lifecycle helpers.

Autodoc#

class rath.backend.Backend[source]#

Abstract base class for sandbox backends.

Subclasses must:

  1. Set the name class attribute and register via rath.backend.register().

  2. Implement the static is_available, capabilities and supported_calls classmethods.

  3. Implement the instance method sandbox_count.

  4. Implement the async hooks _aopen, _aclose and _adispatch. The sync open / close / dispatch defaults below route these through rath._async.runtime.OpenRathRuntime so a single background loop services every subsystem concurrently.

abstractmethod classmethod is_available() bool[source]#

Return whether this backend is usable in the current environment.

Must be cheap (microseconds, no network, no subprocess). Examples: check that a required SDK is importable, or that a config file or environment variable is present.

abstractmethod classmethod capabilities() Capabilities[source]#

Return the static capability description of this backend type.

abstractmethod classmethod supported_calls() frozenset[type[BackendTool]][source]#

Return BackendTool subclasses this backend handles.

abstractmethod sandbox_count() int[source]#

Return the number of open sandboxes managed by this instance.

open(spec: BackendSandboxSpec | None = None) BackendSandbox[source]#

Open a fresh sandbox and return its handle (sync facade).

close(sandbox: BackendSandbox) None[source]#

Close sandbox and release resources (sync facade).

Calling close on an already-closed sandbox is a no-op.

dispatch(sandbox: BackendSandbox, call: BackendTool) ToolResult | bool[source]#

Execute call against sandbox and return its result (sync facade).

class rath.backend.BackendSandbox(backend: Backend, handle: str, spec: BackendSandboxSpec | None = None, closed: bool = False, _refcount: int = 0, _refcount_lock: allocate_lock = <factory>)[source]#

Sandbox handle with reference counting.

Lifecycle is governed by _refcount: each Session.sandbox slot, each with sandbox: block, and any explicit acquire() counts as one reference. release() decrements and, when the count reaches zero, calls backend.close(self). There is no “force close” path — callers that want immediate teardown must drop all references.

Backend.open() returns a sandbox with _refcount == 0. The caller is expected to either bind it to a Session (which acquires) or enter with sandbox: (which acquires) before it can be safely held.

property refcount: int#

Current number of live references; read-only mirror of internal state.

acquire() BackendSandbox[source]#

Add one reference; return self for chaining.

release() None[source]#

Drop one reference; close via the backend when the count hits zero.

dispatch(call: BackendTool) ToolResult | bool[source]#

Apply call through dispatch().

stream(*, buffer: int = 0) Stream[source]#

Return a fresh Stream bound to this sandbox.

buffer=0 (the default) means an unbounded queue; set a positive integer to apply backpressure on Stream.submit().

class rath.backend.BackendSandboxSpec(image: str | None = None, entrypoint: Sequence[str] | None = None, env: Mapping[str, str] | None = None, timeout: timedelta | None = None, working_dir: str | None = None)[source]#

User-facing description of a sandbox to open.

Fields are intentionally optional. Each backend may ignore fields that do not apply (e.g. LocalBackend ignores image).

class rath.backend.BackendToolCommandRun(cmd: str | Sequence[str], env: Mapping[str, str] | None = None, cwd: str | None = None, stdin: bytes | None = None, timeout: float | None = None)[source]#

Run a shell command inside the sandbox.

class rath.backend.BackendToolFilesRead(path: str, encoding: str | None = 'utf-8')[source]#

Read a file from the sandbox.

class rath.backend.BackendToolFilesWrite(path: str, data: bytes | str, mode: int = 420)[source]#

Write a file inside the sandbox with the given Unix mode.

class rath.backend.BackendToolFilesList(path: str)[source]#

List entries (non-recursive) under a sandbox directory.

class rath.backend.BackendToolFilesExists(path: str)[source]#

Check whether a path exists inside the sandbox.

class rath.backend.BackendToolCodeRun(code: str, language: str = 'python', timeout: float | None = None)[source]#

Execute a code snippet inside the sandbox in the given language.

class rath.backend.CommandResult(exit_code: int, stdout: bytes, stderr: bytes, elapsed_ms: float)[source]#

Result of BackendToolCommandRun.

class rath.backend.FileContent(data: bytes | str)[source]#

Result of BackendToolFilesRead.

data is str when the call was made with an encoding set, or bytes when encoding=None.

class rath.backend.FileEntries(entries: tuple[FileEntry, ...])[source]#

Result of BackendToolFilesList.

Entries are sorted by name for stable ordering.

class rath.backend.FileWriteResult(bytes_written: int)[source]#

Result of BackendToolFilesWrite.

Holds the number of bytes actually written so that callers can verify the write without re-reading the file.

class rath.backend.CodeResult(text: str | None, stdout: bytes, stderr: bytes, error: str | None)[source]#

Result of BackendToolCodeRun.

text holds the value of the last expression when the underlying runtime supports value extraction (e.g. a real code interpreter). For backends that only execute the script as a subprocess, text is None.

class rath.backend.ToolExecutionFailure(kind: str, message: str, detail: str | None = None)[source]#

Structured failure from dispatch().

Prefer returning this instead of raising when the tool invocation itself failed or is unsupported, so the session loop can surface text to the model.

class rath.backend.Stream(sandbox: BackendSandbox, *, buffer: int = 0)[source]#

Per-sandbox FIFO queue of tool-call operations (blocking worker thread).

Thread-safety contract:

  • Once __exit__ returns, no caller can submit new ops; the worker has joined, every future the public methods returned is either done or will fail with RuntimeError("stream is closed") (no hanging).

  • Public methods do their _check_closed check and the matching _queue.put under the same lock acquisition so a concurrent __exit__ cannot enqueue the shutdown signal in between.

  • If the worker hits a fatal exception, _fail_remaining empties the queue and fails every future on it. BaseException (e.g. KeyboardInterrupt) is re-raised after that drain so the worker thread terminates with a visible traceback instead of vanishing.

class rath.backend.Event[source]#

Synchronization marker that crosses Stream boundaries.

class rath.backend.Future[source]#

Blocking handle to the result of a submitted tool call.

class rath.backend.persistence.PersistentSandboxRegistry[source]#

Filesystem-backed registry of persisted sandbox identities.

Instances are cheap; everything lives on disk under .openrath/sandboxes/{local,opensandbox}/. The class does no locking; callers that share a registry across threads should serialize writes externally.

alloc_local_id() UUID[source]#

Generate a new UUID and create its working directory.

Returns the UUID. Use local_path() to resolve it back to a pathlib.Path. Repeated calls always return a fresh UUID; for “give me one if missing, else reuse”, call ensure_local() with an explicit id.

ensure_local(sandbox_id: UUID | str) Path[source]#

Create the working directory for sandbox_id if missing; return it.

Idempotent. Useful when a caller already knows the id (e.g. from a persisted session header) and wants to rebind the same workdir.

local_path(sandbox_id: UUID | str) Path[source]#

Resolve a local sandbox id to its on-disk working directory.

Does not check existence — use ensure_local() to create on demand. Returns the path even when the directory has been removed, so callers can decide whether to recreate.

list_local() list[UUID][source]#

Enumerate UUID-named subdirectories under sandboxes/local/.

delete_local(sandbox_id: UUID | str) bool[source]#

Remove the on-disk working directory for a local sandbox.

Returns True when the directory existed and was removed, False when it was already absent. Idempotent.

prune_local(*, older_than: timedelta) list[UUID][source]#

Remove local sandbox dirs whose mtime is older than older_than.

Returns the list of removed ids in deletion order. Useful for a weekly cron / startup sweep — PersistentSandboxRegistry().prune_local(older_than=timedelta(days=30)).

record_remote(backend: str, remote_id: str, spec: BackendSandboxSpec | None = None, *, sandbox_id: UUID | None = None) UUID[source]#

Persist remote_id (e.g. an OpenSandbox native.id) under a new UUID.

backend is the backend name (typically "opensandbox"). spec is the BackendSandboxSpec used to create the remote sandbox; it round-trips through the same JSON projection as the session header. Returns the registry UUID (NOT remote_id) so callers can keep using a stable local handle.

touch_remote(sandbox_id: UUID | str) None[source]#

Update last_used_at on the remote sandbox index file.

Silently no-ops when the record is missing — callers should rely on load_remote() first if presence matters.

load_remote(sandbox_id: UUID | str) RemoteSandboxRecord | None[source]#

Read one remote-sandbox index file. Returns None when missing.

list_remote() list[RemoteSandboxRecord][source]#

Enumerate every remote sandbox record. Unreadable files are skipped.

delete_remote(sandbox_id: UUID | str) bool[source]#

Remove the index file for a recorded remote sandbox.

Returns True when the file existed and was removed; False when absent. Does not kill the remote container on the server — that’s the caller’s responsibility via the backend’s close().

prune_remote(*, older_than: timedelta) list[UUID][source]#

Remove index files whose last_used_at is older than older_than.

Returns the deleted ids in deletion order. The remote containers themselves are not touched.

reattach_remote(sandbox_id: UUID | str) BackendSandbox[source]#

Reattach to a previously recorded remote sandbox by its registry id.

Looks up the index file, then delegates to the named backend’s attach method (currently only OpenSandboxBackend provides one). Updates last_used_at on success.

Raises KeyError when the registry id is unknown, and AttributeError when the recorded backend has no attach — either path produces a clear error rather than silently creating a new container.

ensure_dirs() None[source]#

Create both sandboxes/local/ and sandboxes/opensandbox/ roots.

class rath.backend.persistence.RemoteSandboxRecord(*, schema_version: int, id: UUID, backend: str, remote_id: str, spec: BackendSandboxSpec | None, created_at: datetime, last_used_at: datetime, path: Path)[source]#

One .openrath/sandboxes/opensandbox/<uuid>.json decoded.

rath.backend.get(name: str) Backend[source]#

Look up a backend by name and return the per-process singleton instance.

The same instance is returned for repeated calls with the same name, so per-backend caches (e.g. OpenSandboxBackend._natives) and the sandbox refcount are coherent across all sessions in the process.

rath.backend.preferred(names: list[str]) Backend[source]#

Return an instance of the first available backend in names.

Raises BackendNotFound if none of the listed backends are registered and available.

← API Reference