A remote client for a bsdkrund daemon's GraphQL API — talks straight to
POST <url> (queries/mutations) and a graphql-transport-ws socket
(subscriptions), instead of shelling out to a local bsdkrun binary like
Bsdkrun.Sandbox does. Same contract the web frontend speaks
(web/src/lib/graphql.ts, web/src/lib/api.ts) and the daemon documents in
daemon/README.md.
{:ok, client} = Bsdkrun.Client.from_env()
{:ok, machines} = Bsdkrun.Client.list(client, true)
{:ok, %{exit_code: 0, output: out}} = Bsdkrun.Client.exec(client, "abc123", ["uname", "-a"])Bsdkrun.Client.new/1 builds a client lazily — no connection is made until
the first call. Queries and mutations go straight over HTTP
(Bsdkrun.GraphQL, built on :httpc); anything that streams (exec/4,
shell/3, follow_logs/3, subscribe/4) shares one
graphql-transport-ws socket per {url, token} pair — a
Bsdkrun.GraphQLSocket GenServer, started lazily under
Bsdkrun.Client.SocketSupervisor on first use and found again via
Bsdkrun.Client.Registry (see ensure_conn/1). A %Client{} itself stays
a plain, immutable struct — callers never see the GenServer.
Live output: messages, not callbacks, by default
follow_logs/3 and shell/3 deliver output by sending messages to the
calling process's mailbox — {:bsdkrun_logs, subscription_id, event} and
{:bsdkrun_shell, session_id, event}} respectively, where event is
{:data, binary}, {:exit, exit_code}, {:error, %Bsdkrun.Error{}} or
:complete. That is the idiomatic default for this codebase — every other
blocking call in this SDK (Bsdkrun.Sandbox.exec/3 included) already
returns to an Elixir process, so a mailbox is the natural sink, no
process-owned closure required. Pass on_data: fn id, event -> ... end in
opts to receive a callback instead of messages (and owner: pid to
target a different process's mailbox than the caller's). The escape hatch
subscribe/4 follows the same convention with {:bsdkrun_subscription, id, event}, event being the raw {:next, data} | {:error, _} | :complete.
exec/4 is built on the same shellOutput subscription internally, but
blocks the calling process until the command exits (a receive loop, not
a GenServer.call — see await_exec/3), matching this SDK's otherwise
synchronous feel (Bsdkrun.Sandbox.exec/3 blocks too).
Every fallible function returns {:ok, value} | {:error, %Bsdkrun.Error{}};
list/2 and get/2 (mirroring Bsdkrun.Sandbox) and from_env/0 also
have bang counterparts that unwrap or raise.
Summary
Functions
Snapshot a machine into a named flavor, like docker commit.
Run a command to completion and collect its output. Blocks the calling
process until the command exits (opts[:timeout], default 300_000ms).
command is an argv list or a bare program name string. Opts: :env
(a map or "K=V" list), :rows/:cols (pty size, default 24x80),
:timeout.
Follow the machine's console log live, over the machineLogs subscription.
Opts: :boot (bsdkrun's own boot log, default false), :on_data
(fn subscription_id, event -> end) and :owner — see the module doc.
With no :on_data, events arrive as {:bsdkrun_logs, subscription_id, event} messages, event being {:data, binary} | {:exit, exit_code} | {:error, %Bsdkrun.Error{}} | :complete.
Build a client from BSDKRUN_URL / BSDKRUN_TOKEN. BSDKRUN_URL unset is
an error; BSDKRUN_URL set without BSDKRUN_TOKEN is also an error — this
never silently proceeds unauthenticated (mirrors daemon/src/client.rs's
RemoteConfig::from_env for the gRPC client; different env vars, same
philosophy, since a GraphQL endpoint is a different port and URL shape).
Like from_env/0, but returns the client or raises Bsdkrun.Error.
Fetch a single machine by id or name (a unique prefix is enough), or nil if there is no such machine.
Like get/2, but returns the machine (or nil) or raises Bsdkrun.Error.
List machines. all: true includes exited ones (default: running only).
Like list/2, but returns the list or raises Bsdkrun.Error.
Read the machine's console log as a single string, one-shot. Pass boot: true for bsdkrun's own boot log.
Build a client. Does not connect — connections are made lazily, on first use.
Normalize what a person actually pastes into the daemon's GraphQL endpoint
URL: trim, default to http:// when no scheme is given, strip trailing
slashes, and append /graphql unless it is already there. Mirrors
web/src/lib/connection.ts's normalizeUrl.
Remove one or more machines and their state. force: true stops them first if running.
Run a raw query or mutation. Returns {:ok, data} (the response's data field) or {:error, %Bsdkrun.Error{}}.
Boot a FreeBSD/NetBSD machine. opts maps to RunBsdInput, :os being :freebsd or :netbsd.
Boot a named flavor. opts maps to RunFlavorInput.
Boot a Linux (OCI) machine. opts maps to RunLinuxInput; see the module doc for the input shape.
Open a live interactive session on a machine (or run opts[:command] if
given, instead of a login shell) and return a Shell.t() handle. Output is
delivered as it arrives, exactly as follow_logs/3 does: opts[:on_data]
(fn session_id, event -> end) or, with no callback, {:bsdkrun_shell, session_id, event} messages to opts[:owner] (default: the caller).
event is {:data, binary} | {:exit, exit_code} | {:error, _} | :complete.
Restart a stopped machine in place — same id, disk/rootfs, resources.
Stop the machine (BSD guests clean-poweroff; Linux is SIGTERM'd).
Start a raw subscription. opts[:on_data] (arity 2, fn id, event -> end)
receives {:next, data} | {:error, %Bsdkrun.Error{}} | :complete; with no
callback, the same arrives as {:bsdkrun_subscription, id, event} messages
to opts[:owner] (default: the calling process). Returns a
Subscription.t() — cancel it with Subscription.cancel/1.
Change a machine's recorded vCPU / RAM (:cpus, :mem). Applies on next start/2.
Types
Functions
@spec commit(t(), String.t(), String.t(), String.t()) :: {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}
Snapshot a machine into a named flavor, like docker commit.
@spec exec(t(), String.t(), String.t() | [String.t()], keyword()) :: {:ok, %{exit_code: integer() | nil, output: binary()}} | {:error, Bsdkrun.Error.t()}
Run a command to completion and collect its output. Blocks the calling
process until the command exits (opts[:timeout], default 300_000ms).
command is an argv list or a bare program name string. Opts: :env
(a map or "K=V" list), :rows/:cols (pty size, default 24x80),
:timeout.
@spec follow_logs(t(), String.t(), keyword()) :: {:ok, Bsdkrun.Client.Subscription.t()} | {:error, Bsdkrun.Error.t()}
Follow the machine's console log live, over the machineLogs subscription.
Opts: :boot (bsdkrun's own boot log, default false), :on_data
(fn subscription_id, event -> end) and :owner — see the module doc.
With no :on_data, events arrive as {:bsdkrun_logs, subscription_id, event} messages, event being {:data, binary} | {:exit, exit_code} | {:error, %Bsdkrun.Error{}} | :complete.
@spec from_env() :: {:ok, t()} | {:error, Bsdkrun.Error.t()}
Build a client from BSDKRUN_URL / BSDKRUN_TOKEN. BSDKRUN_URL unset is
an error; BSDKRUN_URL set without BSDKRUN_TOKEN is also an error — this
never silently proceeds unauthenticated (mirrors daemon/src/client.rs's
RemoteConfig::from_env for the gRPC client; different env vars, same
philosophy, since a GraphQL endpoint is a different port and URL shape).
@spec from_env!() :: t()
Like from_env/0, but returns the client or raises Bsdkrun.Error.
@spec get(t(), String.t()) :: {:ok, Bsdkrun.Types.SandboxInfo.t() | nil} | {:error, Bsdkrun.Error.t()}
Fetch a single machine by id or name (a unique prefix is enough), or nil if there is no such machine.
@spec get!(t(), String.t()) :: Bsdkrun.Types.SandboxInfo.t() | nil
Like get/2, but returns the machine (or nil) or raises Bsdkrun.Error.
@spec list(t(), boolean()) :: {:ok, [Bsdkrun.Types.SandboxInfo.t()]} | {:error, Bsdkrun.Error.t()}
List machines. all: true includes exited ones (default: running only).
@spec list!(t(), boolean()) :: [Bsdkrun.Types.SandboxInfo.t()]
Like list/2, but returns the list or raises Bsdkrun.Error.
@spec logs(t(), String.t(), boolean()) :: {:ok, String.t()} | {:error, Bsdkrun.Error.t()}
Read the machine's console log as a single string, one-shot. Pass boot: true for bsdkrun's own boot log.
Build a client. Does not connect — connections are made lazily, on first use.
Normalize what a person actually pastes into the daemon's GraphQL endpoint
URL: trim, default to http:// when no scheme is given, strip trailing
slashes, and append /graphql unless it is already there. Mirrors
web/src/lib/connection.ts's normalizeUrl.
@spec remove(t(), String.t() | [String.t()], boolean()) :: {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}
Remove one or more machines and their state. force: true stops them first if running.
@spec request(t(), String.t(), map()) :: {:ok, term()} | {:error, Bsdkrun.Error.t()}
Run a raw query or mutation. Returns {:ok, data} (the response's data field) or {:error, %Bsdkrun.Error{}}.
@spec run_bsd(t(), keyword() | map()) :: {:ok, String.t()} | {:error, Bsdkrun.Error.t()}
Boot a FreeBSD/NetBSD machine. opts maps to RunBsdInput, :os being :freebsd or :netbsd.
@spec run_flavor(t(), keyword() | map()) :: {:ok, String.t()} | {:error, Bsdkrun.Error.t()}
Boot a named flavor. opts maps to RunFlavorInput.
@spec run_linux(t(), keyword() | map()) :: {:ok, String.t()} | {:error, Bsdkrun.Error.t()}
Boot a Linux (OCI) machine. opts maps to RunLinuxInput; see the module doc for the input shape.
@spec run_nanos(t(), keyword() | map()) :: {:ok, String.t()} | {:error, Bsdkrun.Error.t()}
Boot a Nanos unikernel. opts maps to RunNanosInput. No agent — no exec/4/commit/4.
@spec run_osv(t(), keyword() | map()) :: {:ok, String.t()} | {:error, Bsdkrun.Error.t()}
Boot an OSv unikernel. opts maps to RunOsvInput. No agent — no exec/4/commit/4.
@spec run_unikraft(t(), keyword() | map()) :: {:ok, String.t()} | {:error, Bsdkrun.Error.t()}
Boot a Unikraft unikernel. opts maps to RunUnikraftInput. No disk, no agent — no exec/4/commit/4.
@spec shell(t(), String.t(), keyword()) :: {:ok, Bsdkrun.Client.Shell.t()} | {:error, Bsdkrun.Error.t()}
Open a live interactive session on a machine (or run opts[:command] if
given, instead of a login shell) and return a Shell.t() handle. Output is
delivered as it arrives, exactly as follow_logs/3 does: opts[:on_data]
(fn session_id, event -> end) or, with no callback, {:bsdkrun_shell, session_id, event} messages to opts[:owner] (default: the caller).
event is {:data, binary} | {:exit, exit_code} | {:error, _} | :complete.
@spec start(t(), String.t()) :: {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}
Restart a stopped machine in place — same id, disk/rootfs, resources.
@spec stop(t(), String.t()) :: {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}
Stop the machine (BSD guests clean-poweroff; Linux is SIGTERM'd).
@spec subscribe(t(), String.t(), map(), keyword()) :: {:ok, Bsdkrun.Client.Subscription.t()} | {:error, Bsdkrun.Error.t()}
Start a raw subscription. opts[:on_data] (arity 2, fn id, event -> end)
receives {:next, data} | {:error, %Bsdkrun.Error{}} | :complete; with no
callback, the same arrives as {:bsdkrun_subscription, id, event} messages
to opts[:owner] (default: the calling process). Returns a
Subscription.t() — cancel it with Subscription.cancel/1.
@spec update(t(), String.t(), keyword()) :: {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}
Change a machine's recorded vCPU / RAM (:cpus, :mem). Applies on next start/2.