Python client for a hosted boxkite control-plane (sandbox creation, exec, files, LangChain tools).
Project description
boxkite-client
A Python client for a hosted boxkite control-plane -- create sandboxes,
run commands, edit files, and (optionally) get LangChain tools wired to a
session, all over HTTP. This is not the boxkite package itself
(boxkite/boxkite-sandbox, which embeds SandboxManager directly against
your own Kubernetes cluster) -- use this when you're talking to someone
else's running control-plane, hosted or self-hosted, over its API.
Install
pip install boxkite-client
pip install boxkite-client[langchain] # for create_sandbox_tools
(Not yet published to PyPI -- install from this directory with pip install -e .
until it is.)
Quickstart
from boxkite_client import BoxkiteClient
client = BoxkiteClient(base_url="https://your-control-plane.example.com", api_key="bxk_live_...")
with client.sandbox(label="demo") as sb:
result = sb.exec("python3 -c 'print(1 + 1)'")
print(result["stdout"]) # "2\n"
sb.file_create("notes.txt", "hello from boxkite-client\n")
print(sb.view("notes.txt")["content"])
# sandbox is destroyed automatically here, even if an exception was raised above
Read-only search over the workspace -- ls, glob, grep:
sb.ls() # {"entries": [...]} -- direct children of "/"
sb.ls(path="/src") # direct children of "/src"
sb.glob("**/*.py") # {"matches": [...]} -- find files by name pattern
sb.grep("TODO", path="/src", glob="*.py") # {"matches": [...], "error": None, "truncated": False}
client.ls(session_id, ...), client.glob(session_id, pattern, ...), and
client.grep(session_id, pattern, ...) are the same calls without a
SandboxSession wrapper. grep's max_matches defaults to 500.
Background processes/sessions -- start a long-running process (a dev
server, a test watcher, a long build, a REPL) that keeps running after the
call returns, distinct from exec()'s one-shot request/response:
proc = sb.start_process("npm run dev", max_runtime_seconds=1800) # {"process_id", "status", "started_at"}
sb.get_process_output(proc["process_id"]) # {"status", "stdout_chunk", "next_offset", "truncated", "exit_code"}
sb.send_process_input(proc["process_id"], "y\n")
sb.stop_process(proc["process_id"]) # {"status", "exit_code"}
sb.list_processes() # {"processes": [...]}
This is polling-style, not streaming, and a backgrounded process is not
reachable over the network from any other call -- the same per-exec
network isolation applies here too. See docs/PROCESS-SESSIONS-DESIGN.md
for the full design and its stated non-goals.
Audit log and live watch (see docs/SANDBOX-OBSERVABILITY-DESIGN.md) --
every exec/file operation a sandbox runs is durably logged, readable as
paginated history or as a live feed:
sb.get_log(limit=50, offset=0) # {"entries": [...]} -- paginated exec/file-op history
for entry in sb.watch(): # blocks, yielding one dict per logged operation as it happens
print(entry["operation"], entry.get("detail"))
watch() is a live feed of completed exec/file operations, one per logged
event -- not a live terminal stream of a command's stdout mid-run. watch()
blocks the calling thread for as long as the stream stays open, so iterate it
from a dedicated thread if you need it alongside other work.
Interactive human takeover of a sandbox (a real PTY, proxied over
WS .../takeover) is a materially different problem -- see the design doc
section 3 for why -- but it is a separate call, takeover():
with sb.takeover() as ws: # a websockets ClientConnection -- raw bytes, no envelope
ws.send(b"ls -la\n")
print(ws.recv())
There is no message envelope: .send()/.recv() raw bytes exactly as you
would over a local terminal. A missing/invalid API key, or a session that
isn't yours or was already destroyed, closes the connection with WS close
code 4401/4404 respectively -- this surfaces as
websockets.exceptions.ConnectionClosed the first time you .send() or
.recv(), not at takeover()/connect() time. See docs/API.md's
WS .../takeover section for the full auth and logging contract -- every
takeover session is logged to the same audit trail as get_log/watch,
which is the load-bearing mitigation for shipping this without
fine-grained RBAC yet.
Without the context manager, you own create/destroy yourself:
sandbox = client.create_sandbox(label="manual")
client.exec(sandbox["id"], "echo hi")
client.destroy_sandbox(sandbox["id"])
create_sandbox (and client.sandbox(...)) also accept these optional
keyword arguments:
| Argument | Default | Meaning |
|---|---|---|
size |
"small" |
CPU/memory size preset: "small", "medium", or "large" |
storage_gb |
control-plane default | Overrides the sandbox's workspace/uploads/outputs/skills volume size (GB) |
lifetime_minutes |
control-plane default | Overrides how long the sandbox stays alive before the reaper tears it down |
count |
1 |
Create a batch of count sandboxes in one call, returned as a list |
All of these are bounded by fair-use ceilings on your account, never a paid upgrade -- a request above your account's fair-use limit fails the same way a concurrent-sandbox or monthly-usage limit does (see "Error handling" below).
sandbox = client.create_sandbox(label="build-job", size="medium", storage_gb=20, lifetime_minutes=120)
sandboxes = client.create_sandbox(label="fanout", count=3) # batch-create 3 sandboxes
Async
Same shapes, AsyncBoxkiteClient + async with:
import asyncio
from boxkite_client import AsyncBoxkiteClient
async def main():
client = AsyncBoxkiteClient(base_url="https://your-control-plane.example.com", api_key="bxk_live_...")
async with client.sandbox() as sb:
result = await sb.exec("echo hi")
print(result["stdout"])
await client.aclose()
asyncio.run(main())
LangChain agent
from boxkite_client import BoxkiteClient
from boxkite_client.langchain_tools import create_sandbox_tools
client = BoxkiteClient(base_url="https://your-control-plane.example.com", api_key="bxk_live_...")
sandbox = client.create_sandbox(label="agent-session")
tools = create_sandbox_tools(client, sandbox["id"])
# tools = [bash_tool, file_create, view, str_replace, ls, glob, grep,
# start_process, get_process_output, send_process_input, stop_process,
# list_processes] -- hand these to any LangChain/LangGraph agent, same
# names/shapes as boxkite.tools' own factory.
Command allowlist
get_allowed_commands() / set_allowed_commands(rules) /
clear_allowed_commands() (each available sync and async) manage a
per-account, persisted allowlist enforced on every future exec() call
against /v1/sandboxes/{id}/exec. Empty/unset (the default) means
unrestricted.
client.get_allowed_commands() # {"rules": [...]} -- [] means unrestricted
client.set_allowed_commands([
"git",
{"command": "python3", "args_allow": [r"^-c .*"]},
])
client.clear_allowed_commands() # back to unrestricted
This is an opt-in guardrail, not a sandbox-escape boundary — see the root
README's The boxkite CLI
section for why. A command that doesn't match any rule raises
BoxkiteApiError with .code == "command_not_allowed".
Error handling
Every non-2xx response raises BoxkiteApiError (with .status_code, .code,
.message from the control-plane's error envelope). A network-level failure
(DNS, TLS, timeout) raises BoxkiteConnectionError. Both subclass BoxkiteError.
from boxkite_client import BoxkiteApiError
try:
client.exec(sandbox["id"], "echo hi")
except BoxkiteApiError as exc:
if exc.code == "concurrent_sandbox_limit_reached":
... # back off, destroy an old session, etc.
Quota/usage before you hit a limit: client.usage() returns
monthly_sandbox_hours_used/_limit and concurrent_sandboxes/_limit.
Development
pip install -e ".[dev,langchain]"
pytest tests/
Tests mock the control-plane with httpx.MockTransport -- no real deployment needed.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file boxkite_client-0.1.0.tar.gz.
File metadata
- Download URL: boxkite_client-0.1.0.tar.gz
- Upload date:
- Size: 25.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a010a732182200ac9fd6efcc96496634b8111e15da2d104d8c204790b6a40ae
|
|
| MD5 |
e3eb633b4883547330233f3e109cc08a
|
|
| BLAKE2b-256 |
5d4518177bb4a4aee5706202a39fdb3b9b2f0a338bf0931a0bff0ffe7fc34436
|
File details
Details for the file boxkite_client-0.1.0-py3-none-any.whl.
File metadata
- Download URL: boxkite_client-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25a102737283f9b48754957594df21f6f742a2debb4a736d06657b007eb0396a
|
|
| MD5 |
c921072c58be6a2333b6fdfb10d1f975
|
|
| BLAKE2b-256 |
85450066cd754b264b93b195a636646bf232fe35f411c2bfdf025fe7244181e4
|