Changelog
All notable changes to Swarm are documented in this file.
[0.8.0-beta.12] — 2026-08-05
Section titled “[0.8.0-beta.12] — 2026-08-05”- VM boot disk size.
swarm vm create --disk-size <GB>(validated 10–2048) provisions a custom boot disk; unset means the plan’sstorage_sizeresolved at create time (fallback 25 GB with a logged warning when the plan can’t be resolved). Storage-device construction extracted to a pure, tested function.
[0.8.0-beta.11] — 2026-08-05
Section titled “[0.8.0-beta.11] — 2026-08-05”- Env injection now uses
devcontainer up --remote-envinstead of the nonexistent--envflag, which made every--env/--secretdeploy fail withUnknown argument: envat thedevcontainer upstep.--remote-env NAME=VALUEis the CLI’s mechanism for container env vars (applied when executing user commands, postCreateCommand included).
[0.8.0-beta.10] — 2026-08-05
Section titled “[0.8.0-beta.10] — 2026-08-05”Changed
Section titled “Changed”- Secret names must be valid env var keys (
[A-Za-z_][A-Za-z0-9_]*) on both the CLI and the API, since the secret name is injected verbatim as the environment variable name. Dashes/dots are rejected with a clear message.
[0.8.0-beta.9] — 2026-08-04
Section titled “[0.8.0-beta.9] — 2026-08-04”- Container env injection.
swarm container deploygains--env KEY=VALUE(repeatable) and--env-file(repeatable;#comments/blank lines ignored;--envwins on conflicts). Env is stored in the container record, exposed as key names only incontainer list/info, and injected at start time by the agent viadevcontainer up --env(values redacted from agent error output). - Named secret store.
swarm secret set/list/removebacked by a0600secrets.json(upsert semantics for rotation);GET/POST/DELETE /api/secretsmTLS endpoints returning names andhas_valueonly.container deploy --secret <name>resolves named entries at dispatch (inline env wins on key conflicts); a deploy referencing an unknown secret is rejected with 400 naming it. - Deploy-worker tests synchronized with channels (race-clean).
[0.8.0-beta.8] — 2026-08-04
Section titled “[0.8.0-beta.8] — 2026-08-04”swarm git-credential add/list/remove— register per-host git credentials from the CLI (token read from stdin by default). Backed by a persistedgitCredStore(git-credentials.jsonnext to the registry,0600) and mTLS-authedGET/POST /api/git-credentials,DELETE /api/git-credentials/{host}. The deploy worker resolves from registered ∪ config credentials, registered entries winning on host conflicts. Tokens are never returned by the API (has_tokenonly).
[0.8.0-beta.7] — 2026-08-04
Section titled “[0.8.0-beta.7] — 2026-08-04”- Server-side git credentials —
git_credentialsconfig ([{host, username?, token}]). The deployment worker matches the repo URL’s host and passes the credential to the agent inCreateContainerRequest; the agent injects it into the clone URL (http/https only) and redacts tokens from git error output. The registry and stored records keep the clean repo URL — no morehttps://<token>@in--repo.
[0.8.0-beta.6] — 2026-08-04
Section titled “[0.8.0-beta.6] — 2026-08-04”- Private network VM creation failed with the provider’s
IP_ADDRESS_MISSING: the private interface had noip_addressesentry. It now sends{family: "IPv4"}(DHCP-assigned within the network’s range), matching the public/utility interfaces. Covered byTestNetworkInterfaces_*.
[0.8.0-beta.5] — 2026-08-04
Section titled “[0.8.0-beta.5] — 2026-08-04”- Private networks (V1). Server config
private_network(name/create_if_missing/ip_network),private_networkslist form, andprivate_network_id(existing UUID). Provider:ListNetworks+EnsurePrivateNetwork(UpCloudGetNetworksInZone/CreateNetwork,managed-by: swarmlabel);CreateVMParams.PrivateNetworkIDadds a private interface.PrivateIPincloud.VM, the registry, and the API; refreshed on list/info. swarm vm networks— lists account networks per zone with aserver inmarker (fromserver_private_ipconfig).vm create --network— per-VM network selection (existing account network by name/ID, or configured create-on-demand; unknown → 400).--hostnameflag onvm createwith server-side validation.
Changed
Section titled “Changed”- VM names are no longer used verbatim as the provider hostname: the
hostname is derived by sanitizing the name (lowercase,
[a-z0-9-], collapsed hyphens, 63-char cap,swarm-<ts>fallback).--namestays the console title verbatim.
vm create --name "Swarm Node 1"failed withHOSTNAME_INVALID; hostname derivation resolves it.
[0.8.0-beta.4] — 2026-08-02
Section titled “[0.8.0-beta.4] — 2026-08-02”swarm vm plans— surface the existingGET /api/plansendpoint in the CLI (name, CPU, memory, disk, tier). Plan codes are provider-validated (PLAN_INVALIDon unknown codes); this makes them discoverable.swarm vm zones— newGET /api/zonesendpoint (cloud.Provider.ListZones, UpCloudGetZones),Client.ListZones, and the CLI command.cloud.LoggingProvider.ListZonespass-through.
Changed
Section titled “Changed”- User guide: zones/plans sections and command reference updated.
[0.8.0-beta.3] — 2026-08-02
Section titled “[0.8.0-beta.3] — 2026-08-02”- Operator SSH keys —
swarm ssh-key add/list/removeregisters public keys that are baked into every newly created VM alongside the server’s own key. Persistedkeys.jsonstore next to the registry; mTLS-authedGET/POST /api/ssh-keysandDELETE /api/ssh-keys/{name};cloud.CreateVMParams.SSHKey→SSHKeys(upcloud sends all). postinstgenerates the server SSH keypair when absent — a fresh install no longer fails everyvm createwith “read public key: no such file”.EnvironmentFile=-/etc/swarm/swarm-server.envin the unit for optional provider credentials (UPCLOUD_TOKEN, …).
Changed
Section titled “Changed”- Binaries install to
/usr/bin— systemd unit, deb (build-debs.sh), and bare-metal installer aligned; config examples pointagent_binary_pathat/usr/bin/swarm-agent. FHS-correct for distro packages; dpkg removes the old/usr/local/binfiles on upgrade. - Brew formula is written to
Formula/in the homebrew tap (the deprecated brews pipe defaulted to the repo root; beta.2’s formula was repaired manually, this makes it automatic).
[0.8.0-beta.2] — 2026-08-02
Section titled “[0.8.0-beta.2] — 2026-08-02”tls_sansserver config — extra hostnames/IPs on the self-signed server certificate (defaults:localhost,127.0.0.1). Fixes TLS hostname mismatch when clients reach the server by a public hostname. No client re-login required (clients pin the CA, not the per-start leaf).- Gemfury apt distribution —
make debbuildsswarmandswarm-serverDebian packages (server bundles the agent);make publish-gemfurypushes them to theneuralmuxrepo; the release workflow builds debs via goreleaser nfpm and pushes them with theFURY_TOKENsecret. Client install via a GPG-signedapt.fury.iosource line. - Homebrew tap — goreleaser
brewsconfig publishes a formula tohyperengineering/homebrew-tap; the workflow syncs darwin binaries to the tap.brew install swarmon macOS.
Changed
Section titled “Changed”- Server config: new
tls_sanskey; packaging example updated. - User guide: authentication, credential lifecycle (token TTL, single-use, expiry, revocation, CA rotation), apt install path, proxy mTLS usage.
- Clients connecting to the server by hostname failed TLS
verification (
certificate is valid for localhost, not …);tls_sansresolves it.
[0.8.0-beta.1] — 2026-08-01
Section titled “[0.8.0-beta.1] — 2026-08-01”- mTLS authentication for the swarm-server API and container HTTP
proxy. Every request must present a client certificate signed by
the swarm CA, except
/healthz,/ca, and/enroll. Cert-less requests get 401; unknown-CA certs fail the handshake. The proxy inherits auth with no per-route work (single mux wrapper). - Swarm CA + PKI (
internal/pki). Ed25519 CA generated at first start next to the registry (ca.key,ca.crt), CA-signed self-signed server cert (or operatortls_cert/tls_keyoverride), and CSR signing for client certificates. Stdlibcrypto/x509only. - One-time enrollment. First start prints a single-use token
(default 15m TTL, hashed at rest, persisted across restarts);
POST /enrollexchanges token + CSR for a ~1-year client cert.swarm server tokenmints fresh tokens. swarm login. Generates a client keypair, enrolls over TLS (trust-on-first-use pin, or--ca), stores~/.swarm/{client.crt, client.key, server.pem}. The pinned anchor is the CA cert, so credentials survive server restarts.- Operator admin surface.
swarm user list,swarm user revoke <serial>(instant, serial-list based, persisted), backed byGET /api/users,POST /api/revocations,POST /api/enroll-tokens(all mTLS-authenticated). - Public
GET /ca— the CA bundle for scripted clients. - TLS test harness for the server suite: shared test CA + authed default client; all handler tests now run over TLS with the auth middleware enforced.
Changed
Section titled “Changed”internal/server.Newnow returns(*Server, error)(CA/token-store setup can fail) and takes the auth config fields.- Server config: new
ca_dir,tls_cert,tls_key,enroll_token_ttlkeys;api.EnrollResponsecarriesca_pem. - CLI
getAPIClient()now loads mTLS credentials and upgradeshttp://server URLs tohttps://(with a one-time warning). - Server serves HTTPS (
ListenAndServeTLSwith the CA-signed cert).
Removed
Section titled “Removed”- Unauthenticated access to the API and proxy.
Security
Section titled “Security”- The previously documented “operate swarm-server on a network you control” posture is replaced by per-request client-certificate authentication. See RELEASE_NOTES.md for the upgrade path.
[0.7.0-beta.2] — 2026-05-24
Section titled “[0.7.0-beta.2] — 2026-05-24”A hardening release driven by a full audit of the v0.7.0-beta.1 codebase. Every Critical and High audit finding is closed; most Medium and Low findings are too. Behavior is preserved on the happy path; several incoherent or insecure code paths were tightened or removed.
Security
Section titled “Security”- SSH host-key verification is no longer disabled. Replaced
InsecureIgnoreHostKeywith a file-backed trust-on-first-use store (internal/sshclient.HostKeyStore). The known-hosts path defaults to<registry-dir>/known_hosts.jsonand is configurable via the newknown_hosts_pathfield inswarm-server.json. On VM destroy the host key is dropped so a future VM at the same IP can re-TOFU. The v0.7.0-beta.1 “Known limitations” entry is now resolved. - Bumped
golang.org/x/cryptoto v0.52.0 to clear five reachable vulnerabilities reported bygovulncheck(GO-2026-5013/5017/5018/5019/5020). - JSON request bodies are now bounded. The server caps each
decode at 1 MiB and the agent at 64 KiB via
http.MaxBytesReader, returning 413 instead of streaming an arbitrary-size body into memory. - Alias validation. Container aliases must match
^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$before they’re accepted by the deployment or rename endpoints. Previously a slash or percent- encoded sequence could land in the registry and break downstream URL handling. - CLI bounds error-response reads.
api.parseErrorno longer reads error bodies unbounded; a hostile or buggy server can’t exhaust CLI memory by streaming a giant 4xx/5xx body.
Breaking
Section titled “Breaking”- Removed
swarm container shellandswarm container port. These commands had silently been broken since the V6 client/server split (they read SSH connection details that the server never populated). Usessh root@<vm-ip> -- docker exec -it <docker-id> bashdirectly; for HTTP services prefer the container HTTP proxy. Documented in the README and user guide. - Removed the sync container-deploy endpoints
POST /api/containersandPOST /api/containers/batch, plus the matchingClient.DeployContainer/BatchDeployContainermethods and theContainerDeployRequest/ContainerBatchDeploy*types ininternal/api. They had no non-test callers; deploys go throughPOST /api/deployments. sshclient.Newrequires aHostKeyCallback. PassHostKeyStore.Callback()in production.nilis rejected.cloud.Provider.ListTemplatestakes azoneparameter. Implementations should filter server-side when supported; the contract permits client-side filtering as a fallback (the UpCloud implementation does this since the SDK has no zone filter).registry.RemoveContainersByStatus(status, olderThan)now takes an age threshold. Callers must specify how old a record must be before they sweep it.- Agent
Containerstruct dropped three fields (RemoteUser,Shell,ForwardPorts) along withagent.ParseDevcontainerConfigand theinternal/sshclient/exec.goshell/tunnel command builders. All were consumed only by the removed CLI shell/port path.
- Configurable in-container bootstrap script. The agent’s new
--bootstrap-urlflag (and matchingagent_bootstrap_urlin the server config) points at a script that gets piped to bash inside every newly-created container. The default is now “skip the bootstrap step entirely” — the previous v0.7.0-beta.1 hardcodedhttps://example.com/bootstrap.shreliably failed because example.com returns HTML, not a shell script. - Graceful shutdown for
swarm-server. Handles SIGINT/SIGTERM, drains in-flight deploy workers, and gives them a configurable grace period before cancelling their contexts as a last resort. HTTP server timeouts (ReadHeaderTimeout,ReadTimeout,IdleTimeout) are now set explicitly. - Deployment store eviction. A background goroutine evicts terminal deployments older than the retention window (default 1 hour) so a long-running server doesn’t leak deployment records.
- TTL on agent auto-update. The “agent commit matches server” mark expires after 5 minutes by default so a stale agent that gets rebuilt out-of-band is eventually re-detected.
- Per-VM parallelism in container list.
handleContainerListnow reconciles every bootstrapped VM concurrently. With N VMs the list latency drops fromN × dial-timeto roughly one dial time. internal/statuspackage centralizes the container and deployment status strings used on the wire. A small test pins the values so renames trip a check instead of silently breaking deployed agents.- Shared
cloud.ParseLevel/cloud.LevelNamefor level-string parsing; the duplicate copies incmd/swarm-server/main.goandinternal/serverare gone. - Cancellable context in the TUI. Every API call uses
app.ctxinstead of a freshcontext.Background(), so Ctrl+C cancels in-flight requests instead of hanging.
- The agent stops mutating users’
devcontainer.json. v0.7.0-beta.1 stripped comments, re-indented, and injected--label runArgsincluding a stray emptyswarm.ref=label. The label-injection path is gone; reconciliation has always primarily used the.swarm.jsonsidecar (Phase 1) and still does. - Alias conflicts on deploy return 409 Conflict instead of silently clobbering the existing container’s registry record.
- Registry update failures are now logged instead of swallowed
via
_ =. 16 call sites across server handlers and deploy workers use the news.updateContainer/s.updateVMhelpers. - Agent error responses use a JSON envelope (
{"error":"..."}) matching the server’s shape. Everyhttp.Errorcall site was swept to a newwriteErrorhelper. - CLI poll-now-then-sleep.
pollDeploymentno longer wastes the first 10 seconds after dispatching a deploy before checking status. - Hoisted
ListVMs()out of an inner loop inhandleContainerList, dropping the dead-VM-cleanup cost from O(N×M) to O(N+M). upcloud.ListVMsdocumented as IP-summary-only. The interface contract now states that callers needing IPs must follow up withGetVM; the implementation hasn’t changed but the surprise is removed.- Stale “deploying” cleanup respects an age threshold. The startup sweep only removes entries older than 30 minutes so a brief restart-overlap with an in-flight deploy doesn’t drop a container record out from under the running worker.
- Container HTTP proxy prefix-trim is encoding-safe. Uses
r.URL.EscapedPath()andurl.PathEscape(aliasOrID)so any percent-encoded characters in the path can’t confuse the split.
Changed
Section titled “Changed”- All binaries log via
log/slog. Replaced the stdliblog.Printf/log.Fatalfcalls acrosscmd/swarm-server,cmd/swarm-agent, andinternal/server/agent_connector.go. The agent gained a matching--log-levelflag. - Body buffering in logging paths is gated on log level.
cloud.LoggingTransportandserver.loggingMiddlewarepreviously buffered full request/response bodies on every call regardless of log level; both now short-circuit unlessTrace/Debugis enabled. swarm-server/server.goandswarm-agent/main.gosplit by concern. Server: lifecycle, helpers, middleware, plus onehandlers_*.goper route group (vm/container/deployment/proxy/ loglevel). Agent:main.go(server + healthz),lifecycle.go,reconcile.go,proxy.go. Largest production file is nowhandlers_deployment.goat ~385 LOC (was 1,380).- Test files split to match.
server/server_test.gowent from 2,774 LOC to six per-concern files (largest ~1,000).tui/app_test.gowent from 2,880 to six (largest ~1,100).swarm-agent/main_test.gowent from 1,795 to four (largest ~680). - CLI test suite is ~40s faster. Pinning the deploy poll
interval to 1ms in test setup shrinks
cmd/swarmfrom 41s to 1s. - Container status strings are constants. Magic strings like
"deploying"/"running"/"failed"now referenceinternal/statusconsts.
Removed
Section titled “Removed”internal/agent/devcontainer.go(parser for fields no longer used).internal/sshclient/exec.go(shell/tunnel command builders).- Agent’s hand-rolled JSONC comment stripper (no longer needed once the destructive devcontainer.json mutation went away).
Internal
Section titled “Internal”- Added
TECH_DEBT_AUDIT.mddocumenting the audit input that drove this release. The 56 findings it lists are all addressed by the commits in this release. staticcheck ./...is now silent;go test -race ./...passes across the whole tree;govulncheck ./...reports no vulnerabilities.
[0.7.0-beta.1] — 2026-04-17
Section titled “[0.7.0-beta.1] — 2026-04-17”- Container HTTP proxy — services running inside deployed containers
(bound to
0.0.0.0:8900by default) are reachable viaANY /api/containers/<id>/proxy/<path>. Includes server routing, agent-side reverse proxy, and configurable proxy port. - Async container deployment — deploy requests return immediately; progress is tracked via polling. Both the CLI and TUI use the new async API.
- TUI dashboard — full terminal UI for managing VMs and containers:
- VM and Container tabs with auto-refresh
- VM-to-container drill-down (Enter/Esc)
- Text filter/search across all columns
- Create VM form with template picker
- Batch container deploy with plan picker
- Destroy VM / remove container with confirmation dialogs
- Live deployment status with fast-polling and error display
- Container rename from the TUI
- Agent bootstrap — automatic agent installation and startup after devcontainer creation.
- Agent auto-update — version handshake between server and agent; the server re-bootstraps the agent when versions diverge.
- Agent reconciliation — server recovers container state from agents on startup.
- Container rename —
swarm container rename <id> <alias>in both CLI and TUI. - Docker labels and
.swarm.jsonmetadata — containers carry identification metadata for recovery after agent restarts. - Server logging — three-tier structured logging (info / debug / trace).
- Stale container cleanup — deploying containers stuck in the registry are cleaned up on server startup.
- Reference example:
examples/proxy-echo/demonstrating the HTTP proxy. - User documentation: container proxy guide, client integration guide, and design/shaping documents.
- Deploy/list race that lost container aliases.
- Agent auto-update not retrying after bootstrap failure.
resolveCredentialshandling ofprovider_token_cmd.ReplaceAttrpanic on log-level key collision.- Bootstrap reliability and end-to-end container deployment flow.
Known limitations
Section titled “Known limitations”- SSH host key verification is disabled (
InsecureIgnoreHostKey) — acceptable for beta but should not be used in zero-trust environments. Tracked for a future release.
[0.6.0] — 2026-02-08
Section titled “[0.6.0] — 2026-02-08”Initial internal release with VM provisioning, container deployment, CLI, and UpCloud provider integration.