Bsdkrun.Client (bsdkrun_ex v0.6.0)

Copy Markdown View Source

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

The coding agents, and whether each one's sandbox image is built.

Remove an agent's sandboxes, and unless keep_home its saved login too.

Agent sandboxes, newest first.

The argv that starts the agent's TUI — pass it to shell/3.

Start (or reuse) a sandbox; returns its machine id.

Stop an agent's sandboxes. Its saved login survives.

Boot a NEW machine from a snapshot — or from a machine, which is snapshotted first — and return the new machine's id.

Snapshot a machine into a named flavor, like docker commit.

Act on containers: start / stop / restart / kill / pause / unpause / rm.

Containers in the engine. all: false lists only running ones.

One container's logs (stdout+stderr, most recent tail lines).

Start (or resume) the engine, returning its status once it answers.

Is the Docker engine up, and where is its socket?

Stop the engine. Images and containers stay on its disk.

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.

Delete snapshots and their data. Machines branched from them are unaffected.

Run a raw query or mutation. Returns {:ok, data} (the response's data field) or {:error, %Bsdkrun.Error{}}.

Put a machine's disk state back to one of its snapshots.

Restore a machine to its most recent snapshot.

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.

Boot a Nanos unikernel. opts maps to RunNanosInput. No agent — no exec/4/commit/4.

Boot an OSv unikernel. opts maps to RunOsvInput. No agent — no exec/4/commit/4.

Boot a Solo5 (MirageOS) unikernel. opts maps to RunSolo5Input.

Boot a Unikraft unikernel. opts maps to RunUnikraftInput. No disk, no agent — no exec/4/commit/4.

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.

Capture a machine's disk state. :name defaults to <machine>-<n>.

List snapshots, newest first. Pass a machine id/name to narrow to its own.

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

t()

@type t() :: %Bsdkrun.Client{token: String.t(), url: String.t()}

Functions

ai_agents(client)

@spec ai_agents(t()) ::
  {:ok, [Bsdkrun.Types.AiAgent.t()]} | {:error, Bsdkrun.Error.t()}

The coding agents, and whether each one's sandbox image is built.

ai_remove(client, agent, keep_home \\ false)

@spec ai_remove(t(), String.t(), boolean()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Remove an agent's sandboxes, and unless keep_home its saved login too.

ai_sessions(client)

@spec ai_sessions(t()) ::
  {:ok, [Bsdkrun.Types.AiSession.t()]} | {:error, Bsdkrun.Error.t()}

Agent sandboxes, newest first.

ai_shell_command(client, agent, machine_id)

@spec ai_shell_command(t(), String.t(), String.t()) ::
  {:ok, [String.t()]} | {:error, Bsdkrun.Error.t()}

The argv that starts the agent's TUI — pass it to shell/3.

ai_start(client, agent, opts \\ [])

@spec ai_start(t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Start (or reuse) a sandbox; returns its machine id.

Options: :cpus, :mem, :workspace (a path on the engine's host), and :new to boot a second sandbox against the same saved login.

ai_stop(client, agent)

@spec ai_stop(t(), String.t()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Stop an agent's sandboxes. Its saved login survives.

branch(client, snapshot, opts \\ [])

@spec branch(t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

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. With no :ports, the snapshot's own forwards are inherited, with any host port that is already taken swapped for a free one.

commit(client, id, name, description \\ "")

@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.

docker_container(client, action, ids)

@spec docker_container(t(), String.t(), String.t() | [String.t()]) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Act on containers: start / stop / restart / kill / pause / unpause / rm.

docker_containers(client, all \\ true)

@spec docker_containers(t(), boolean()) ::
  {:ok, [Bsdkrun.Types.DockerContainer.t()]} | {:error, Bsdkrun.Error.t()}

Containers in the engine. all: false lists only running ones.

docker_logs(client, id, tail \\ 200)

@spec docker_logs(t(), String.t(), pos_integer()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

One container's logs (stdout+stderr, most recent tail lines).

docker_start(client, opts \\ [])

@spec docker_start(
  t(),
  keyword()
) :: {:ok, Bsdkrun.Types.DockerStatus.t()} | {:error, Bsdkrun.Error.t()}

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. Options: :cpus, :mem, :mounts, :no_home, :publish_bind, :disk_size.

docker_status(client)

@spec docker_status(t()) ::
  {:ok, Bsdkrun.Types.DockerStatus.t()} | {:error, Bsdkrun.Error.t()}

Is the Docker engine up, and where is its socket?

docker_stop(client)

@spec docker_stop(t()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Stop the engine. Images and containers stay on its disk.

exec(client, machine_id, command, opts \\ [])

@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.

follow_logs(client, id, opts \\ [])

@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.

from_env()

@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).

from_env!()

@spec from_env!() :: t()

Like from_env/0, but returns the client or raises Bsdkrun.Error.

get(client, id)

@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.

get!(client, id)

@spec get!(t(), String.t()) :: Bsdkrun.Types.SandboxInfo.t() | nil

Like get/2, but returns the machine (or nil) or raises Bsdkrun.Error.

list(client, all \\ false)

@spec list(t(), boolean()) ::
  {:ok, [Bsdkrun.Types.SandboxInfo.t()]} | {:error, Bsdkrun.Error.t()}

List machines. all: true includes exited ones (default: running only).

list!(client, all \\ false)

@spec list!(t(), boolean()) :: [Bsdkrun.Types.SandboxInfo.t()]

Like list/2, but returns the list or raises Bsdkrun.Error.

logs(client, id, boot \\ false)

@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.

new(opts)

@spec new(keyword()) :: t()

Build a client. Does not connect — connections are made lazily, on first use.

normalize_url(input)

@spec normalize_url(String.t()) :: String.t()

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(client, ids, force \\ false)

@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.

remove_snapshots(client, names)

@spec remove_snapshots(t(), String.t() | [String.t()]) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Delete snapshots and their data. Machines branched from them are unaffected.

request(client, query, variables \\ %{})

@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{}}.

restore(client, id, snapshot, opts \\ [])

@spec restore(t(), String.t(), String.t(), keyword()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Put a machine's disk state back to one of its snapshots.

:force (default true) stops the machine first — it holds the very files being replaced. :backup (default true) snapshots the state being overwritten, which is a CoW clone and therefore free. The machine is left stopped.

rollback(client, id, opts \\ [])

@spec rollback(t(), String.t(), keyword()) ::
  {:ok, Bsdkrun.Types.CommandResult.t()} | {:error, Bsdkrun.Error.t()}

Restore a machine to its most recent snapshot.

run_bsd(client, opts)

@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.

run_flavor(client, opts)

@spec run_flavor(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot a named flavor. opts maps to RunFlavorInput.

run_linux(client, opts)

@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.

run_nanos(client, opts)

@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.

run_osv(client, opts)

@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.

run_solo5(client, opts \\ [])

@spec run_solo5(t(), keyword() | map()) ::
  {:ok, String.t()} | {:error, Bsdkrun.Error.t()}

Boot a Solo5 (MirageOS) unikernel. opts maps to RunSolo5Input.

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 host-side facts are taken: :path (a .hvt binary or a project dir whose dist/ holds one, default "."), :block backing files ("NAME=FILE"), and :args handed to the unikernel itself. Always a single vCPU — :cpus above 1 is warned about and ignored. No disk, no agent — no exec/4/commit/4.

run_unikraft(client, opts)

@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.

shell(client, machine_id, opts \\ [])

@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.

snapshot(client, id, opts \\ [])

@spec snapshot(t(), String.t(), keyword()) ::
  {:ok, Bsdkrun.Types.SnapshotInfo.t()} | {:error, Bsdkrun.Error.t()}

Capture a machine's disk state. :name defaults to <machine>-<n>.

A BSD guest is powered off first — a mounted UFS cannot be cloned consistently — so the machine is left stopped; start/2 brings it back.

snapshots(client, machine \\ nil)

@spec snapshots(t(), String.t() | nil) ::
  {:ok, [Bsdkrun.Types.SnapshotInfo.t()]} | {:error, Bsdkrun.Error.t()}

List snapshots, newest first. Pass a machine id/name to narrow to its own.

start(client, id)

@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.

stop(client, id)

@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).

subscribe(client, query, variables \\ %{}, opts \\ [])

@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.

update(client, id, opts \\ [])

@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.