Skip to main content
Subclass a session to change command defaults, image resolution, or operation handling. To replace storage or file transfer, implement the matching sync or async interface and pass it to the sandbox factory. See Advanced: assemble a sandbox yourself if you also need to manage component construction and cleanup.

Customize command policy and operations

RunRequest is a frozen configuration value. Its argument tuple and environment mapping are copied at construction. Files and stdin remain caller-owned resources. Use dataclasses.replace() to apply policy without mutating the caller’s request. This example rejects shell commands, applies a default timeout, and counts waits. Configure a connection as described in Connection sources.
The session calls prepare_request() before uploading files or submitting work. It passes the prepared inputs to submit_request(), then constructs the handle through create_operation(). The OperationContext holds the effective request and the source image, history entry, and branch used for the eventual commit. If operation construction fails after submission, the session attempts remote cancellation and preserves the factory error. create_store() and create_file_transfer() supply defaults only when no component was passed explicitly. Constructor-time factories must not depend on attributes that a subclass initializes after super().__init__().

Choose what to override

Use native async I/O in async overrides; offload blocking work explicitly.

Implement file transfer

Inherit SyncFileTransfer or AsyncFileTransfer and implement:
  • upload(UploadFileSpec) -> FileSpec: return a file identifier valid on the session’s server.
  • read_file(image_uuid, path) -> bytes: return exact bytes from the specified image.
The default prepare_files() expands directories, rejects destination conflicts, and calls upload() with a bounded worker count. Set max_concurrency on a custom transfer to select that limit. Override iter_file() to yield chunks and release its reader when the generator closes. Its default implementation buffers through read_file(). The inherited download_file() writes those chunks atomically. See Upload inputs and download results for tree preparation, callbacks, cancellation, and ownership. The async implementation cancels and joins sibling uploads if one fails. Override this method for batching or bounded concurrency. Override read_stdin() to support additional input handling. Never close a caller-owned input stream. Inject a component with file_transfer=. ClientFileTransfer and AsyncClientFileTransfer provide the standard implementations and can also be subclassed. Import UploadFileSpec, UploadedFile, InputSource, and the readable protocols from contree_sdk.files.

Implement history storage

Implement SyncStore or AsyncStore from contree_sdk.store. The same interface supports session history and reusable results. The core operation is append(session_id, node): save an immutable node with an explicit parent. Preserve these guarantees across all clients of your backend:
  • A parent belongs to the same live tree; a tree has one root.
  • Repeating an identical node succeeds without a duplicate. Reusing its identity with different contents raises NodeConflictError without overwriting data.
  • Reads and writes detach JSON values from caller-owned objects.
  • Tree and node kind filters apply before cursor pagination.
  • get_nodes() reads up to 1000 known identities atomically, preserving input order and None slots for missing nodes. Implement it as a batch read when network round trips are expensive.
  • TTL and deletion cover the tree. Reads and append do not renew expiry.
See Tree stores for the complete contract. Store does not implement branch switching, command execution, or Docker hashing. Session handles local navigation and serializes its payloads into nodes. Staging records contain attachments and applied paths. Detached completion saves its response and result in one node, using the identity recorded before collection.

Store reusable results

The same Store holds history and reusable results. Implement its contract with Redis or PostgreSQL and supply it through store= in helpers and direct constructors. Tree kind distinguishes purposes; the backend does not interpret user JSON. Stores expose close() and a matching context manager. Sessions and builders leave caller-supplied stores open. See Reuse uploads and retain their sources for upload reuse and Advanced: resume a background operation for operation recovery.

Replace an integration backend

The framework adapters depend only on SyncExecutor or AsyncExecutor:
  • session_id identifies the execution context.
  • execute(RunRequest) returns contree_client.models.InstanceResult.
  • write_file(path, content) writes bytes and returns None after application.
  • read_file(path) returns bytes; use client NotFoundError or UnprocessableEntityError for missing or invalid paths.
A nonzero process exit code belongs in the result. Operation-level failure is an exception. Preserve cancellation and timeout errors. An executor used by the sandbox must retain filesystem changes when disposable=False; the adapter uses this for commands and uploads. It serializes its own mutating calls, but callers sharing the same executor must coordinate any additional concurrent mutations. The adapters never access a concrete session’s client, store, or history internals. They do not close the executor. The async adapter requires native async methods; its sync entry points raise NotImplementedError. write_file() has a default implementation using shell execution and binary RunRequest.stdin. Existing executors inherit it. Override this method to replace the transfer mechanism; adapters do not construct upload commands themselves. A failed write raises FileWriteError(path, message). Transport errors, timeouts, and cancellation must propagate unchanged. A write must apply the content before returning. Uploading a blob or staging files for a later command is not sufficient. A native transfer implementation must also coordinate with VM startup, active commands, and snapshots. Keep the VM selected for the transfer until the backend confirms application. Blob upload and application to a VM are separate operations. A future backend implementation can reuse uploaded blobs here without changing framework calls. Archive extraction needs explicit extraction semantics; write_file always means writing the supplied bytes, even when those bytes contain an archive. See File transfers and executor contracts.

Extend Dockerfile builds

The same directive class supports both builders through execute() and execute_async(). Create a parser per dialect, then register your directive:
Pass the parser with parser=make_parser() to either builder. Registration copies no state into the module registry and cannot change another builder’s dialect. execute() and execute_async() define the instruction’s sync and async behavior. Implement both if the dialect supports both builders. The session_factory receives client as its first positional argument, then session_id, store, image, node_id, and tree_kind as keyword arguments. It returns a ContreeSession or ContreeAsyncSession implementation. Both fresh layers and cache hits use this factory. Set context_class to a BuildContext or AsyncBuildContext subclass when only additional methods or state are needed. Override create_context(BuildRequest) for a different construction policy. execute_directive() intercepts normal steps, stage sealing, and final file materialization. Custom directives that execute nested directives must call context.execute_directive(). A custom create_context() must pass directive_executor=self.run_step to preserve event reporting and policy dispatch for nested steps. run_step() wraps execute_directive() with step notifications. Override BuildContext.run_operation(request, branch=...) or its async equivalent to customize RUN orchestration. The default uses session spawn_request(), operation events()/wait(), and session commit_result(). It applies prepare_request() and the session operation factory through these public hooks. A custom directive must include every input that affects a layer in the context’s cache state or its hash contribution. Changing the implementation of an instruction can invalidate previous cached layers; use a new session/cache namespace or disable cache reuse. A parser extension alone does not automatically version existing caches.

Operation contract obligations

OperationContract and AsyncOperationContract expose status, events, waiting, stdin, signals, cancellation, context management, and subprocess creation. A session-created operation has an OperationContext. A reattached UUID-only handle may have no context and cannot be committed to session history. wait() must store the final OperationResponse before returning its InstanceResult. Cancellation and cleanup must preserve the primary exception. Use SubprocessContract or AsyncSubprocessContract when replacing subprocess handles. Iteration followed by wait() must not wait for a second exit event. A stream failure must reach its consumer instead of appearing as successful EOF. The built-in operations own one unfiltered reader per handle. Override open_event_stream() to replace its source. Calling events() creates a local subscription; overriding it does not replace the source used by wait() or subprocesses. Register observers before entering the context or first consuming events() or wait(). Sync observers return directly; async observers are awaited. They receive (event, None) for events and (None, error) at EOF. Keep them short; do not wait for the operation from its own observer. Observer errors fail the reader. LazySession requires add_observer() support from a custom operation contract. It delegates request policy and operation creation to the wrapped session. Override keepalive_request() on the lazy wrapper to use another main process. See Sessions and operations for the public snapshot-policy interface.

Resolve application-specific image names

A sync override runs during session construction. An async override runs during ensure_ready() or the first operation. Return a valid UUID for the selected server; the example uses a mapping that your application supplies.

Select a commit policy

ApiSuccessCommitPolicy is the default for both session implementations. It retains successful API results from non-disposable commands, including nonzero and unknown process exit codes. API failure and cancellation raise errors and never change history. Pass commit_policy=ZeroExitCommitPolicy() to retain only results with an explicit zero exit code. A rejected result is still returned to the caller. Rejection does not change the session image, branches, or pending files. commit_result() uses the same policy after an explicit spawn() and wait(); it returns None when it skips the commit. Disposable requests cannot create history entries through commit_result(), even if the server returns an image.
For an application-specific rule, inherit AbstractCommitPolicy and implement should_commit(context: OperationContext, result: InstanceResult) -> bool. The session passes the effective request and its original history position. The method must not perform blocking I/O. The session validates API success before calling it and requires a result image before committing an accepted result. Policies are session components; pass the same policy when resuming a session. Session commands save by default; disposable=True opts out. Snapshots are lightweight and can run after every command, but stop the VM. An unchanged filesystem can return the same image UUID; policies must not require a new UUID. A LazySession snapshot also uses the underlying session’s commit policy. If the policy rejects that snapshot, the lazy session fails and leaves history unchanged. It cannot restart from the previous image while claiming that changes were saved. The policy sees the keepalive process result, not individual subprocess results. Use the default policy to preserve snapshots after terminating the keepalive.