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.
- Sync
- Async
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
InheritSyncFileTransfer 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.
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
ImplementSyncStore 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
NodeConflictErrorwithout 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 andNoneslots 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.
Store reusable results
The same Store holds history and reusable results. Implement its contract with Redis or PostgreSQL and supply it throughstore= 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 onSyncExecutor or AsyncExecutor:
session_ididentifies the execution context.execute(RunRequest)returnscontree_client.models.InstanceResult.write_file(path, content)writes bytes and returnsNoneafter application.read_file(path)returns bytes; use clientNotFoundErrororUnprocessableEntityErrorfor missing or invalid paths.
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 throughexecute() and
execute_async(). Create a parser per dialect, then register your directive:
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 duringensure_ready() or the first operation. Return a valid UUID for the selected server;
the example uses a mapping that your application supplies.
- Sync
- Async
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.
- Sync
- Async
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.