isb from code
Everything isb up and isb exec do is available from code, for test
harnesses, CI tools and agent frameworks that make sandboxes on the fly. The
Python and TypeScript SDKs drive the isb binary over isb rpc, a
line-delimited JSON protocol on its stdin and stdout (The rpc
protocol); the Rust crate is isb itself. All three use
the same engine, so a spec behaves the same whether it came from YAML, Python
or TypeScript, and the spec objects use the compose file's field names
exactly (isb.yaml reference).
Like the CLI, they need access to the incus socket ($INCUS_SOCKET, else
$INCUS_DIR/unix.socket, else /var/lib/incus/unix.socket), which usually
means membership in incus-admin, and that access is root-equivalent on the
host.
Python#
pip install isb-sdk # imported as `isb`
Python 3.10 or later on Linux, no runtime dependencies, typed. The platform
wheels (x86_64 and aarch64 Linux) bundle a static isb binary; otherwise the
binary is found as Client(isb_bin=...), then $ISB_BIN, then the bundled
one, then isb on PATH.
import asyncio
from isb import Sandbox, Volume
async def main() -> None:
sb = await Sandbox.connect_or_create(
"web",
image="images:ubuntu/24.04",
cpus=2,
idmap="auto",
volumes=[Volume.bind("./site", "/home/ubuntu/site")],
ports=["8000:8000"],
user="ubuntu",
working_dir="/home/ubuntu/site",
)
out = await sb.exec("uname", ["-a"]) # argv, never a shell string
print(out.stdout_text)
proc = await sb.exec_stream(["sh", "-c", "for i in 1 2 3; do echo $i; sleep 1; done"])
async with proc:
async for event in proc:
print(event.text, end="")
print("exit code:", await proc.wait())
await sb.remove(force=True)
asyncio.run(main())
A compose file:
import isb
project = await isb.Project.load("isb.yaml", vars={"WORKTREE": "/srv/wt"})
for plan in await project.plan():
print(plan.name, plan.status, plan.actions)
for service, report in await project.up(on_progress=print): # like `isb up -d`
print(service, report.created, report.ports)
await project.sandbox("web").exec(["bun", "install"]) # with the service's exec defaults
await project.down(volumes=True)
The full API (the client, Sandbox, exec and exec_stream, the Volume
and PortBinding builders, Project, volumes, prune and the error classes) is
in sdk/python/README.md.
TypeScript#
bun add @execution-associates/isb # or npm install
Bun first; Node 20 or later works too. ESM only, no runtime dependencies. The
optional packages @execution-associates/isb-linux-x64 and -linux-arm64
carry the binary; otherwise it is found as new Client({ isbBin }), then
$ISB_BIN, then the platform package, then isb on PATH.
import { Sandbox, Volume } from "@execution-associates/isb";
const sb = await Sandbox.connectOrCreate({
container_name: "web",
image: "images:ubuntu/24.04",
cpus: 2,
idmap: "auto",
volumes: [Volume.bind("./site", "/home/ubuntu/site")],
ports: ["8000:8000"],
user: "ubuntu",
working_dir: "/home/ubuntu/site",
});
const out = await sb.exec("uname", ["-a"]);
console.log(out.stdoutText);
const proc = await sb.execStream(["sh", "-c", "for i in 1 2 3; do echo $i; sleep 1; done"]);
const decoder = new TextDecoder();
for await (const event of proc) {
process.stdout.write(decoder.decode(event.data));
}
console.log("exit code:", await proc.wait());
await sb.remove({ force: true });
Project.load({ files, vars }) with plan(), up(), down() and
sandbox(service) works as in Python. The full API is in
sdk/typescript/README.md.
Rust#
cargo add isb
use isb::{Client, Sandbox, SandboxSpec, Volume};
fn main() -> isb::Result<()> {
let client = Client::new();
let spec = SandboxSpec::new("web", "images:ubuntu/24.04")
.cpus(2)
.volume("/home/ubuntu/site", Volume::bind("./site"));
let sb = Sandbox::connect_or_create(&client, &spec)?;
let out = sb.exec(["uname", "-a"])?;
print!("{}", out.stdout_text());
Ok(())
}
API docs are on docs.rs/isb.
What every SDK shares#
- Converge, don't recreate.
connect_or_create/connectOrCreatecreates what is missing and changes only what differs; a correct mount is never remounted.planshows the difference first. - argv stays a list. Nothing is joined into a shell string; use
["sh", "-c", "..."]when you want a shell. A non-zero exit is a result, not an exception. - Exec defaults travel with the spec. A handle from
connect_or_createorProject.sandboxruns commands as the spec'suser, in itsworking_dir, with itsexec.env; per-call arguments override them. - Streaming gives output as it is produced, stdin writes, signals and terminal resizes.
- A service's
commandis for the foreground CLIisb up;Project.upbehaves likeisb up -dand does not run it. - Errors carry a stable code (
not_found,already_exists,not_ready,connect, ...) from the rpc protocol.
Another language can speak the same protocol: start isb rpc, read its hello
line, and send one JSON request per line.
Driving isb serve instead#
The SDKs manage sandboxes on the local incus. To deploy apps or stacks to an
isb serve daemon, possibly remote, call its tools over REST or MCP with an
API token (HTTP API, Agents and MCP):
curl -sS https://isb.example.com/orgs/acme/api/v1/tools/app_deploy \
-H "Authorization: Bearer $ISB_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "web", "wait": true}'