Skip to main content

shardflux

Python client for Shardflux: cloud computers for AI agents.

Open a persistent workspace by key, run commands in it, read and write its files, suspend it when idle, resume it later with its disk and memory intact, and fork it.

Early access. Shardflux is in early access. The API is versioned (/v1), but this client is below 1.0: a minor release may contain breaking changes.

Versions. This README describes 0.2.0. Anything marked (0.2.0+) is not in 0.1.0; the changelog (CHANGELOG.md, shipped in the source distribution and in the wheel's metadata) lists what each version added. Check yours with python -c "import shardflux; print(shardflux.__version__)".

  • Python 3.10 or later; one dependency (httpx).
  • Typed (py.typed).
  • Retries, idempotency keys, operation waits and tool-token refresh are handled for you.

Install

pip install shardflux

Quick start

Create a project API key in the Shardflux console (sfk_<key id>_<secret>). API keys are server credentials; keep them out of client-side code.

from shardflux import Shardflux, format_timing

sf = Shardflux()  # reads SHARDFLUX_API_KEY; or Shardflux(api_key="...")


def open_workspace():
    return sf.open(key="customer-42/main", template="python-node-browser")


# 1. Open the workspace (created on first use) and run a command. Check that it worked.
ws = open_workspace()
result = ws.exec("python3 -c 'print(40 + 2)'")
if result.exit_code != 0:
    raise RuntimeError(f"python3 exited {result.exit_code}: {result.stderr}")
print(result.stdout.strip())  # 42

# 2. Write a file, then suspend the workspace and wait until the suspend has finished.
ws.files.write("/home/user/notes.txt", "hello from Python\n")
ws.suspend(wait=True)  # returns once suspended: ws.state == "suspended"

# 3. Open the same key again: the workspace resumes, and the file is still there.
again = open_workspace()
print(again.files.read_text("/home/user/notes.txt"))  # hello from Python
print(format_timing(again.last_timing))  # (0.2.0+) where the resume's time went

open() waits until the workspace is running. Opening the same key again never resets it: files, installed packages and running processes are still there. The same program is in examples/quickstart.py (SHARDFLUX_API_KEY=sfk_... python examples/quickstart.py).

With 0.1.0, drop format_timing and last_timing; everything else above works as shown.

Configuration

Argument Environment variable Default
api_key SHARDFLUX_API_KEY required
base_url SHARDFLUX_API_URL https://api.shardflux.dev
timeout 30.0 seconds per request
max_retries 2 (safe or idempotent requests only)
http_client a new httpx.Client (pass your own for proxies or custom transports)
on_progress (0.2.0+) none: a listener for the progress of every traced call (see "Timing and progress")

Shardflux is a context manager (with Shardflux() as sf: ...); close() closes the HTTP client it created.

Commands

r = ws.exec("pip install requests && python3 app.py", cwd="/home/user/project", env={"DEBUG": "1"}, timeout=600)
r = ws.exec(["python3", "-V"])  # a list runs as argv, without a shell

r.exit_code, r.stdout, r.stderr, r.timed_out, r.ok

A string runs through bash -lc; a list runs as argv. timeout (seconds) is enforced inside the workspace. Pass on_output=lambda stream, chunk: ... to receive output as it arrives. If the connection drops, exec resumes the output from byte offsets; it never starts the command twice. Ctrl-C cancels the command in the workspace.

Files

ws.files.write("/home/user/data.bin", b"\x00\x01\x02", create_parents=True)
data = ws.files.read("/home/user/data.bin")  # bytes, the whole file
text = ws.files.read_text("/home/user/notes.txt")
ws.files.list("/home/user")  # {"entries": [...], "truncated": False}
ws.files.stat("/home/user/notes.txt")
ws.files.remove("/home/user/data.bin")

Writes are atomic and durable: they are acknowledged after the file and its directory are fsynced.

Lifecycle

ws.suspend(wait=True)  # memory and processes are checkpointed; returns once suspended
ws.resume(wait=True)  # or simply open() the key again

copy = ws.fork("customer-42/experiment")  # waits until the fork is running
copy.delete()  # tool access ends at once; keys are never reused

for w in sf.workspaces.list_all(key_prefix="customer-42/"):
    print(w.key, w.state)
page = sf.workspaces.list(limit=50)  # page.data, page.next_cursor

Requested or finished

suspend, resume, snapshot, delete, close and reset start a lifecycle operation and return it. What the call means depends on wait:

Call Returns when Returns
ws.suspend() the suspend is requested (usually queued; the workspace is still running) the Operation
ws.suspend(wait=True) the suspend has finished (ws.state is then suspended) the succeeded Operation

fork() works the other way round: it waits by default and returns the new workspace; pass wait=False to get it as soon as the fork is requested. The same calls on sf.workspaces (sf.workspaces.suspend(workspace_id, wait=True), ...) take wait too (0.2.0+).

A failed operation raises OperationFailedError. If timeout (default 300 s) passes first, OperationTimeoutError is raised and the operation keeps running server side: wait again with sf.workspaces.wait_for_operation(err.operation_id). Without wait, the returned operation is the handle for the work in progress: pass its id to wait_for_operation() when you need it finished.

Waiting asks the API to hold each poll until the operation changes (Prefer: wait, at most 20 s per request), so completion arrives within one round trip of the commit (0.2.0+). Against an API without bounded waits (or with server_wait=False) it polls with backoff (250 ms doubling to 5 s, ±20 % jitter). A waiting open() asks the API to hold the open itself until the workspace is ready; the answer carries the first tool token, so the first tool call starts at once (0.2.0+).

Suspended workspaces wake on use (0.2.0+)

A tool call (exec, files, changes) on a suspended workspace resumes it (or joins the resume or open already running), then runs. A call made during a suspend or resume waits for the transition to finish. The call never runs twice: the workspace executes nothing it refused.

  • The wait is bounded per call by transition_timeout (default 120 s), shared by the transition waits and at most 3 wakes. A resume or open still pending at the end raises OperationTimeoutError, naming the operation, its state and reason. A failed resume or open raises OperationFailedError at once.
  • Following an exec's output never wakes a workspace, so an explicit suspend() is respected.
  • ws.wake(timeout=...) does the same on demand (True when it resumed or waited, False when the workspace was already running).
  • ws.cell(wake=None) returns workspace_not_running instead.
cell = ws.cell(transition_timeout=30)  # give up waking after 30 s
cell.exec_run(["make", "test"])  # resumes the workspace first if it is suspended

Timing and progress (0.2.0+)

Every open, wake and lifecycle call is traced. ws.last_timing (and err.timing when the call fails) says where the time went, and format_timing() prints it:

from shardflux import format_timing

ws = sf.open(key="customer-42/main", template="python-node-browser")
print(format_timing(ws.last_timing))

A slow open (34 s instead of the usual second) then reads, for example:

open 34.18 s, succeeded (workspace 01a0e5a8-3edd-74ba-b489-d62b8925e342, operation 01a0e5a8-3ef0-7ecb-975e-dff2d5ca6e33)
  client: request 20.01 s (held) → capacity_pending 13.52 s (no_ready_host) → running 590 ms → view 42 ms ∥ token 61 ms
  server: queued 33.40 s, ran 620 ms, total 34.02 s; start warm, boot to ready 79 ms
  outside the server: 160 ms

Here the time went to waiting for a host with capacity; the network and the VM start were fast.

  • client phases are on your monotonic clock: the request (a held open waits on the server: held), then each operation state the SDK observed while waiting, with the server's reason (capacity_pending / no_ready_host: waiting for a host; running / template_downloading: fetching the template), then reading the workspace and issuing the first tool token (together: ∥).
  • server timing comes from the operation itself (one database clock): queued is creation until it began running, including any wait for capacity; ran is the cell's work (placement, boot or restore, guest readiness). start / resume from and boot to ready / host ... are what the cell reported.
  • outside the server is your total minus the operation's: network, TLS, polling latency, view and token. A large value with a small server total points at the connection between you and the API, not at the workspace.
  • retries lists transient failures the SDK retried (cause and backoff).

The timing is a LifecycleTiming dataclass (phases, retries, server, outside_server_ms, ...). Watch it live with on_progress, on the client for every call or per call (sf.open(..., on_progress=...), ws.suspend(wait=True, on_progress=...), sf.workspaces.wait_for_operation(..., on_progress=...), ws.wake(on_progress=...), ws.cell(on_progress=...)):

import sys

from shardflux import ProgressEvent, Shardflux, format_timing


def progress(e: ProgressEvent) -> None:
    if e.type == "phase":
        print(f"{e.action}: {e.phase}{f' ({e.reason})' if e.reason else ''} at {e.at_ms} ms", file=sys.stderr)
    elif e.type == "retry" and e.retry:
        print(f"{e.action}: retry {e.retry.request}: {e.retry.cause}", file=sys.stderr)
    elif e.type == "done" and e.timing:
        print(format_timing(e.timing), file=sys.stderr)


sf = Shardflux(on_progress=progress)

Events are phase (a phase began), retry and done (with the timing). Tool calls add token traces (a tool token fetched: initial, expiring or invalidated) and tool events (a busy wait, a stale token replaced, a retry). A listener that raises never breaks the call. queued and ran need an API that reports the operation's started_at (Operation.started_at); otherwise they are missing.

Sessions, reset and changes (0.2.0+)

# A session workspace is discarded when the session ends: close(), or the idle timeout
# (idle_timeout_seconds, default 600). The same key then opens a new, empty workspace.
with sf.open(key="agent-7/task", template="python-node-browser", lifetime="session") as ws:
    ws.exec("python3 run.py")
# Leaving the block called ws.close(). For a persistent workspace close() makes no API call: it only
# drops cached tool tokens, so it is safe in `finally`.

sf.workspaces.list(lifetime="any", purpose="any", include_deleted=True)  # default: persistent standard only
ws = sf.workspaces.get_by_key("customer-42/main")  # any lifetime/purpose; live row over tombstones; None if absent

ws.reset(wait=True)  # layered workspaces: wipe every change, back to the template
page = ws.changes(path_prefix="/home/user", hash=True, summary=True)  # what changed vs. the template
for c in page.data:
    print(c.change, c.path)  # added, modified, metadata, deleted, replaced

Workspace views also carry disk_layout, purpose, origin, ended_reason, dev_template_id and update_policy. Reset, changes() and save-as-template need a layered workspace.

Templates (0.2.0+)

sf.templates.files("python-node-browser", 3, "/home/user")  # one directory level of a version
sf.templates.file_entry("python-node-browser", 3, "/usr/bin/python3")
sf.templates.diff("my-agent", from_="base", to=2)  # .data: path, change, before, after; .summary

result = ws.save_as_template("my-agent", description="Agent with tools preinstalled")
result.build_id, result.build["target_version"]

draft = sf.templates.draft("my-agent")  # dev mode: edit a template live
d = draft.create(base="python-node-browser@3")
d.workspace.exec("pip install -r requirements.txt")
state = draft.capture_state(label="deps", wait=True).result["checkpoint_id"]
with draft.open_test_instance(state_id=state) as test:  # disposable copy of that state
    test.exec("python3 -m pytest")
draft.publish(state_id=state, description="deps")  # the next version
draft.discard()

Drafts and test instances are for owners, admins and API keys with a tool permission. A draft is a persistent workspace (purpose="template_draft"); test instances are sessions.

Secrets (0.2.0+)

Store credentials once and give them to a workspace's processes as environment variables. Values are write-only: no API returns them. Every exec and terminal in the workspace receives the secrets bound to it, plus any the call names in secret_refs.

import os

project_id = sf.me()["api_key"]["project_id"]
sf.secrets.create(project_id, "OPENAI_API_KEY", os.environ["OPENAI_API_KEY"])

# Bind by name when opening (a new key gets the binding; an existing key has it replaced).
ws = sf.open(key="customer-42/main", template="python-node-browser", secrets=["OPENAI_API_KEY"])

ws.secrets.get()  # {"names": [...], "secrets": [{"name", "status", "secret_id", "scope"}]}
ws.secrets.set(["OPENAI_API_KEY", "DATABASE_URL"])  # replace; [] clears
ws.exec("python3 agent.py")  # sees $OPENAI_API_KEY and $DATABASE_URL
  • A name that is unknown, or a secret this workspace may not use, raises ShardfluxApiError 422 (err.reason == "secret_not_available", details["names"]); nothing changes. A bound secret must allow the exec and pty tools (the default).
  • status per bound name: available, not_allowed (its permissions no longer cover this workspace; starts are refused with 403 until fixed) or deleted.
  • Deleting a secret removes it from every binding. Forks keep the binding, but secrets limited to specific workspaces are checked against the fork's own id.
  • sf.secrets also has list, get, update, rotate, versions, delete, access_events, create_organization and list_organization. Permission arguments you do not pass are left alone; None means "no restriction". Organization-wide secrets and access logs belong to organization owners and admins, so a project API key gets 403 for those.

Errors

All errors derive from ShardfluxError.

  • ShardfluxApiError: the API or the workspace refused the request. It mirrors the error envelope: code, message, request_id, retryable, plus status, details, operation_id, retry_after and reason (0.2.0+) (details["reason"], e.g. workspace_not_running, not_session, legacy_disk_layout).
  • OperationFailedError: an awaited operation ended failed or canceled (error_code, operation).
  • OperationTimeoutError: waiting gave up; the operation continues (operation_id, last_state, last_reason).
  • ShardfluxProtocolError: a response was not the documented shape.

Each of them has timing (0.2.0+) when a traced call (open, lifecycle call, wait, wake, token fetch) failed with it: format_timing(err.timing) says where the time went before the failure.

from shardflux import ShardfluxApiError

try:
    sf.open(key="customer-42/main", template="python-node-browser")
except ShardfluxApiError as err:
    print(err.code, err.reason, err.message, err.request_id, err.retryable)

Treat unknown error codes and reasons as generic errors: show message, and use retryable.

Anything else

sf.me() returns the API key's organization and project. sf.secrets manages secrets and sf.templates template file trees, diffs and drafts (see above). sf.request(method, path, ...) calls any /v1 route with the client's authentication, retries and error handling. The TypeScript SDK (npm install @shardflux/sdk) covers the full API, including agent tool definitions for model providers.

Compatibility

  • The client follows the API's /v1 contract. New fields, enum values and error codes can appear in any release; ignore unknown fields.
  • While below 1.0, a breaking change bumps the minor version (0.1 to 0.2).
  • shardflux.__version__ is exported; requests send User-Agent: shardflux-sdk-python/<version>.
  • Examples in this README and in examples/ name the version they need.

License

Apache-2.0

Metadata

Release files for shardflux 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shardflux 0.2.0
File Size Uploaded
shardflux-0.2.0.tar.gz 94.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for shardflux 0.2.0
File Interpreter ABI Platform
shardflux-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 157.8 kB

Release files / shardflux-0.2.0.tar.gz

Download URL shardflux-0.2.0.tar.gz
Size 94.7 kB
Tags Source
SHA-256 checksum
How to use checksums
01ff3506d9fbad485ae299e1ad28ffd4d1264886871fa3a1b69a0d6760c67aa4
BLAKE2b-256 checksum
How to use checksums
c39c0bfbbae2e54161400e95d3d8aa3aab8051000cf7545d00499456104c34c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / shardflux-0.2.0-py3-none-any.whl

Download URL shardflux-0.2.0-py3-none-any.whl
Size 63.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c70734c05121aadcc691350ea682050171a3298d1f2eb45365c92953bbbc4ff4
BLAKE2b-256 checksum
How to use checksums
d5c8020710518ac442e76f62e392e463a91da37b7e81c54ca35b04731e494266
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page