Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

boxkite_client-0.1.0.tar.gz (25.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

boxkite_client-0.1.0-py3-none-any.whl (16.3 kB view details)

Uploaded Python 3

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

Hashes for boxkite_client-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6a010a732182200ac9fd6efcc96496634b8111e15da2d104d8c204790b6a40ae
MD5 e3eb633b4883547330233f3e109cc08a
BLAKE2b-256 5d4518177bb4a4aee5706202a39fdb3b9b2f0a338bf0931a0bff0ffe7fc34436

See more details on using hashes here.

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

Hashes for boxkite_client-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 25a102737283f9b48754957594df21f6f742a2debb4a736d06657b007eb0396a
MD5 c921072c58be6a2333b6fdfb10d1f975
BLAKE2b-256 85450066cd754b264b93b195a636646bf232fe35f411c2bfdf025fe7244181e4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page