Skip to content

Changelog

All notable changes to Swarm are documented in this file.

  • VM boot disk size. swarm vm create --disk-size <GB> (validated 10–2048) provisions a custom boot disk; unset means the plan’s storage_size resolved 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.
  • Env injection now uses devcontainer up --remote-env instead of the nonexistent --env flag, which made every --env/--secret deploy fail with Unknown argument: env at the devcontainer up step. --remote-env NAME=VALUE is the CLI’s mechanism for container env vars (applied when executing user commands, postCreateCommand included).
  • 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.
  • Container env injection. swarm container deploy gains --env KEY=VALUE (repeatable) and --env-file (repeatable; # comments/blank lines ignored; --env wins on conflicts). Env is stored in the container record, exposed as key names only in container list/info, and injected at start time by the agent via devcontainer up --env (values redacted from agent error output).
  • Named secret store. swarm secret set/list/remove backed by a 0600 secrets.json (upsert semantics for rotation); GET/POST/DELETE /api/secrets mTLS endpoints returning names and has_value only. 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).
  • swarm git-credential add/list/remove — register per-host git credentials from the CLI (token read from stdin by default). Backed by a persisted gitCredStore (git-credentials.json next to the registry, 0600) and mTLS-authed GET/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_token only).
  • Server-side git credentials — git_credentials config ([{host, username?, token}]). The deployment worker matches the repo URL’s host and passes the credential to the agent in CreateContainerRequest; 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 more https://<token>@ in --repo.
  • Private network VM creation failed with the provider’s IP_ADDRESS_MISSING: the private interface had no ip_addresses entry. It now sends {family: "IPv4"} (DHCP-assigned within the network’s range), matching the public/utility interfaces. Covered by TestNetworkInterfaces_*.
  • Private networks (V1). Server config private_network (name/create_if_missing/ip_network), private_networks list form, and private_network_id (existing UUID). Provider: ListNetworks + EnsurePrivateNetwork (UpCloud GetNetworksInZone/CreateNetwork, managed-by: swarm label); CreateVMParams.PrivateNetworkID adds a private interface. PrivateIP in cloud.VM, the registry, and the API; refreshed on list/info.
  • swarm vm networks — lists account networks per zone with a server in marker (from server_private_ip config).
  • vm create --network — per-VM network selection (existing account network by name/ID, or configured create-on-demand; unknown → 400).
  • --hostname flag on vm create with server-side validation.
  • 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). --name stays the console title verbatim.
  • vm create --name "Swarm Node 1" failed with HOSTNAME_INVALID; hostname derivation resolves it.
  • swarm vm plans — surface the existing GET /api/plans endpoint in the CLI (name, CPU, memory, disk, tier). Plan codes are provider-validated (PLAN_INVALID on unknown codes); this makes them discoverable.
  • swarm vm zones — new GET /api/zones endpoint (cloud.Provider.ListZones, UpCloud GetZones), Client.ListZones, and the CLI command.
  • cloud.LoggingProvider.ListZones pass-through.
  • User guide: zones/plans sections and command reference updated.
  • Operator SSH keys — swarm ssh-key add/list/remove registers public keys that are baked into every newly created VM alongside the server’s own key. Persisted keys.json store next to the registry; mTLS-authed GET/POST /api/ssh-keys and DELETE /api/ssh-keys/{name}; cloud.CreateVMParams.SSHKey → SSHKeys (upcloud sends all).
  • postinst generates the server SSH keypair when absent — a fresh install no longer fails every vm create with “read public key: no such file”.
  • EnvironmentFile=-/etc/swarm/swarm-server.env in the unit for optional provider credentials (UPCLOUD_TOKEN, …).
  • Binaries install to /usr/bin — systemd unit, deb (build-debs.sh), and bare-metal installer aligned; config examples point agent_binary_path at /usr/bin/swarm-agent. FHS-correct for distro packages; dpkg removes the old /usr/local/bin files 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).
  • tls_sans server 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 deb builds swarm and swarm-server Debian packages (server bundles the agent); make publish-gemfury pushes them to the neuralmux repo; the release workflow builds debs via goreleaser nfpm and pushes them with the FURY_TOKEN secret. Client install via a GPG-signed apt.fury.io source line.
  • Homebrew tap — goreleaser brews config publishes a formula to hyperengineering/homebrew-tap; the workflow syncs darwin binaries to the tap. brew install swarm on macOS.
  • Server config: new tls_sans key; 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_sans resolves it.
  • 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 operator tls_cert/tls_key override), and CSR signing for client certificates. Stdlib crypto/x509 only.
  • One-time enrollment. First start prints a single-use token (default 15m TTL, hashed at rest, persisted across restarts); POST /enroll exchanges token + CSR for a ~1-year client cert. swarm server token mints 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 by GET /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.
  • internal/server.New now 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_ttl keys; api.EnrollResponse carries ca_pem.
  • CLI getAPIClient() now loads mTLS credentials and upgrades http:// server URLs to https:// (with a one-time warning).
  • Server serves HTTPS (ListenAndServeTLS with the CA-signed cert).
  • Unauthenticated access to the API and proxy.
  • 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.

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.

  • SSH host-key verification is no longer disabled. Replaced InsecureIgnoreHostKey with a file-backed trust-on-first-use store (internal/sshclient.HostKeyStore). The known-hosts path defaults to <registry-dir>/known_hosts.json and is configurable via the new known_hosts_path field in swarm-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/crypto to v0.52.0 to clear five reachable vulnerabilities reported by govulncheck (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.parseError no longer reads error bodies unbounded; a hostile or buggy server can’t exhaust CLI memory by streaming a giant 4xx/5xx body.
  • Removed swarm container shell and swarm container port. These commands had silently been broken since the V6 client/server split (they read SSH connection details that the server never populated). Use ssh root@<vm-ip> -- docker exec -it <docker-id> bash directly; for HTTP services prefer the container HTTP proxy. Documented in the README and user guide.
  • Removed the sync container-deploy endpoints POST /api/containers and POST /api/containers/batch, plus the matching Client.DeployContainer / BatchDeployContainer methods and the ContainerDeployRequest / ContainerBatchDeploy* types in internal/api. They had no non-test callers; deploys go through POST /api/deployments.
  • sshclient.New requires a HostKeyCallback. Pass HostKeyStore.Callback() in production. nil is rejected.
  • cloud.Provider.ListTemplates takes a zone parameter. 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 Container struct dropped three fields (RemoteUser, Shell, ForwardPorts) along with agent.ParseDevcontainerConfig and the internal/sshclient/exec.go shell/tunnel command builders. All were consumed only by the removed CLI shell/port path.
  • Configurable in-container bootstrap script. The agent’s new --bootstrap-url flag (and matching agent_bootstrap_url in 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 hardcoded https://example.com/bootstrap.sh reliably 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. handleContainerList now reconciles every bootstrapped VM concurrently. With N VMs the list latency drops from N × dial-time to roughly one dial time.
  • internal/status package 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.LevelName for level-string parsing; the duplicate copies in cmd/swarm-server/main.go and internal/server are gone.
  • Cancellable context in the TUI. Every API call uses app.ctx instead of a fresh context.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 runArgs including a stray empty swarm.ref= label. The label-injection path is gone; reconciliation has always primarily used the .swarm.json sidecar (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 new s.updateContainer / s.updateVM helpers.
  • Agent error responses use a JSON envelope ({"error":"..."}) matching the server’s shape. Every http.Error call site was swept to a new writeError helper.
  • CLI poll-now-then-sleep. pollDeployment no longer wastes the first 10 seconds after dispatching a deploy before checking status.
  • Hoisted ListVMs() out of an inner loop in handleContainerList, dropping the dead-VM-cleanup cost from O(N×M) to O(N+M).
  • upcloud.ListVMs documented as IP-summary-only. The interface contract now states that callers needing IPs must follow up with GetVM; 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() and url.PathEscape(aliasOrID) so any percent-encoded characters in the path can’t confuse the split.
  • All binaries log via log/slog. Replaced the stdlib log.Printf / log.Fatalf calls across cmd/swarm-server, cmd/swarm-agent, and internal/server/agent_connector.go. The agent gained a matching --log-level flag.
  • Body buffering in logging paths is gated on log level. cloud.LoggingTransport and server.loggingMiddleware previously buffered full request/response bodies on every call regardless of log level; both now short-circuit unless Trace / Debug is enabled.
  • swarm-server/server.go and swarm-agent/main.go split by concern. Server: lifecycle, helpers, middleware, plus one handlers_*.go per route group (vm/container/deployment/proxy/ loglevel). Agent: main.go (server + healthz), lifecycle.go, reconcile.go, proxy.go. Largest production file is now handlers_deployment.go at ~385 LOC (was 1,380).
  • Test files split to match. server/server_test.go went from 2,774 LOC to six per-concern files (largest ~1,000). tui/app_test.go went from 2,880 to six (largest ~1,100). swarm-agent/main_test.go went 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/swarm from 41s to 1s.
  • Container status strings are constants. Magic strings like "deploying" / "running" / "failed" now reference internal/status consts.
  • 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).
  • Added TECH_DEBT_AUDIT.md documenting 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.
  • Container HTTP proxy — services running inside deployed containers (bound to 0.0.0.0:8900 by default) are reachable via ANY /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.json metadata — 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.
  • resolveCredentials handling of provider_token_cmd.
  • ReplaceAttr panic on log-level key collision.
  • Bootstrap reliability and end-to-end container deployment flow.
  • SSH host key verification is disabled (InsecureIgnoreHostKey) — acceptable for beta but should not be used in zero-trust environments. Tracked for a future release.

Initial internal release with VM provisioning, container deployment, CLI, and UpCloud provider integration.