msbd

September 3, 2026 · View on GitHub

A small HTTP server that wraps microsandbox and exposes its microVMs over a clean REST API.

CI Release Container License

What is this?

microsandbox is a local microVM runtime — fast, hardware-isolated sandboxes booted from OCI images via libkrun. It's terrific, but the SDK is in-process and Go-only.

msbd puts a small daemon and a REST API in front of it, so any language can drive microsandbox over plain HTTP. Run msbd once on a host that has /dev/kvm, then curl it (or generate a client from the OpenAPI spec) from wherever.

  • Simple. ~12 endpoints, OpenAPI 3.1 spec, JSON in/out, bearer auth.
  • MicroVMs survive restarts. Sandboxes are created detached; msbd reconnects them by name on boot.
  • Native primitives. Real exec sessions for async jobs, real file IO over the guest filesystem.
  • Interactive terminals. A real kernel-PTY shell over WebSocket — colors, line editing, window resize, and full-screen TUIs (vim, top) all work.
  • Persisted credentials. Rotatable API keys and dashboard accounts (admin/viewer) in a local database, managed from the CLI or the web UI — no restart to add or revoke one.

Quickstart

1. Run the server

docker run --rm \
  --device /dev/kvm \
  -p 8099:8099 \
  -e MSBD_API_KEY=devkey \
  -v msbd-data:/root/.microsandbox \
  ghcr.io/mark3labs/msbd:latest

The first start downloads the microsandbox runtime (~30 MB) into the mounted volume. Subsequent starts skip it. Wait for /readyz to return 200:

curl -fsS localhost:8099/readyz   # → ready

2. Boot a microVM

curl -s -H "Authorization: Bearer devkey" \
     -X POST localhost:8099/api/v1/sandboxes \
     -d '{"image":"alpine:3.19","resources":{"memory_mb":512,"cpu":1}}'
{
  "id": "sbx_1ea598fdaabd2a46",
  "image": "alpine:3.19",
  "state": "running",
  "workdir": "/",
  "uptime_seconds": 0,
  "labels": null
}

3. Run a command in it

ID=sbx_1ea598fdaabd2a46
curl -s -H "Authorization: Bearer devkey" \
     -X POST localhost:8099/api/v1/sandboxes/$ID/exec \
     -d '{"cmd":"uname -a && whoami"}'
{
  "exit_code": 0,
  "stdout": "Linux sbx_1ea598fdaabd2a46 6.12.68 ... x86_64 GNU/Linux\nroot\n",
  "stderr": ""
}

4. Clean up

curl -s -H "Authorization: Bearer devkey" -X DELETE localhost:8099/api/v1/sandboxes/$ID

Browse the full API interactively at http://localhost:8099/docs (Swagger UI), or fetch the raw spec from /openapi.yaml. Both are unauthenticated.

Nix

msbd is packaged as a flake. cgo is enabled at build time, but the only thing the C side links is libdl — the microsandbox Rust FFI library is dlopen'd at runtime, so no Rust toolchain is needed to build msbd.

# Build the binary
nix build github:mark3labs/msbd

# Run it (uses the FHS-wrapped variant — works on NixOS too)
nix run github:mark3labs/msbd

Why the FHS wrapper? msbd itself is a normal Nix-built binary, but the msb supervisor it downloads on first run and the embedded FFI .so it extracts are vanilla glibc binaries that expect a dynamic loader at /lib64/ld-linux-*.so.2 and libcap-ng.so.0 on a standard path. Plain NixOS has neither, so the msbd-fhs package (what nix run and the NixOS module use) provides that FHS layout. On a regular glibc distro (Debian/Ubuntu/Fedora) the plain msbd package is enough.

Flake outputs:

OutputWhat
packages.default / packages.msbdThe bare cgo binary (good on any glibc distro).
packages.msbd-fhsFHS-wrapped binary for NixOS hosts.
devShells.defaultGo + gcc + the runtime libs, CGO_ENABLED=1.
nixosModules.defaultservices.msbd — runs it as a hardened systemd service with /dev/kvm access.

As a NixOS service:

{
  inputs.msbd.url = "github:mark3labs/msbd";

  # in your system config:
  imports = [ msbd.nixosModules.default ];
  services.msbd = {
    enable = true;
    listen = ":8099";
    apiKeyTokenFile = "/run/secrets/msbd-token";  # file holding ONLY the token
    openFirewall = true;
  };
}

The module joins the service to the kvm group, allows /dev/kvm, and keeps the runtime + image cache under /var/lib/msbd.

Two ways to pass the API key, and the distinction matters:

OptionFile formatUse for
apiKeyTokenFilethe bare token, nothing elseagenix / sops-nix secrets (MSBD_API_KEY_FILE)
environmentFilesystemd KEY=value linesseveral secrets at once, e.g. MSBD_API_KEY + MSBD_DASHBOARD_PASS

apiKeyFile is the old name for environmentFile and still works, but warns — despite the name it is an EnvironmentFile, not a token file. Point it at a bare token and the whole KEY=value line becomes your token.

The module also exposes dashboard, dashboardUser, dashboardAllowInsecure, hostPaths, pullTimeoutSecs, jobTtlSecs, jobMaxBytes and shutdownTimeoutSecs; anything else goes through environment. There is deliberately no dashboardPass option, since it would write the password into the world-readable Nix store — set MSBD_DASHBOARD_PASS via environmentFile, or just run msbd users add.

Heads-up on a fresh NixOS deploy. Setting an API key but no dashboard credentials makes GET / return the 403 "Dashboard locked" page (see Web dashboard). Because the service runs as a DynamicUser, the state directory is really /var/lib/private/msbd owned by a per-unit UID, so running msbd users add --data-dir ... as root can leave SQLite sidecar files the service cannot write. Create the account as the service identity instead — the module header in nix/module.nix carries a ready-made systemd-run recipe.

Host requirements

msbd boots real microVMs, so the host machine must have working hardware virtualization:

HostNeeds
Bare-metal Linux/dev/kvm present (almost always)
Linux VMNested virtualization enabled by the parent hypervisor; /dev/kvm exposed
Docker containerRun with --device /dev/kvm (or --privileged). The host kernel still has to expose KVM.
macOS / WindowsUse the upstream microsandbox SDK directly; msbd is Linux-only by design.

Quick host check:

ls -l /dev/kvm                                          # device exists
egrep -c '(vmx|svm)' /proc/cpuinfo                      # CPU virt flag present
cat /sys/module/kvm_{intel,amd}/parameters/nested 2>/dev/null   # Y/1 if VM

Configuration

All via environment variables (also settable as --flag — see msbd serve --help). A .env file in the working directory is loaded on startup via godotenv; shell env and -e still win over .env, and CLI flags win over env. Copy .env.example to .env for a documented starting point.

VarDefaultDescription
MSBD_LISTEN:8099HTTP listen address.
MSBD_API_KEY(empty)Bearer token(s) required on every request; comma-separated to accept several (zero-downtime rotation). Empty = unauthenticated (dev only).
MSBD_API_KEY_FILE(empty)Read the bearer token from a file instead of the env (Docker/K8s secrets). Takes precedence over MSBD_API_KEY.
MSBD_DEFAULT_IMAGEmicrosandbox/pythonOCI image used when create omits image.
MSBD_MAX_SANDBOXES0 (unlimited)Hard cap on concurrent sandboxes; rejects new creates above this with 507 capacity. Admission is serialized (no overshoot).
MSBD_CREATE_TIMEOUT_SECS300Boot deadline (covers cold OCI pulls).
MSBD_PULL_TIMEOUT_SECS900Deadline for a standalone image pull (POST /api/v1/images/pull); larger than create since a cold pull of a big image can outlast a boot.
MSBD_JOB_MAX_BYTES0 (1 MiB)Per-stream cap on an async job's stdout/stderr ring buffer. 0 uses the built-in 1 MiB default; older output is dropped once the cap is hit.
MSBD_JOB_TTL_SECS0 (15 min)How long a finished job's output is retained before the janitor evicts it. 0 uses the built-in 15-minute default.
MSBD_SHUTDOWN_TIMEOUT_SECS60Graceful-drain deadline on SIGTERM/Ctrl-C. A drain overrun warns and exits 0 (no spurious restart failure).
MSBD_HOST_PATHS(empty)Comma-separated allowlist of host path prefixes the host-transfer endpoints (copy-from-host, copy-to-host, snapshot export/import) may touch. Empty = all host transfers denied (403). Symlinks are resolved to block escapes.
MSBD_LOG_LEVELinfoLog verbosity: debug, info, warn, error. Invalid values fail fast. Output is colorized on a TTY, plain otherwise.
MSBD_DATA_DIR~/.microsandbox/msbdDirectory holding the SQLite database of dashboard users, API keys and sessions. Created 0700, database 0600. See Users & API keys.
MSBD_SESSION_TTL_SECS0 (12 h)Dashboard login lifetime.
MSBD_DASHBOARDtrueServe the web dashboard at /. Set false to disable.
MSBD_DASHBOARD_USER(empty)Legacy single-account HTTP Basic auth username. Setting user or pass turns it on. Superseded once a stored account exists.
MSBD_DASHBOARD_PASS(empty)Legacy Basic auth password. Both empty and no stored users = dashboard is unauthenticated. When an API key IS set but the dashboard has no auth, the dashboard is refused (it would bypass the API token) unless MSBD_DASHBOARD_ALLOW_INSECURE=true.
MSBD_DASHBOARD_ALLOW_INSECUREfalseOverride the safety refusal above and serve the dashboard without auth even when an API key is set (unsafe).

Request bodies are size-capped (1 MiB control-plane, 64 MiB file writes) and reject unknown JSON fields (a typo'd field is a 400, not a silent no-op). The server sets IdleTimeout and echoes an X-Request-Id on every response.

Flags mirror every var (--dashboard, --host-paths, --shutdown-timeout, --api-key-file, …); flag › env › default.

Users & API keys

msbd keeps a small SQLite database (pure-Go driver — no extra cgo) of API keys, dashboard users and login sessions. It lives at $MSBD_DATA_DIR (default ~/.microsandbox/msbd/msbd.db), which is inside the directory every deployment path already persists: the Docker VOLUME, the compose named volume, the NixOS StateDirectory.

Nothing about this is mandatory. MSBD_API_KEY and MSBD_DASHBOARD_USER/_PASS keep working exactly as before; stored credentials are accepted in addition to them.

API keys

msbd keys create ci-runner            # prints the token ONCE
msbd keys create temp --expires 30d
msbd keys list
msbd keys revoke ci-runner            # by name, token prefix, or numeric id
msbd keys rm 3

Only sha256(token) is stored, so the token cannot be recovered — losing it means minting a new one. Keys are accepted alongside MSBD_API_KEY, and creating the first one flips an otherwise-open server to authenticated without a restart (the daemon and the CLI share the same database file).

Revoking is fail-safe: a revoked or expired key stops working within a few seconds, but the server stays authenticated — revoking your last key locks the API down rather than throwing it open. msbd keys rm (deleting every row) is the explicit way back to an unauthenticated dev server.

Dashboard users

msbd users add alice                  # prompts for a password, twice
msbd users add ci --role viewer
echo "$PW" | msbd users add bot --password-stdin
msbd users list
msbd users passwd alice               # also signs alice out everywhere
msbd users role alice viewer
msbd users rm alice

Creating the first user upgrades the dashboard from HTTP Basic (or no auth) to a login page with server-side sessions — again with no restart. Passwords are bcrypt-hashed; there is deliberately no --password flag, since it would leak the secret into the process list and shell history.

Two roles: admin (everything, including managing users and keys) and viewer (read-only — every mutating endpoint is refused server-side, not merely hidden). Removing or demoting the last admin is refused so the dashboard can never become unreachable.

Maintenance

msbd db path        # where the database is
msbd db migrate     # create/migrate explicitly (serve does it automatically)
msbd db sweep       # drop expired sessions

Back it up by copying that one file. Every command takes --data-dir (or MSBD_DATA_DIR) — it must match what the daemon uses.

Web dashboard

A self-contained web UI lives at the root (/) and manages everything the REST API does — sandboxes (create, start/stop/delete, inspect, run commands, live logs & metrics, a file browser, and a real kernel-PTY terminal), volumes, images and snapshots. The REST API is namespaced under /api/v1, so the two never collide.

# Recommended: a real account with a login page and sessions.
msbd users add admin
msbd serve
# → open http://localhost:8099/

# Legacy single-account HTTP Basic auth (still supported):
MSBD_DASHBOARD_USER=admin MSBD_DASHBOARD_PASS=s3cret msbd serve

Every section is a real, bookmarkable URL/ (overview), /sandboxes, /sandboxes/{id}, /volumes, /images, /snapshots, /settings/keys, /settings/users — so refresh, browser back/forward and shared links all work. Datastar SSE is used for in-page updates only, over a separate /ui/* endpoint namespace.

PageWhat you get
OverviewFleet counts by state, capacity headroom against --max-sandboxes, aggregate CPU/memory across running sandboxes, cache sizes, recent sandboxes.
SandboxesSortable + searchable table with state filters, live auto-refresh (pausable, and gated on tab visibility), per-row start/stop/terminal/snapshot/delete.
Sandbox detailLive header (state + uptime stream in), full lifecycle actions, and tabs for Overview (metadata + charted live metrics), Run, Logs, Files and an embedded Terminal.
RunCommands execute as async jobs: output streams in as it is produced, long commands are cancellable, and the last 25 commands are offered as autocomplete.
LogsTimestamped and source-coloured, with source/tail filters, search, wrap and follow toggles, jump-to-bottom, and a plain-text download.
FilesA real browser: breadcrumb navigation, view/edit/save, upload, download, new folder, delete, and a hidden-files toggle. Binary files get a read-only hex preview.
Settings → API keys / UsersCreate, revoke and delete REST API keys (the token is revealed exactly once), and manage dashboard accounts, passwords and roles. Admin-only.
Volumes / Images / SnapshotsSearchable, sortable tables showing creation and last-used times. Images can be inspected (OCI config + layers), are flagged when a live sandbox uses them, and can seed a new sandbox in one click. Prune reports exactly what it reclaimed.

Other niceties: a light/dark/system theme toggle (persisted, no flash of the wrong palette), a responsive layout with a mobile nav drawer and horizontally scrollable tables, styled confirmation dialogs (never window.confirm), busy states on every mutating control so a double-click can't boot two sandboxes, sticky error toasts plus inline errors next to the control that failed, and keyboard/screen-reader support (skip link, aria-labels on icon-only controls, table captions, aria-sort on sorted columns).

It is server-rendered with templ + templui components, styled with Tailwind, and made reactive with Datastar (SSE-driven DOM patching). Everything — the compiled CSS, the Datastar runtime, xterm.js and the component JavaScript — is embedded in the binary (//go:embed); there are no external assets to deploy. Auth is independent of MSBD_API_KEY: the API stays bearer-gated while the dashboard has its own. It picks the strongest option available — a login page with HttpOnly, SameSite=Lax session cookies once you have created an account (msbd users add), the legacy single-account HTTP Basic if only MSBD_DASHBOARD_USER/_PASS are set, and open otherwise. The terminal page never embeds the API key — it uses a short-lived, single-use ticket.

If the API requires a key but the dashboard would have no auth at all, msbd locks the dashboard — every route serves a short page telling you to run msbd users add, which takes effect on the next reload with no restart. Override with MSBD_DASHBOARD_ALLOW_INSECURE=true.

Note what this means now that the dashboard lives at the root: a deployment that sets MSBD_API_KEY but no dashboard credentials (the Docker quickstart above) answers GET / with a 403 "Dashboard locked" page rather than the 404 it used to. That is the safety refusal working as intended, not a broken deploy — msbd users add <name> clears it on the next reload. /healthz and /readyz are unaffected, so container and Kubernetes probes keep passing; only point uptime checks at those, never at /.

REST API

Breaking change since v0.7.1. Every versioned endpoint is now under /api/v1 (was /v1), and the web dashboard moved to the root (was /dashboard). There are no redirects or aliases — the old paths return 404, so update clients, bookmarks and any path-based reverse-proxy rules. The unversioned ops endpoints (/healthz, /readyz, /metrics, /docs, /openapi.yaml) did not move. In most clients this is a one-line base-URL change:

- http://msbd.internal:8099/v1
+ http://msbd.internal:8099/api/v1

If you proxy msbd by path, note that the dashboard now owns the root and serves its own SSE endpoints under /ui/*. Those are internal to the web UI and cookie-authenticated — they are not a public API, and nothing outside the browser should call them. Reserve /api for the REST API when adding routes in front of msbd.

Method & pathPurpose
GET /healthz · GET /readyzLiveness · readiness (runtime loaded + /dev/kvm accessible).
GET /docs · GET /openapi.yamlSwagger UI · raw OpenAPI spec (unauthenticated).
GET /Web management UI with its own auth — see Web dashboard.
GET /api/v1/versionDefault image + runtime/SDK versions (diagnostics).
GET /metricsPrometheus text-exposition operational metrics (sandbox counts, jobs, terminals, request classes).
POST /api/v1/terminal-ticketsMint a short-lived single-use terminal ticket (browser WS auth without exposing the API key).
POST /api/v1/sandboxes · GET /api/v1/sandboxes · GET/DELETE /api/v1/sandboxes/{id}Lifecycle. Create accepts user, hostname, network_policy, ports, secrets, mounts.
GET /api/v1/sandboxes/{id}/inspectSandbox metadata + raw SDK config blob.
POST /api/v1/sandboxes/{id}/stop · .../startPause / ensure-running.
POST /api/v1/sandboxes/{id}/exec · .../runSynchronous exec — exec is short, run is long-safe and ensures-running.
GET /api/v1/sandboxes/{id}/terminalInteractive kernel-PTY terminal over WebSocket (binary stdin/stdout; text control frames for resize/signal). Colors, line editing, resize, vim/top all work. Auth via header, ?key=, or a single-use ?ticket= (see POST /api/v1/terminal-tickets).
POST /api/v1/sandboxes/{id}/jobs · GET /api/v1/sandboxes/{id}/jobs/{job}Async (background) jobs. Output is a bounded ring buffer (1 MiB/stream); poll reports truncated + stdout_bytes/stderr_bytes, and finished jobs are evicted after a TTL.
POST /api/v1/sandboxes/{id}/jobs/{job}/stdin · .../signalWrite to a job's stdin (launch with stdin:true) · send a signal (≤0 = kill).
POST /api/v1/sandboxes/{id}/files/read · .../files/writeNative file IO, base64-encoded.
POST /api/v1/sandboxes/{id}/files/{list,stat,exists,mkdir,remove,copy,rename}Extended filesystem operations.
POST /api/v1/sandboxes/{id}/files/{copy-from-host,copy-to-host}Copy between an allowlisted host path (MSBD_HOST_PATHS) and the sandbox. Denied (403) when the allowlist is empty or the path escapes it.
GET /api/v1/metrics · GET /api/v1/sandboxes/{id}/metricsPoint-in-time per-sandbox resource metrics (all / one). For scrapeable ops telemetry use GET /metrics.
GET /api/v1/sandboxes/{id}/logsRead persisted stdout/stderr/system logs (?tail=, ?sources=).
POST/GET /api/v1/volumes · GET/DELETE /api/v1/volumes/{name}Named persistent volumes.
POST /api/v1/volumes/{name}/files/{read,write,mkdir,remove,exists}Volume file IO.
GET /api/v1/images · GET /api/v1/images/inspect · POST /api/v1/images/pull · DELETE /api/v1/images · POST /api/v1/images/pruneCached OCI image inventory. pull fetches an image into the cache (long-running; boots a throwaway microVM).
POST/GET /api/v1/snapshots · GET/DELETE /api/v1/snapshots/{name} · .../verifySandbox rootfs snapshots.
POST /api/v1/snapshots/{export,import,reindex}Export/import snapshot archives · rebuild the index.

Full schemas: see openapi.yaml.

Lifecycle semantics

  • Detached by default. Every sandbox is created detached, so the microVM keeps running when msbd restarts.
  • Reconnect at boot. On startup msbd lists all known sandboxes and re-attaches by name. A sandbox that existed before the restart is still callable through the same id.
  • Transparent resume. run, launch, and files/* all ensure-running first — a paused box silently resumes on the next call. exec (one round-trip helpers) deliberately does not, so it stays cheap.
  • Jobs and terminals are in-memory. A job that was running when msbd restarts polls as gone, and an open terminal's WebSocket simply closes (the VM survives; the streaming attach does not). Re-launch / reconnect from the client side.
  • Names are ids. Sandbox names (≤128 bytes UTF-8) ARE the provider id. msbd generates them as sbx_<16hex>; you can also pass your own.

What it is, what it isn't

✅ A simple way to expose microsandbox over HTTP so any language can drive it. ✅ A single-host device server with enough auth to be safe on its own: API keys, dashboard accounts and an admin/viewer split. Auth your real end users upstream.

❌ Not a multi-host scheduler. Capacity = the one host. ❌ Not a multi-tenant platform with quotas, billing or fine-grained RBAC. (Bring your own.) ❌ Not a re-implementation of microsandbox's own cloud backend.

Development

# Build (or `task build`)
go build -o ./bin/msbd ./cmd/msbd

# Run (these are equivalent — the bare binary defaults to `serve`)
MSBD_API_KEY=devkey ./bin/msbd
./bin/msbd serve --api-key devkey --listen :8099

# Explore the CLI (styled help, version, shell completions)
./bin/msbd --help
./bin/msbd serve --help
./bin/msbd users --help
./bin/msbd keys --help
./bin/msbd --version

# Lint, format, test (or `task lint` / `task fmt` / `task test`)
golangci-lint run ./...
gofmt -w .
go test ./...

The CLI is built on cobra and styled with charmbracelet/fang. Every MSBD_* env var has a matching serve flag (flag overrides env overrides default), and Ctrl-C / SIGTERM trigger a graceful drain of in-flight requests.

Repo layout

cmd/msbd/main.go              # entrypoint — EnsureInstalled, reconcile, serve
cmd/msbd/admin.go             # `msbd users` / `msbd keys` / `msbd db` subcommands
assets.go                     # //go:embed openapi.yaml (served at /docs)
internal/api/router.go        # HTTP router + middleware (auth, recover, log)
internal/api/handlers.go      # core lifecycle/exec/jobs/files handlers
internal/api/handlers_ext.go  # inspect, metrics, logs, fs, volumes, images, snapshots
internal/api/terminal.go      # interactive terminal WebSocket handler
internal/api/docs.go          # Swagger UI (/docs) + raw spec (/openapi.yaml)
internal/api/dto.go           # wire shapes
internal/core/service.go      # SDK-facing business logic (lifecycle/exec/jobs/files)
internal/core/terminal.go     # interactive PTY terminal: Session interface + OpenTerminal
internal/core/terminal_agent.go # kernel-PTY backend over the raw agent protocol (CBOR)
internal/core/fs.go           # extended filesystem ops + host transfer
internal/core/metrics.go      # point-in-time resource metrics
internal/core/logs.go         # persisted log reads
internal/core/volume.go       # named persistent volumes + volume file IO
internal/core/image.go        # cached OCI image inventory
internal/core/snapshot.go     # sandbox rootfs snapshots
internal/core/registry.go     # live handle cache + workdir cache + reconcile
internal/core/jobs.go         # async job registry (+ stdin/signal/cancel)
internal/core/version.go      # SDK / runtime version helpers
internal/store/store.go       # SQLite state: open + embedded migrations (the only SQL)
internal/store/users.go       # dashboard accounts (bcrypt) + roles
internal/store/apikeys.go     # REST bearer tokens (sha256-hashed, shown once)
internal/store/sessions.go    # dashboard login sessions
internal/store/cache.go       # TTL cache in front of token verification
internal/dashboard/dashboard.go    # web UI: page + SSE routes and their guards (Mount on the api mux)
internal/dashboard/auth.go         # open / basic / session auth modes, guards, cookies
internal/dashboard/handlers.go     # page handlers (one real URL per section) + shared SSE helpers
internal/dashboard/handlers_*.go   # Datastar SSE handlers (overview, sandboxes, files, volumes, images, snapshots, settings)
internal/dashboard/views/*.templ   # templ pages/fragments (templui components + Datastar attrs)
internal/dashboard/components/      # vendored templui components (via `templui add`)
internal/dashboard/assets/          # input.css + committed output.css, datastar/xterm, component JS (embedded)
openapi.yaml                  # the contract
VERSION                       # release version (single source of truth)
Taskfile.yml                  # dev + release tasks (go-task)
flake.nix                     # Nix package + dev shell + NixOS module
Dockerfile                    # build from source
Dockerfile.release            # used by goreleaser
docker-compose.yml            # example compose deploy

Releasing

The git tag is the source of truth for the version. Use the release task so the VERSION file and the tag are bumped atomically (you type the version once):

task release NEW_VERSION=1.2.3      # bump VERSION, commit, tag locally
git push origin HEAD v1.2.3         # push to trigger the release workflow

# or in one shot:
task release:push NEW_VERSION=1.2.3

The task refuses to run on a dirty tree, validates semver, won't clobber an existing tag, and seds openapi.yaml's info.version in the same commit so the published spec always carries the release number. The release workflow then verifies the tag equals both VERSION and the spec's version, failing on any mismatch (i.e. a hand-made tag after the spec drifted).

GoReleaser injects the version from the tag (-X main.version); the Nix flake reads the same number from VERSION (flakes can't see git tags), so nix build off a tagged checkout reports an identical version. commit/date are filled from the tag's revision in both paths.

License

Apache-2.0 — see LICENSE and NOTICE.

msbd wraps the microsandbox Go SDK (also Apache-2.0). The microVM runtime it drives — msb + libkrunfw (LGPL) — is not bundled with msbd; the SDK downloads it to ~/.microsandbox/ on first run. See NOTICE for details.