bsdkrun/client
A client for a remote bsdkrund daemon’s GraphQL API — the network
counterpart to bsdkrun/sandbox, which shells out to a local bsdkrun
binary. Point one at a daemon (client.new/client.from_env) and get
the same SandboxInfo records back over HTTP + WebSocket instead of a
subprocess.
import bsdkrun/client
import gleam/option.{None}
let assert Ok(c) = client.from_env()
let assert Ok(machines) = client.list(c, all: True)
let assert Ok(res) = client.exec(c, id: "abc123", command: ["uname", "-a"], env: [])
Every function returns Result(_, GraphqlError) — an alias for
bsdkrun/error’s Error type (see that module), extended with the two
variants this client can produce: GraphqlError (a resolver error, a
malformed response, or the daemon being unreachable) and AuthError
(the daemon rejected the bearer token). Pattern-match on
error.GraphqlError/error.AuthError.
Design notes
- HTTP transport:
bsdkrun/graphql_transport, over:httpc— see that module. WebSocket transport (subscriptions):bsdkrun/ws, hand-rolled RFC 6455 framing over:gen_tcp/:ssl— see that module’s doc for the split between its pure, unit-tested protocol functions and its Erlang-FFI connection process. - One shared socket per
Client:Clientitself is an immutable url/token pair (constructing one does not connect anything). The socket is opened lazily on first use and cached — seebsdkrun/ws.ensure. execblocks;shell/follow_logs/subscribedo not.execruns in the calling process and does a boundedsubject.receiveloop itself (see the module doc onbsdkrun/subjectfor whatSubjectis here, givengleam_erlangis not a dependency this SDK can use). The other three each spawn one small background process (via Erlang’s ownspawn/1, called directly through@external) whose only job is to translate the WebSocket connection’s raw events into this module’s richer event types and forward them to aSubjectthe caller reads at its own pace — an unbounded wait, since an idle interactive shell or a quiet log stream legitimately has nothing to say for a long time and should not be timed out for it. A dead connection still always produces a terminal event (the WS actor notifies every open subscription when its socket closes), so this does not risk an actual forever-hang.
Types
BsdOs (daemon/src/graphql.rs ~line 324): which BSD to boot.
pub type BsdOs {
Freebsd
Netbsd
}
Constructors
-
Freebsd -
Netbsd
A handle to a daemon’s GraphQL endpoint: its URL and bearer token.
Constructing one does not connect to anything — the HTTP transport is a
plain request per call, and the WebSocket connection (only needed for
exec/shell/follow_logs/subscribe) is opened lazily on first use.
pub opaque type Client
A bsdkrun/client error — every variant bsdkrun/error.Error has, but
only GraphqlError and AuthError are ones this module itself produces.
pub type GraphqlError =
error.Error
The net field shared by every run* mutation except runFlavor
(daemon/src/graphql.rs’s NetInput, ~line 360).
pub type NetOptions {
NetOptions(
no_net: Bool,
ports: List(String),
mac: option.Option(String),
network: option.Option(String),
name: option.Option(String),
)
}
Constructors
-
NetOptions( no_net: Bool, ports: List(String), mac: option.Option(String), network: option.Option(String), name: option.Option(String), )
RunBsdInput (daemon/src/graphql.rs ~line 406).
pub type RunBsdOptions {
RunBsdOptions(
os: BsdOs,
version: option.Option(String),
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
volume: option.Option(String),
persist: Bool,
force: Bool,
firmware: option.Option(String),
attach_disk: List(String),
disk_size: option.Option(String),
repo: option.Option(String),
command: List(String),
)
}
Constructors
-
RunBsdOptions( os: BsdOs, version: option.Option(String), cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), volume: option.Option(String), persist: Bool, force: Bool, firmware: option.Option(String), attach_disk: List(String), disk_size: option.Option(String), repo: option.Option(String), command: List(String), )
RunFlavorInput (daemon/src/graphql.rs ~line 506).
pub type RunFlavorOptions {
RunFlavorOptions(
name: String,
cpus: option.Option(Int),
mem: option.Option(Int),
ports: List(String),
volume: option.Option(String),
repo: option.Option(String),
)
}
Constructors
-
RunFlavorOptions( name: String, cpus: option.Option(Int), mem: option.Option(Int), ports: List(String), volume: option.Option(String), repo: option.Option(String), )
RunLinuxInput (daemon/src/graphql.rs ~line 384).
pub type RunLinuxOptions {
RunLinuxOptions(
image: String,
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
volume: option.Option(String),
mounts: List(String),
attach_disk: List(String),
env: List(String),
entrypoint: option.Option(String),
initramfs: Bool,
kernel: option.Option(String),
kernel_version: option.Option(String),
console: option.Option(String),
repo: option.Option(String),
command: List(String),
)
}
Constructors
-
RunLinuxOptions( image: String, cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), volume: option.Option(String), mounts: List(String), attach_disk: List(String), env: List(String), entrypoint: option.Option(String), initramfs: Bool, kernel: option.Option(String), kernel_version: option.Option(String), console: option.Option(String), repo: option.Option(String), command: List(String), )
RunNanosInput (daemon/src/graphql.rs ~line 428). Nanos has no agent
(no exec/shell/commit), but does have a root disk, so persist is
the one disk option it takes.
pub type RunNanosOptions {
RunNanosOptions(
image: String,
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
kernel: option.Option(String),
cmdline: option.Option(String),
persist: Bool,
)
}
Constructors
-
RunNanosOptions( image: String, cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), kernel: option.Option(String), cmdline: option.Option(String), persist: Bool, )
RunOsvInput (daemon/src/graphql.rs ~line 467). Like Nanos, no agent —
but it does have a root filesystem, so unlike Unikraft it takes the disk
options.
pub type RunOsvOptions {
RunOsvOptions(
image: String,
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
cmdline: option.Option(String),
disk: option.Option(String),
no_disk: Bool,
attach_disk: List(String),
gic: option.Option(String),
persist: Bool,
volume: option.Option(String),
)
}
Constructors
-
RunOsvOptions( image: String, cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), cmdline: option.Option(String), disk: option.Option(String), no_disk: Bool, attach_disk: List(String), gic: option.Option(String), persist: Bool, volume: option.Option(String), )
RunSolo5Input (daemon/src/graphql.rs). Solo5 (MirageOS) runs under
the solo5-hvt tender rather than libkrun; the unikernel declares its
own network and block devices in its MFT1 manifest note, so only what
the host alone can know is carried: block backing files ("NAME=FILE")
and the args handed to the unikernel itself. Always a single vCPU —
cpus above 1 is warned about and ignored. No disk, no agent.
pub type RunSolo5Options {
RunSolo5Options(
path: option.Option(String),
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
block: List(String),
args: List(String),
)
}
Constructors
-
RunSolo5Options( path: option.Option(String), cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), block: List(String), args: List(String), )
RunUnikraftInput (daemon/src/graphql.rs ~line 449). A unikernel has
no disk and no agent, so this carries none of the volume/persist/
repo/command fields the other guests take.
pub type RunUnikraftOptions {
RunUnikraftOptions(
path: option.Option(String),
cpus: option.Option(Int),
mem: option.Option(Int),
net: option.Option(NetOptions),
cmdline: option.Option(String),
initramfs: option.Option(String),
mounts: List(String),
)
}
Constructors
-
RunUnikraftOptions( path: option.Option(String), cpus: option.Option(Int), mem: option.Option(Int), net: option.Option(NetOptions), cmdline: option.Option(String), initramfs: option.Option(String), mounts: List(String), )
A live, interactive shell session opened with shell. Output streams to
shell_output’s Subject; shell_send/shell_resize/shell_close
drive it — these four function names (there is no single “shell handle”
API in the daemon’s schema to mirror 1:1) are this SDK’s own choice of
shape for the “session ID + a way to write/resize/close it” the design
note asked for.
pub opaque type ShellSession
Values
pub fn ai_agents(
client: Client,
) -> Result(List(types.AiAgent), error.Error)
The coding agents, and whether each one’s sandbox image is built.
pub fn ai_remove(
client: Client,
agent agent: String,
keep_home keep_home: Bool,
) -> Result(types.CommandResult, error.Error)
Remove an agent’s sandboxes, and unless keep_home its saved login too.
pub fn ai_sessions(
client: Client,
) -> Result(List(types.AiSession), error.Error)
Agent sandboxes, newest first.
pub fn ai_shell_command(
client: Client,
agent agent: String,
machine_id machine_id: String,
) -> Result(List(String), error.Error)
The argv that starts the agent’s TUI — pass it to the shell.
pub fn ai_start(
client: Client,
agent agent: String,
cpus cpus: option.Option(Int),
mem mem: option.Option(Int),
workspace workspace: option.Option(String),
new new: Bool,
) -> Result(String, error.Error)
Start (or reuse) a sandbox; returns its machine id.
workspace is a path on the engine’s host — a remote daemon cannot see
your own filesystem. new boots a second sandbox against the same login.
pub fn ai_stop(
client: Client,
agent agent: String,
) -> Result(types.CommandResult, error.Error)
Stop an agent’s sandboxes. Its saved login survives.
pub fn branch(
client: Client,
snapshot snapshot: String,
name name: option.Option(String),
cpus cpus: option.Option(Int),
mem mem: option.Option(Int),
ports ports: List(String),
no_ports no_ports: Bool,
) -> Result(String, error.Error)
Boot a NEW machine from a snapshot — or from a machine, which is snapshotted first — and return the new machine’s id.
The state is cloned, never booted in place, so the source is untouched and
one snapshot can be branched any number of times. An empty ports
inherits the snapshot’s own forwards, with any host port that is already
taken swapped for a free one; no_ports drops them instead.
pub fn commit(
client: Client,
id id: String,
name name: String,
description description: String,
) -> Result(types.CommandResult, error.Error)
Snapshot a machine into a named flavor, like docker commit.
pub fn docker_container(
client: Client,
action action: String,
ids ids: List(String),
) -> Result(types.CommandResult, error.Error)
Act on containers: start / stop / restart / kill / pause / unpause / rm.
pub fn docker_containers(
client: Client,
all all: Bool,
) -> Result(List(types.DockerContainer), error.Error)
Containers in the engine. all: False lists only running ones.
pub fn docker_logs(
client: Client,
id id: String,
tail tail: Int,
) -> Result(String, error.Error)
One container’s logs (stdout+stderr, most recent tail lines).
pub fn docker_start(
client: Client,
cpus cpus: option.Option(Int),
mem mem: option.Option(Int),
mounts mounts: List(String),
no_home no_home: Bool,
publish_bind publish_bind: option.Option(String),
disk_size disk_size: option.Option(String),
) -> Result(types.DockerStatus, error.Error)
Start (or resume) the engine, returning its status once it answers.
Idempotent: the VM has a fixed name, so this resumes the existing one rather than creating a second.
pub fn docker_status(
client: Client,
) -> Result(types.DockerStatus, error.Error)
Is the Docker engine up, and where is its socket?
pub fn docker_stop(
client: Client,
) -> Result(types.CommandResult, error.Error)
Stop the engine. Images and containers stay on its disk.
pub fn exec(
client: Client,
id id: String,
command command: List(String),
env env: List(String),
) -> Result(types.ExecResult, error.Error)
One-shot command execution: openShell (with command, so it runs that
instead of a login shell) + shellOutput + closeShell, exactly the
sequence daemon/README.md’s “Interactive shells over GraphQL” section
describes (open, then subscribe, then wait for exit — no input to
send). Blocks the calling process until the command exits.
pub fn follow_logs(
client: Client,
id id: String,
follow follow: Bool,
boot boot: Bool,
) -> Result(subject.Subject(types.ShellEvent), error.Error)
Follow a machine’s console log live — everything buffered since the
subscription started, then new lines as they’re written. Ends
(ShellClosed) when the underlying bsdkrun logs -f exits, which with
follow: True is when the machine stops.
pub fn from_env() -> Result(Client, String)
Build a client from BSDKRUN_URL/BSDKRUN_TOKEN. BSDKRUN_URL unset is
Error (nothing to connect to). BSDKRUN_URL set without
BSDKRUN_TOKEN is also Error, deliberately — not a silent fallback
to running unauthenticated — mirroring the same rule
daemon/src/client.rs’s RemoteConfig::from_env applies to the gRPC
BSDKRUN_HOST/BSDKRUN_TOKEN pair (a different pair of variables: this
is the GraphQL port, not the gRPC one, so it gets its own).
pub fn get(
client: Client,
id id: String,
) -> Result(option.Option(types.SandboxInfo), error.Error)
A single machine by id, name, or unique id prefix — or None if there is
no such machine.
pub fn list(
client: Client,
all all: Bool,
) -> Result(List(types.SandboxInfo), error.Error)
Machines. all: True includes stopped ones, like bsdkrun ps -a.
pub fn logs(
client: Client,
id id: String,
boot boot: Bool,
) -> Result(String, error.Error)
A machine’s console log as a single string, as it stands right now. Use
follow_logs to watch it live.
pub fn net_options() -> NetOptions
Default networking: attached, no forwards, no mac/network/name override.
pub fn new(url url: String, token token: String) -> Client
Build a client explicitly. url is normalized exactly like
bsdkrun/graphql_transport.normalize_url (and the web UI’s connection
setup): a scheme is assumed if missing, trailing slashes are stripped,
and /graphql is appended if the path doesn’t already end with it.
pub fn remove(
client: Client,
ids ids: List(String),
force force: Bool,
) -> Result(types.CommandResult, error.Error)
Remove one or more machines. force stops any that are still running
first.
pub fn remove_snapshots(
client: Client,
names names: List(String),
) -> Result(types.CommandResult, error.Error)
Delete snapshots and their data. Machines already branched from them are unaffected.
pub fn request(
client: Client,
query query: String,
variables variables: dynamic.Dynamic,
) -> Result(dynamic.Dynamic, error.Error)
Run any query or mutation bsdkrun/client’s typed API does not cover.
variables is a Dynamic — build one from a Dict/List/literal via
gleam/dynamic.from; see bsdkrun_remote_ffi.erl’s dynamic_to_json/1
doc comment for exactly which shapes it understands. Returns the data
field, for the caller to decode the same way bsdkrun/types’s decoders
do (gleam/dynamic/decode).
pub fn restore(
client: Client,
id id: String,
snapshot snapshot: String,
force force: Bool,
backup backup: Bool,
) -> Result(types.CommandResult, error.Error)
Put a machine’s disk state back to one of its snapshots.
force stops the machine first — it holds the very files being replaced.
backup snapshots the state being overwritten, which is a CoW clone and
therefore free. The machine is left stopped.
pub fn rollback(
client: Client,
id id: String,
force force: Bool,
backup backup: Bool,
) -> Result(types.CommandResult, error.Error)
Restore a machine to its most recent snapshot.
pub fn run_bsd(
client: Client,
opts opts: RunBsdOptions,
) -> Result(String, error.Error)
Boot a FreeBSD/NetBSD machine, detached. Returns the new machine’s id.
pub fn run_bsd_options(os: BsdOs) -> RunBsdOptions
Defaults for RunBsdOptions: everything unset/empty/false except os.
pub fn run_flavor(
client: Client,
opts opts: RunFlavorOptions,
) -> Result(String, error.Error)
Boot a saved flavor, detached. Returns the new machine’s id.
pub fn run_flavor_options(name: String) -> RunFlavorOptions
Defaults for RunFlavorOptions: everything unset/empty except name.
pub fn run_linux(
client: Client,
opts opts: RunLinuxOptions,
) -> Result(String, error.Error)
Boot a Linux (OCI) machine, detached. Returns the new machine’s id.
pub fn run_linux_options(image: String) -> RunLinuxOptions
Defaults for RunLinuxOptions: everything unset/empty except image.
pub fn run_nanos(
client: Client,
opts opts: RunNanosOptions,
) -> Result(String, error.Error)
Boot a Nanos unikernel, detached. Returns the new machine’s id.
pub fn run_nanos_options(image: String) -> RunNanosOptions
Defaults for RunNanosOptions: everything unset/false except image.
pub fn run_osv(
client: Client,
opts opts: RunOsvOptions,
) -> Result(String, error.Error)
Boot an OSv unikernel, detached. Returns the new machine’s id.
pub fn run_osv_options(image: String) -> RunOsvOptions
Defaults for RunOsvOptions: everything unset/empty/false except
image.
pub fn run_solo5(
client: Client,
opts opts: RunSolo5Options,
) -> Result(String, error.Error)
Boot a Solo5 (MirageOS) unikernel, detached. Returns the new machine’s id.
pub fn run_solo5_options() -> RunSolo5Options
Defaults for RunSolo5Options: everything unset/empty (path defaults
to "." daemon-side when left None).
pub fn run_unikraft(
client: Client,
opts opts: RunUnikraftOptions,
) -> Result(String, error.Error)
Boot a Unikraft unikernel, detached. Returns the new machine’s id.
pub fn run_unikraft_options() -> RunUnikraftOptions
Defaults for RunUnikraftOptions: everything unset/empty (path
defaults to "." daemon-side when left None).
pub fn shell(
client: Client,
id id: String,
command command: option.Option(List(String)),
env env: List(String),
rows rows: Int,
cols cols: Int,
) -> Result(ShellSession, error.Error)
Open an interactive shell (or, with command, run that command with a
live, writable session rather than blocking — the non-blocking sibling
of exec). Output arrives on shell_output(session) as it’s produced;
this function itself returns as soon as the session and its subscription
are set up.
pub fn shell_close(session: ShellSession) -> Nil
Close the session and kill its command. Idempotent.
pub fn shell_id(session: ShellSession) -> String
The daemon-side session id, if you need it for client.request.
pub fn shell_output(
session: ShellSession,
) -> subject.Subject(types.ShellEvent)
This session’s live output Subject — subject.receive it in a loop.
pub fn shell_resize(
session: ShellSession,
rows rows: Int,
cols cols: Int,
) -> Result(Nil, error.Error)
Apply a terminal resize, so full-screen programs in the guest redraw.
pub fn shell_send(
session: ShellSession,
data: BitArray,
) -> Result(Nil, error.Error)
Send keystrokes/input to the session.
pub fn snapshot(
client: Client,
id id: String,
name name: option.Option(String),
description description: String,
) -> Result(types.SnapshotInfo, error.Error)
Capture a machine’s disk state. name of None yields <machine>-<n>.
A BSD guest is powered off first — a mounted UFS cannot be cloned
consistently — so the machine is left stopped; start brings it back.
pub fn snapshots(
client: Client,
machine machine: option.Option(String),
) -> Result(List(types.SnapshotInfo), error.Error)
Snapshots, newest first. machine narrows the list to one machine’s.
pub fn start(
client: Client,
id id: String,
) -> Result(types.CommandResult, error.Error)
Restart a stopped machine in place.
pub fn stop(
client: Client,
id id: String,
) -> Result(types.CommandResult, error.Error)
Stop a machine.
pub fn subscribe(
client: Client,
query query: String,
variables variables: dynamic.Dynamic,
) -> Result(subject.Subject(types.SubscriptionEvent), error.Error)
Subscribe to any subscription bsdkrun/client’s typed API does not
cover. Each next payload’s data arrives as SubNext(Dynamic) on the
returned Subject, terminated by SubError/SubComplete — decode it
the same way request’s result.
pub fn update(
client: Client,
id id: String,
cpus cpus: option.Option(Int),
mem mem: option.Option(Int),
) -> Result(types.CommandResult, error.Error)
Change a machine’s recorded vCPU/RAM. Applies on the next start.