tbcore
Run repeatable agent experiments in Docker, with the evidence to explain every result.
tbcore (Token-Build Core) captures a task and its environment, runs baseline and candidate workloads, grades their outputs, and saves a comparison you can inspect or replay. The same toolkit includes model-call accounting, resumable batches, and statistical checks. Start with a complete local experiment: no API key or paid model required.
Status: alpha;
pip install tbcore(Python 3.11+). The Docker sandbox is defense in depth for a trusted local operator, not a hardened multi-tenant isolation boundary. Read SECURITY.md before running hostile code.
Try the experiment
You need Python 3.11+, Docker with a running daemon, and a clone of this repository. From the repository root:
python -m pip install -e .
python examples/experiment_workflow.py --store .tbcore/example
The example builds the bundled images, runs the same fixture task with baseline and candidate settings, grades the outputs, and replays the candidate from an exported bundle. It prints run IDs and writes:
.tbcore/example/comparison.md- the baseline/candidate report..tbcore/example/candidate-bundle.json- a portable run bundle.
See the experiment walkthrough for how evidence is captured, how to resume work, and what the comparison can establish.
On Windows, these commands work in PowerShell when python is on PATH.
For an isolated install, create a venv with python -m venv .venv and use
.venv\Scripts\python.exe in place of python (on macOS/Linux, .venv/bin/python).
What is included
| Component | What it does |
|---|---|
| Experiments | Captures workspaces and environment provenance; links trials, attempts, events, artifacts, evaluations, and baseline/candidate comparisons. |
| Sandbox | Runs commands in disposable Docker containers with resource limits, timeouts, and a network allowlist. Sessions keep a workspace across commands. |
| Client and Pool | Logs model usage, latency, and price estimates; runs rate-limit-aware batches from a resumable SQLite queue. |
| Stats | Computes summaries, bootstrap confidence intervals, and two-sample regression checks. |
The experiment store captures metadata by default. Workspace contents, prompts, outputs, and other text evidence require an explicit content-capture policy; review exports before sharing them. See experiment storage and security.
Python 3.11+ is required. Docker is needed for sandbox and experiment runs; the model client, pool, and stats can be used without it. If you only need to call an API, the provider's official SDK is a better fit. tbcore is for work that needs the run, its accounting, and its comparison together.
Model-call quickstart
This example calls Anthropic and requires ANTHROPIC_API_KEY on the host. It can
incur provider charges. For a no-key first run, use the experiment.
from tbcore import Client
client = Client("sonnet") # API key read from ANTHROPIC_API_KEY
completion = client.complete(
system="Answer tersely.",
messages=[{"role": "user", "content": "Why is the sky blue?"}],
)
print(completion.text, completion.usage, completion.latency_s)
Every call is appended as one JSON line to .tbcore/calls.jsonl in the working
directory (configurable via CallLog or TBCORE_LOG_PATH).
Providers
Anthropic is the default. For Ollama, vLLM, LM Studio, OpenAI, or any other
OpenAI-compatible endpoint, build a spec with make_model_spec (or use a
registered shortcut):
from tbcore import Client, make_model_spec
claude = Client("sonnet") # ANTHROPIC_API_KEY
local = Client(make_model_spec("qwen3:8b", provider="ollama")) # localhost:11434
served = Client(make_model_spec("org/model", provider="vllm")) # localhost:8000
gpt = Client("gpt-4o-mini") # OPENAI_API_KEY
Notes:
- An API key is sent as
Authorization: Bearer ...on the OpenAI-compatible wire when available; Ollama/vLLM need none by default. cache_prefixuses Anthropic prompt caching and raises on the OpenAI-compatible wire;toolsis Anthropic-wire only for now.- Cost-equivalent uses registered prices. Models without registered prices (local
models, unknown ids) log
cost_equiv: null, not0; checkModelSpec.priced. Client(..., base_url=...)overrides the endpoint on both wires.- Registered prices are snapshots, not provider invoices; check current rates before using cost estimates to set a budget.
- On the OpenAI-compatible wire,
Usage.input_tokensexcludes cached tokens (reported separately ascache_read_input_tokens), matching the Anthropic wire.
Pool
Queue work in a SQLite file, then drain it with bounded concurrency. Re-running after a crash resumes: finished rows are never redone.
import asyncio
from tbcore import Client, ExecutionPool, WorkStore, enqueue
store = WorkStore("work.db")
enqueue(
store,
[
{"messages": [{"role": "user", "content": f"Summarize item {i}"}], "max_tokens": 200}
for i in range(100)
],
)
pool = ExecutionPool(Client("haiku"), store, concurrency=8)
print(asyncio.run(pool.run()))
# After a crash or Ctrl-C, run again with the same work.db: only unfinished rows run.
Payloads take messages (required) and optionally system, cache_prefix, max_tokens,
temperature, and tools. Rate-limit and transient errors are retried with exponential
backoff; a server Retry-After hint is honored, and a 429 pauses every worker until the
backoff window ends. Results (text, usage including cache tokens, latency) are stored in
the result column of the work table.
Sandbox
Run model-generated commands in a disposable container with resource limits and a network allowlist. The bundled image runs non-root, and tbcore drops container capabilities. Docker is required. This is defense in depth; read SECURITY.md for the complete threat model.
from tbcore import Sandbox, build_default_images
# Builds tbcore/sandbox:latest and tbcore/proxy:latest from contexts bundled in the package.
images = build_default_images()
with Sandbox(images["sandbox"], timeout_s=30, allow_hosts=(), read_only=True) as box:
result = box.run(
["python", "main.py"],
files={"main.py": "import os; print(os.environ['GREETING'])"},
env={"GREETING": "hello"},
)
assert result.exit_code == 0
print(result.stdout.strip()) # hello
allow_hosts=() gives the container no network. The default allowlist is
api.anthropic.com; set it explicitly for each workload. read_only=True is
recommended, while the default remains False for compatibility.
Published images
Each release also publishes the sandbox and proxy images to GitHub Container Registry as
ghcr.io/adityaarakeri/tbcore-{sandbox,proxy}:<version> (amd64 and arm64, with build
provenance attestations). pull_default_images() pulls the ones matching your installed
tbcore and returns digest-pinned references; use it instead of building when you run a
released version:
from tbcore import Sandbox, pull_default_images
images = pull_default_images() # {"sandbox": "ghcr.io/...@sha256:...", "proxy": "..."}
with Sandbox(images["sandbox"], proxy_image=images["proxy"], allow_hosts=()) as box:
print(box.run(["python", "-c", "print('hello')"]).stdout)
tbcore never pulls the proxy implicitly. The proxy reports a protocol version when it
starts, and Sandbox refuses a proxy image whose version does not match this tbcore, so
an outdated local tbcore/proxy:latest fails with a rebuild hint instead of misbehaving.
Unreleased checkouts and offline hosts keep using build_default_images().
image_context("sandbox") / image_context("proxy") return the bundled build contexts if
you want to build them yourself (or extend them with build_image). Containers run with all
capabilities dropped, as the image's own USER (uid 1000 in the bundled image); pass
Sandbox(..., user="1000:1000") to force a non-root user on images that default to root.
Keep provider credentials in the proxy
Passing an API key via env= hands it to the untrusted code. inject_headers keeps it in the
proxy instead: the proxy sets the headers on every request to that host, and discards any
Authorization / x-api-key the sandbox sends.
import os
from tbcore import Sandbox, build_default_images
images = build_default_images()
with Sandbox(
images["sandbox"],
inject_headers={"api.anthropic.com": {"x-api-key": os.environ["ANTHROPIC_API_KEY"]}},
) as box:
print(box.base_url("api.anthropic.com")) # http://proxy:8080/api.anthropic.com
The Anthropic SDK refuses to send a request without some key, which is why
ANTHROPIC_API_KEY=sandbox-placeholder is set; the proxy overwrites it on the wire. This
automatic setup also applies to OpenAI injection: OPENAI_BASE_URL points to the proxy's
/v1 path and OPENAI_API_KEY is a placeholder. Codex CLI additionally needs a custom
Responses provider configured to use that URL (see below).
For other hosts or SDKs, point the client's base URL at box.base_url(host) (plain HTTP to the proxy,
which speaks verified HTTPS to the real host). Injected hosts are not reachable through
HTTPS_PROXY/CONNECT, so code cannot reach them with a key of its own. This protects against key
theft and against sending data to an attacker's account via that API. It does not stop sandboxed
code from using your key through the proxy (requests are billed to, and land in, your account).
Codex CLI in a sandbox
Run python examples/codex_sandbox.py with OPENAI_API_KEY set on the host. Set
TBCORE_CODEX_MODEL to choose a model. The example
builds the optional image_context("codex") image and the proxy, creates a writable
session workspace, asks Codex to edit a file, checks the timeout, exit status, and file
contents. The default build_default_images() continues to build only the
Python sandbox and proxy; build the Codex image explicitly with build_image.
Codex's custom provider uses base_url = "http://proxy:8080/api.openai.com/v1",
env_key = "OPENAI_API_KEY", wire_api = "responses", and
supports_websockets = false. The sandbox receives a placeholder key while the proxy adds
the host's real Bearer key to upstream requests. The proxy handles HTTP streaming; this
initial integration uses API-key auth only, without ChatGPT sign-in or WebSocket transport.
The Codex CLI's internal danger-full-access setting lets it reach the proxy and edit
/work inside the tbcore container. Docker remains the outer isolation boundary;
this setting does not change the Docker mounts or network allowlist. Set a finite
session timeout and close it after each workload. See
OpenAI Docs configuration
and non-interactive mode.
Sessions
run() starts a fresh container per call. For an agent loop that works on one workspace, use a
session (one long-lived container, same hardening and limits):
from tbcore import Sandbox, build_default_images
images = build_default_images()
with Sandbox(images["sandbox"], allow_hosts=()) as box, box.session() as session:
session.put_files({"app.py": "print('v1')\n"}) # files belong to the container user
session.exec(["python", "app.py"], timeout_s=10) # killed in-container at the deadline
print(session.read_text("app.py"))
Sessions replace the image's ENTRYPOINT/CMD with sleep infinity, and exec
needs coreutils timeout in the image. session.copy_out(...) transfers results
to the host with a default 100 MB size cap and rejects links outside the destination.
Stronger isolation
Use Sandbox(images["sandbox"], runtime="runsc", read_only=True) when gVisor is
installed and registered with Docker.
read_only=Truemounts the root filesystem read-only (a 64 MB/tmptmpfs and an anonymous/workvolume stay writable), so code cannot modify binaries or libraries in its container. It is recommended; the default staysFalsefor compatibility. Custom images need a/workdirectory writable by their user.runtime="runsc"runs sandbox containers under gVisor, which puts a user-space kernel between the code and the host kernel. It reduces the shared-kernel attack surface; it is not a guarantee. Installrunscand register it with Docker first (see gVisor's Docker quick start). The proxy container always uses the default runtime.
Container upgrades and maintenance
Image build contexts ship with the package under src/tbcore/images.
Base images are digest-pinned. Rebuild with a new tag, test the exact candidate image,
and record its image ID before switching workloads; updating tbcore does not rebuild
existing images or replace running containers. The optional Codex image must be built
separately with an exact CODEX_VERSION. See CONTRIBUTING.md for
the required checks and the experiment guide for
recording environment provenance and recovery.
Agent experiments
| Path | Status |
|---|---|
| Offline Docker fixture | Baseline/candidate, grade, export, and replay example with an installed-wheel integration test. |
| Codex CLI with an OpenAI API key | Experimental sandbox example; paid live-provider smoke is opt-in. |
| Claude Code, OpenCode, agy | No end-to-end managed-agent experiment fixture yet. |
| Kubernetes or managed execution | Deferred; Docker is the first backend. |
The workflow guide covers telemetry gaps, recovery,
cost limits, and the benchmark recipe. Local samples and their limits are in
docs/performance-results.json. CI includes
a separate installed-wheel Docker job.
Stats
from tbcore import regressed, summary
print(summary([0.8, 0.9, 0.85, 0.7])) # mean, stddev, bootstrap CI
# Lower-is-better metric (cost, latency): is the candidate significantly higher?
print(regressed([1.0, 1.1, 0.9], [1.2, 1.3, 1.1]).regressed)
# Higher-is-better metric (accuracy): is the candidate significantly lower?
print(regressed([0.9, 0.8, 0.9], [0.7, 0.8, 0.7], higher_is_better=True).regressed)
effect_size is Cohen's d with a fixed sign convention (positive means the candidate mean
is higher) and is nan when undefined (too little data or zero variance).
bootstrap_ci and cohens_d are exported too.
Development
python -m pip install -e ".[dev]"
ruff check .
ruff format --check .
mypy
python -m pytest -m "not installed_wheel and not live" --cov=tbcore --cov-report=term-missing
See CONTRIBUTING.md for test markers and image maintenance. CI runs these checks on Python 3.11-3.13 (Linux), plus 3.12 on Windows and macOS, with a separate installed-wheel Docker job. Docker tests skip when no container runtime is available; paid live-provider tests are excluded.
License
MIT - see LICENSE.
Metadata
Release files for tbcore 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tbcore-0.2.0.tar.gz | 120.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tbcore-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 196.8 kB
Release files / tbcore-0.2.0.tar.gz
| Download URL | tbcore-0.2.0.tar.gz |
|---|---|
| Size | 120.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
96a3c890eee589b585e9fc918ea959054307c8593fb0005d1a1fc288ea59b196
|
|
BLAKE2b-256 checksum How to use checksums |
5d8a79c3521a9eb6b1cdfa8cd384278f7eac9f4df225705e8c0c8cff68c8d06c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / tbcore-0.2.0-py3-none-any.whl
| Download URL | tbcore-0.2.0-py3-none-any.whl |
|---|---|
| Size | 76.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0a1a6b2b9b6f5114608d6c9722ee55a93bf60bbaecec9b54b95e0d3cce7dca21
|
|
BLAKE2b-256 checksum How to use checksums |
e694ba5191ca012dc1cc19217c1503b03dc1b34cf873961ffb3ccb61242efb0a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency log