Tenki Cloud Python SDK — cloud sandboxes (microVMs) for AI agents and code execution
Project description
Tenki Python SDK
Python SDK for Tenki: cloud sandboxes (microVMs) for AI agents and code execution.
pip install tenki
The SDK is published as
tenki. Existing code that doesimport tenki_sandboxkeeps working (that module ships insidetenki), but new code should useimport tenki.
Protobuf compatibility
The SDK supports protobuf>=5.29.5 with no upper bound. Its Python bindings are
temporarily generated with protobuf 5.29.5 so applications that require
protobuf<6 can install the SDK alongside dependencies such as Memori.
Protobuf 6.31.x emits a benign warning when it loads 5.29.5-generated bindings. If a test suite pins that runtime and promotes warnings to errors, scope the exception narrowly instead of disabling protobuf's runtime checks:
[tool.pytest.ini_options]
filterwarnings = [
"ignore:Protobuf gencode version 5\\.29\\.5 is exactly one major version older than the runtime version 6\\.31\\..*:UserWarning:google\\.protobuf\\.runtime_version",
]
from tenki import Sandbox
# create() waits by default via a single server-held request and returns an
# exec-ready sandbox with data-plane access primed (wait=False to skip).
with Sandbox.create(cpu_cores=2, memory_mb=4096) as sb:
result = sb.exec("python3", "-c", "print('hello')")
result.check()
print(result.stdout_text)
# fs paths are relative to the sandbox workdir (absolute paths must stay inside it)
sb.fs.write_text("input.txt", "data")
print(sb.fs.read_text("input.txt"))
preview = sb.expose_port(3000, ttl=3600)
print(preview.url)
The API key determines the Workspace automatically; ordinary Sandbox calls do not require a Workspace ID.
Async (asyncio)
AsyncClient / AsyncSandbox expose the same surface with native async/await,
built on grpc.aio — no asyncio.to_thread wrapping required. Use it inside any
asyncio server (FastAPI, aiohttp, etc.).
import asyncio
from tenki import AsyncSandbox
async def main():
async with await AsyncSandbox.create(cpu_cores=2) as sb:
result = await sb.exec("python3", "-c", "print('hello')")
result.check()
print(result.stdout_text)
await sb.fs.write_text("input.txt", "data")
print(await sb.fs.read_text("input.txt"))
proc = await sb.start("bash", "-lc", "read name; echo hi $name")
await proc.write_stdin("tenki\n")
await proc.close_stdin()
async for chunk in proc.stdout:
print(chunk.decode(), end="")
(await proc.wait()).check()
asyncio.run(main())
The sync Client / Sandbox remain available for non-async consumers. SSH,
dial, and host-port tunnels are also async: AsyncSandbox.ssh() returns an
AsyncSSHConn (raw async read/write/close, matching the TS/Go SDKs; needs
the tenki[async] extra for websockets), dial() returns an
AsyncDialConn, and expose_host_port() / expose_host_port_resilient() return
async tunnels with await tunnel.terminated.wait() and on_terminated callbacks.
Auth
Auth resolution:
auth_token=passed toClientorSandbox.createTENKI_AUTH_TOKENTENKI_API_KEY
TENKI_API_ENDPOINT overrides the API URL; legacy TENKI_API_URL is also accepted.
Process API
exec collects stdout/stderr and returns a result:
result = sb.exec("npm", "test", cwd="project", timeout=60, env={"CI": "1"})
print(result.stdout_text)
result.check()
start returns a live process:
proc = sb.start("bash", "-lc", "read name; echo hello $name")
proc.write_stdin("tenki\n")
proc.close_stdin()
for chunk in proc.stdout:
print(chunk.decode(), end="")
proc.wait().check()
Use shell() when you want shell parsing:
sb.shell("python3 -m http.server 3000 >/tmp/server.log 2>&1 &")
Process cwd values follow the guest contract: relative paths are normalized
under the sandbox guest workdir (/home/tenki by default), absolute paths are
used unchanged, and missing or non-directory targets fail before the process
starts.
Sandbox lifetime
Long-lived sandboxes are a parameter choice at create(), not a separate API:
sb = client.create(
sticky=True, # long-lived session: not reaped on idle
idle_timeout_minutes=120, # or: generous idle window before auto-pause
max_duration=8 * 3600, # total lifetime cap (seconds)
pause_retention=24 * 3600,# how long a paused session is kept resumable
)
max_durationcaps total lifetime;sb.extend(seconds)pushes the deadline (sb.info.timeout_at) while running.idle_timeout_minutesauto-pauses an idle sandbox;sb.resume()brings it back with the filesystem intact.sticky=Trueopts the session out of idle reaping for keep-warm use cases (workspaces cap concurrent sticky sessions).client.list(sticky=True)filters for long-lived sessions in the API key's Workspace.
SSH
For tools that speak SSH (paramiko, scp, IDE remote dev), the SDK can mint a short-lived OpenSSH user certificate for your session and open a transport to the sandbox SSH gateway. No keys are provisioned into the guest; the engine signs your local public key and the gateway verifies the certificate.
Requires pip install websocket-client paramiko (websocket-client for the
transport, paramiko if you want a client in-process).
import subprocess
import paramiko
# 1. local keypair (any OpenSSH key works; ed25519 shown)
subprocess.run(["ssh-keygen", "-t", "ed25519", "-N", "", "-q", "-f", "id_tenki"], check=True)
# 2. engine signs a short-lived user cert for this sandbox
cert = sb.issue_ssh_cert(open("id_tenki.pub").read(), ttl=600)
open("id_tenki-cert.pub", "w").write(cert.ssh_cert)
# 3. open the gateway transport and run a paramiko session over it
pkey = paramiko.Ed25519Key.from_private_key_file("id_tenki")
pkey.load_certificate("id_tenki-cert.pub")
transport = paramiko.Transport(sb.ssh()) # WebSocket-backed socket
transport.connect(username="tenki", pkey=pkey)
session = transport.open_session()
session.exec_command("echo hello-over-ssh")
print(session.makefile().read().decode())
transport.close()
Notes:
sb.ssh()/client.ssh(session_id)discover an active gateway and returnSSHConn, anio.RawIOBasesocket usable anywhere paramiko accepts one.- The SSH username is
tenki. TENKI_SANDBOX_GATEWAY_URLoverrides the gateway WebSocket URL (it is otherwise derived from the API endpoint).- Certificate RPCs use the Connect protocol over HTTPS (same as the Go SDK and
the
tenkiCLI), so they work through standard HTTP load balancers. sb.update_ssh_authorized_keys([...])additionally plants long-lived public keys in the guest'sauthorized_keysif you prefer key-based auth for the in-guest sshd.
Resource APIs
from tenki import Client, GiB
client = Client()
volume = client.volumes.create(
name="cache",
size_bytes=10 * GiB,
)
preview = client.preview_urls.create(
slug="demo",
session_id=sb.id,
port=3000,
)
Registry
Delete an untagged, non-latest, unshared registry version by image and snapshot ID:
result = client.registry.delete_version(
"11111111-1111-1111-1111-111111111111",
"55555555-5555-5555-5555-555555555555",
)
Templates (typed builder)
TemplateSpec is an immutable typed recipe (every builder call returns a new
value). Builds freeze the normalized spec + spec_hash server-side and return
a private digest-addressed Image, the normal sandbox launch source.
from tenki import Client, TemplateSpec
client = Client()
spec = (
TemplateSpec() # defaults to base image "sandbox"
.from_image("sandbox-v2") # or .from_template(...) / .from_snapshot(...)
.with_git_context("https://github.com/acme/node-api", "main")
.run("npm ci", name="Install dependencies")
.runtime_env({"NODE_ENV": "production"})
.start(["npm", "start"], ready_when=[{"http": "http://localhost:3000/health"}])
)
spec.validate() # aggregate local validation; server stays authoritative
template = client.templates.create(name="node-api", spec=spec)
build = client.templates.build(
template, # resource objects in, IDs as fallback
build_secrets={"GITHUB_TOKEN": token}, # explicit request-time values
wait_for_completion=True,
on_event=lambda e: print(e), # one ordered log/progress handler
)
sb = client.create(image=build.image, wait_for_runtime=True)
wait_build(build_id) reconnects to an existing build (ordered, deduplicated
events). Local interruption (Ctrl-C or cancel_event) stops observation only;
client.templates.cancel_build(build) cancels remotely. Waited failures raise
TemplateBuildFailedError carrying the final redacted build; runtime waits
raise TemplateRuntimeFailedError without terminating the running sandbox.
Strict authored JSON import/export: spec.to_json() / TemplateSpec.from_json(text).
Checkout mode uses "contents" or "directory"; runtime runAt,
restartPolicy, and snapshotMode use short values such as "build",
"on-failure", and "memory". Full protobuf enum names remain accepted;
unknown fields and enum values are rejected.
See examples/template_builder.py for the full filesystem/memory workflow.
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 tenki-0.5.1.tar.gz.
File metadata
- Download URL: tenki-0.5.1.tar.gz
- Upload date:
- Size: 184.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a1ed4e753db3d4a38a71cfb8d9cbf0cd05b28463f4c718fc9ec37f3155f28b4c
|
|
| MD5 |
62b49b0aa7b44fd5b5acaab4ae62292b
|
|
| BLAKE2b-256 |
062440a827e1795d7cafaffbf615c3b13ad5aa8462359db01bab969f3f00913b
|
Provenance
The following attestation bundles were made for tenki-0.5.1.tar.gz:
Publisher:
pypi-publish.yml on LuxorLabs/tenki-sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tenki-0.5.1.tar.gz -
Subject digest:
a1ed4e753db3d4a38a71cfb8d9cbf0cd05b28463f4c718fc9ec37f3155f28b4c - Sigstore transparency entry: 2241625773
- Sigstore integration time:
-
Permalink:
LuxorLabs/tenki-sdk-python@36f5d8ff26410f220db55f80920e4719084453d4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/LuxorLabs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
pypi-publish.yml@36f5d8ff26410f220db55f80920e4719084453d4 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tenki-0.5.1-py3-none-any.whl.
File metadata
- Download URL: tenki-0.5.1-py3-none-any.whl
- Upload date:
- Size: 158.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e296544c85b8709f85e84728b5de0c4cb42009fac5475899a1ac238b99ab459
|
|
| MD5 |
39729809459236ffd5597da8276dcd74
|
|
| BLAKE2b-256 |
58cbcba98c901307d5e266a072015d7963834f28ba14cf74598149a98093c655
|
Provenance
The following attestation bundles were made for tenki-0.5.1-py3-none-any.whl:
Publisher:
pypi-publish.yml on LuxorLabs/tenki-sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tenki-0.5.1-py3-none-any.whl -
Subject digest:
2e296544c85b8709f85e84728b5de0c4cb42009fac5475899a1ac238b99ab459 - Sigstore transparency entry: 2241626036
- Sigstore integration time:
-
Permalink:
LuxorLabs/tenki-sdk-python@36f5d8ff26410f220db55f80920e4719084453d4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/LuxorLabs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
pypi-publish.yml@36f5d8ff26410f220db55f80920e4719084453d4 -
Trigger Event:
workflow_dispatch
-
Statement type: