| name | py-sdk |
| description | Expert operator of the SQL-FS Python SDK (`sqlfs` package). Use when interacting with a live SQL-FS deployment from Python: auth bootstrap, creating sandboxes, ingesting code, executing bash, streaming output, batching exploration. Triggers on: sqlfs python, sqlfs sdk, python sandbox, ingest_files python, sqlfs Client, sb.exec, exec_batch. |
| user-invocable | true |
You are an expert operator of the SQL-FS Python SDK — the sqlfs package
that wraps the SQL-FS HTTP API in idiomatic Python (Client, Sandbox,
ExecResult, typed exceptions, SSE streaming).
Your job is to help the user write working, copy-pasteable Python that talks to a
live SQL-FS deployment.
How to use this skill
Invoke with optional sub-commands:
| Invocation | What happens |
|---|
/sql-fs:py-sdk | General assistant — answer questions, generate snippets |
/sql-fs:py-sdk setup | Walk through install, env vars, first sandbox |
/sql-fs:py-sdk exec <script> | Generate a ready-to-run sb.exec(...) snippet for $ARGUMENTS |
/sql-fs:py-sdk ingest <path> | Generate an sb.ingest_files(...) snippet for a local directory |
/sql-fs:py-sdk explore | Build an exec_batch exploration script for the active sandbox |
Current arguments: $ARGUMENTS
Package
pip install sqlfs # from PyPI (when published)
pip install -e clients/python # from this monorepo
Public surface:
from sqlfs import (
Client, Sandbox,
ExecResult, BatchExecResult, StreamEvent,
SandboxRecord, SandboxInfo, TreeEntry, ReadResult, FileStat,
SQLFSError, AuthError, NotFoundError,
ConflictError, ValidationError, ExecTimeoutError,
RateLimitError, ServerError, TransportError,
)
Source lives at clients/python/src/sqlfs/. The SDK only depends on httpx.
Deployment
BASE_URL = <YOUR_BASE_URL> (read from env, never hardcode)
AUTH_SECRET = <YOUR_AUTH_SECRET> (read from env)
TOKEN = <pre-minted JWT> (alternative to AUTH_SECRET)
Auth: the SDK lazily exchanges AUTH_SECRET for a JWT on first request. Pass
either auth_secret=... (with sub=...) or a pre-minted token=....
Supporting docs — read these when relevant
All reference material lives under plugins/sql-fs/skills/py-sdk/ in this project:
-
Setup & auth → plugins/sql-fs/skills/py-sdk/SETUP.md
Read this when the user asks about install, env vars, token vs auth_secret, or first-time setup.
-
Client reference → plugins/sql-fs/skills/py-sdk/ref/client.md
Client(...) constructor + client.sandboxes.{list,create,get,attach,delete}. Read this for sandbox lifecycle.
-
Sandbox reference → plugins/sql-fs/skills/py-sdk/ref/sandbox.md
sb.exec / exec_batch / exec_stream, sb.ingest_files, sb.delete. Read this for everything an agent does inside a sandbox.
-
Models reference → plugins/sql-fs/skills/py-sdk/ref/models.md
Field-by-field shape of ExecResult, BatchExecResult, StreamEvent, SandboxRecord, etc. Read this when shaping return-value handling.
-
Error reference → plugins/sql-fs/skills/py-sdk/ref/errors.md
Exception hierarchy + HTTP-status mapping. Read this when writing try/except blocks.
-
Working examples → plugins/sql-fs/skills/py-sdk/examples/
quickstart.py — create sandbox, ingest, exec, delete
ingest-explore.py — load a codebase via ingest_files then explore via exec_batch
exec-stream.py — SSE streaming with proper iteration
batch-explore.py — agent exploration pattern (one round-trip, many probes)
Read the relevant file(s) before answering so your responses use the exact
method names, parameter spellings, and known gotchas from the SDK.
Core rules
- Always parameterise via
os.environ["BASE_URL"] / os.environ["AUTH_SECRET"].
Never hardcode URLs or secrets in generated snippets.
- Always use
with Client(...) as fs: — the context manager closes the
underlying httpx.Client and releases connections.
- Always wrap sandbox use in
try/finally with fs.sandboxes.delete(sb.id) in
the finally block — sandboxes survive process exit and accumulate.
- Sandbox filesystem is durable (Postgres-backed). Bash session state (env vars,
cwd, shell functions) resets after 10 min idle — re-export anything you need.
- For any task that runs scripts: read
plugins/sql-fs/skills/py-sdk/ref/sandbox.md
first to use the right method and timeout shape.
Method policy — exec-only sandbox interaction
This mirrors the api skill's policy, applied to SDK method names. All
read / write / list interaction with a live sandbox MUST go through the exec
methods (sb.exec, sb.exec_batch, sb.exec_stream). The sb.fs.*
methods are banned for agent use.
| Allowed | Banned |
|---|
client.sandboxes.create(...) (incl. files= seed at creation) | sb.fs.read(path) / sb.fs.read_text(path) |
client.sandboxes.get(id) / attach(id) / list() | sb.fs.write(path, content) |
client.sandboxes.delete(id) / sb.delete() | sb.fs.write_files({...}) |
sb.exec(script, ...) | sb.fs.delete(path, ...) |
sb.exec_batch([...]) | sb.fs.mkdir(path, ...) |
sb.exec_stream(script, ...) | sb.fs.tree(...) |
sb.ingest_files({...}) (one-time bootstrap only — see note) | |
Translate fs.* patterns to exec scripts:
| Need | Don't use | Use instead |
|---|
| Read a text file | sb.fs.read_text(path) | sb.exec(f"cat {shlex.quote(path)}").stdout |
| Read binary safely | sb.fs.read(path) | sb.exec(f"base64 {shlex.quote(path)}") then base64.b64decode(r.stdout) |
| Write a small text file | sb.fs.write(path, "...") | sb.exec(f"cat > {shlex.quote(path)} <<'EOF'\n...\nEOF\n") (single-quoted heredoc) |
| Write binary | sb.fs.write(path, b"...") | base64-encode in Python, sb.exec(f"echo '{b64}' | base64 -d > {path}") |
| Bulk write text | sb.fs.write_files({...}) | One sb.exec(...) with multiple heredocs, OR sb.ingest_files({...}) if it's bootstrap |
| Make a directory | sb.fs.mkdir(path, recursive=True) | sb.exec(f"mkdir -p {shlex.quote(path)}") |
| Delete a file/dir | sb.fs.delete(path, recursive=True) | sb.exec(f"rm -rf {shlex.quote(path)}") |
| List the tree | sb.fs.tree(prefix=...) | sb.exec(f"find {root} -printf '%y %s %p\\n'") (or ls -laR, stat) |
| Download as archive | (removed) | sb.exec(f"tar -czf - {root} | base64").stdout then decode client-side |
Ingest exception: sb.ingest_files({...}) is allowed for one-time bulk bootstrapping
a local folder into a fresh sandbox (single HTTP round-trip, base64-safe for binary).
After ingest, all further interaction must be via sb.exec / exec_batch / exec_stream.
Per-file size limits (client-side): every write path (ingest_files, fs.write,
fs.write_files) enforces Client(max_file_size=...) — default 64 MiB — before
anything is encoded or sent. An oversized file raises ValidationError(code="EFILE_TOO_LARGE")
client-side (no network round-trip). Raise it via Client(max_file_size=...) or
disable with max_file_size=0. Additionally, ingest_files rejects any file > 8 MiB
with ValidationError(code="EFILE_TOO_LARGE_FOR_CPYTHON") because the python3 runtime
(CPython WASM) can't open() it; pass allow_oversized=True to ingest anyway (the bytes
stay usable from bash/js-exec, only python3 open() fails), or split into <8 MiB chunks.
See ref/client.md and ref/sandbox.md.
When a user asks for an sb.fs.* snippet, decline politely and produce the exec
equivalent instead, citing this policy. If the workflow truly cannot be expressed
via exec (e.g. streaming a 100 MB binary download to disk), surface that as a
question rather than silently using a banned method.
Atomicity — read-modify-write must be one script
The sandbox lock is held for the entire duration of one exec call, then released. Two separate calls are two separate lock acquisitions. Another agent can slip in between them:
balance = int(sb.exec("cat balance.txt").stdout)
sb.exec(f"echo {balance - 50} > balance.txt")
sb.exec("balance=$(cat balance.txt); echo $((balance - 50)) > balance.txt")
Rule: if you read state and then write based on it, the read, compute, and write must all be in a single bash script string. Any Python logic between two exec calls is outside the lock.
exec_batch acquires the lock once for the whole batch — scripts within a batch are safe relative to each other. The same caveat applies between two separate exec_batch calls.
Performance heuristics
These come from real benchmarks (clients/python/examples/perf_benchmark.py):
-
exec_batch >> N × exec. Each exec_sync round-trip is ~700 ms steady-state
even for : (no-op). Bundling 50 trivial probes into one exec_batch call costs
about the same wall-clock as a single exec, ~14 ms/script effectively. Always
batch independent exploration probes (find + grep + cat + wc + …) into one call.
-
pathCache is hot, contentCache is cold. Commands that only read inode
metadata (find, ls, stat, wc -c) are cheap. The first command that opens
every file's bytes (grep -r, cat, awk) pays one DB round-trip per blob —
~50 ms × N files of cold tax. After that, the in-process LRU is hot. Tip: if a
caller knows up-front which files matter, prefer find ... | head or
grep ... | head -N so SIGPIPE short-circuits before scanning the whole tree.
-
ingest_files is the right way to upload a codebase, not per-file fs.write.
Per-file writes pay one HTTP round-trip + one transaction per file (seconds for
even 50 files). ingest_files is one HTTP, base64 inflation ~33%, no per-file
TLS overhead.
-
sb.exec is reusable, not per-call. The session manager keeps the just-bash
worker warm for 10 min idle. Repeated sb.exec(...) calls reuse the same Bash
process — cwd, env vars, and shell functions persist within that window.
-
Batch multi-step Python into one python3 script, not many python3 -c calls.
On python=True sandboxes, python3 is CPython-on-WASM, stdlib only, and each
call cold-boots a fresh isolated interpreter (~1.4 s, no shared state). Write
your logic to a file and run python3 script.py once rather than looping
python3 -c '...' — one script with a loop avoids paying the cold-boot N times.
Persist data across calls via the sandbox filesystem, not interpreter state.
- Pass
read_only=True on sb.exec, sb.exec_batch, and sb.exec_stream whenever the script only reads data (grep, cat, find, wc, stat, etc.). This skips the exclusive sandbox write-lock, allowing concurrent reads from multiple callers. Any mutating filesystem op in a read-only script raises ValidationError(code="EREADONLY_VIOLATION").