WP-02 · Release engineering

Fleet operations: one-command provisioning, numbered releases on a pinned runtime, and nightly backups across tenant servers

Abstract. I describe the control plane that provisions and maintains a fleet of single-tenant agent servers, one per small-business owner. A tenant is created by a one-command, two-phase installer that pins the upstream agent runtime to a fixed commit, injects secrets over standard input, and then converges the host to the head of a declarative release manifest. Releases are ordered, numbered and composed of named steps; each step is guarded by a precondition probe so that every step is safe to run twice. The updater computes the minimal step set from a version marker written on the tenant, defers restarts behind a conversational quiescence gate, and promotes the new version only after a liveness check on the messaging channel. Nightly backups are streamed, verified and atomically published under a 14-archive retention window. I report the release history for the first two days of the current design, the concurrency incident that produced the per-tenant lock, and the open problems.

Written 2026-10-23 from the fleet tooling, the release manifest and project notes. Hosts, tenants and credentials are omitted. Every number below is counted from source, not projected.

1. System model

The fleet is a set of tenants. Each tenant is one small cloud server held in an account the owner controls, running one instance of an open source agent runtime plus a thin layer of owner-facing services: a forms store, a reverse proxy for private pages, a settings service and a mail intake. Nothing is shared between tenants at runtime. A single operator host holds the control plane: a tenant registry, the release manifest, step bundles, the updater, the backup job and a read-only status probe. The operator console, a page inside the operator's own CRM, is a thin client over the same scripts.

The tenant registry is deliberately small. Each record carries a slug, the owner's name and business, a trade, a role (test or client), a daily-backup flag and a flag that controls whether the owner is texted about updates. Provider context for tenants created by the self-service path lives in the same record. At the time of writing the registry held four tenants.

Two invariants shape everything below. First, the only state the updater trusts about a tenant is what the tenant itself reports: the control plane keeps logs, but the version of record is a marker file on the tenant. Second, every operation that mutates a tenant holds that tenant's lock for its full duration.

2. Provisioning

Fig. 1 · Provisioning pipeline

Provisioning pipelineSix stages under one per-tenant lock: image rebuild, reachability, phase A (packages, service account, runtime pinned to a commit), phase B (secrets over stdin, configuration, memory, skills, gateway), convergence through every release, and per-tenant personalization.ONE EXCLUSIVE PER-TENANT LOCK, HELD FROM THE FIRST LINE TO EXIT01Image rebuildclean OS imagecloud-init seed02ReachabilitySSH poll, 60 x 10 sfresh host key03Phase AOS packagesservice accountruntime @ commit04Phase Bsecrets via stdinconfig, memory,skills, gateway05Convergerelease v0 tolatest, in order06Personalizetemplate renderedper tenantBEFORE 01: THE PROVIDER'S RECORD OF THE SERVER MUST MATCH THE TENANT RECORD, OR THE RUN STOPS
Six stages under one lock. The installer refuses to start if the provider's record of the server does not match the tenant registry.

Provisioning is a single command that takes a tenant slug. It is destructive by design: it rebuilds the server from a clean operating system image, so it is used both to create a tenant and to return one to a known state.

Identity check. Before anything is rebuilt, the installer asks the cloud provider for the server's public address and compares it with the tenant registry. A mismatch stops the run. This is the cheapest available guard against rebuilding the wrong machine.

Rebuild and reachability. The server is rebuilt with a cloud-init seed that creates an operations account. Because every rebuild produces a new host identity, the control plane re-establishes trust with the rebuilt host before continuing. It then polls SSH every 10 seconds, up to 60 times, and blocks until cloud-init reports completion.

Phase A. Operating system packages, a dedicated unprivileged service account, and the agent runtime installed at a fixed upstream commit. Pinning the commit makes a fresh tenant reproducible: two tenants provisioned a day apart run the same runtime until a release explicitly moves them.

Phase B. Secrets are written over the SSH pipe into a root-only file through standard input, never through command-line arguments, which leak into process tables and audit logs. Configuration, memory, skills, the web layer, the personality file and the messaging gateway follow. Per-tenant parameters are passed as environment for that single invocation.

Convergence. The installer then releases its own lock and hands off to the updater, which takes the same lock, finds no version marker, treats the tenant as version zero and replays the entire release manifest in order. A fresh tenant and a long-lived tenant therefore reach the head of the manifest through the same code path. This is the property that makes the manifest the specification of a tenant, rather than a changelog.

Personalization. Last, a personality template is rendered for the tenant, with the tenant's own forwarding address and page host substituted in. It runs after convergence because several release steps patch the stock personality text and expect to find it unmodified.

A second entry point resumes from convergence for a tenant whose phase B already completed, so a failure late in provisioning does not require a full rebuild.

3. The release manifest

A release is a record with four fields: a version, a title, an ordered list of step names, and two pieces of prose: details for the operator and a one-line note written for the owner. Versions are date based with an optional sequence suffix. Comparison extracts every digit group and compares the resulting integer tuples, so a sequence suffix orders correctly against the bare date.

The manifest covers 21 releases published over two days, 18 of them on 2026-10-22. They reference 25 step entries, which resolve to 22 distinct step handlers; four releases carry two steps and one step is shared by two releases. The titles below are the operator titles, lightly generalized.

VersionTitleSteps
2026.10.22Owner forms and motion1
2026.10.22.2Edit links and a delegated page builder2
2026.10.22.3Pick your look1
2026.10.22.4Private AI hosts only2
2026.10.22.5Smaller chat model1
2026.10.22.6Runtime upgrade to a newer pinned version1
2026.10.22.7Email is read only1
2026.10.22.8No secrets in chat2
2026.10.22.9Your own forwarding email1
2026.10.22.10First-contact intro and guide1
2026.10.22.11Plain words, no slash commands1
2026.10.22.12No system notices1
2026.10.22.13Hands off after the build1
2026.10.22.14Links always arrive1
2026.10.22.15Your settings page1
2026.10.22.16Better pages1
2026.10.22.17A real guide1
2026.10.22.18Your guide, narrated1
2026.10.23Offers, drafted1
2026.10.23.1Listing sheets1
2026.10.23.2Safer settings links2

Three classes of step appear. Install steps ship a bundle (a directory of scripts and assets) to a private staging directory and run its installer. Patch steps make a narrow edit to the personality or configuration, after first copying the file to a step-specific backup. Upgrade steps move a component to a new version, after first taking a snapshot of the agent's state directory. Trade-specific steps are conditional on the tenant's trade in the registry: a real estate step installs its skills only on real estate tenants, while refreshing shared guide content everywhere.

Every step is required to be idempotent, and most enforce it with a precondition probe rather than by convention. A forms step exits early if the forms service is already installed; a model step exits early if the configured chat model already matches; a runtime upgrade step exits early if the reported runtime version is already at or above the target. The rest are written so that a second run produces the same file as the first.

4. Convergence and the updater

Fig. 2 · Updater control flow

Updater control flowProbe, diff, apply, quiesce, restart and liveness on the top row, then commit the version marker, notify the owner if opted in, and record history. Any exception moves the run to a stopped state without writing the marker.Proberead-only statusover stdinDiffreleases > markersteps dedupedApplyeach stepguarded, rerunnableQuiescelast message> 90 s agoRestartgatewayport up, 120 s maxLivenesschannel connected120 s maxCommitwrite versionmarkerNotifyowner text,opt-in, 480 charsRecordhistory line,state filepoll 15 s, 40xAny exceptionstate: stopped · history: ok=falsestaging dir removed in finallyno marker written, no owner text
The happy path runs left to right then down. Any exception lands in the dashed lane: the run is marked stopped, history records the failure, and the version marker is not advanced.

Probe. The updater begins by sending a read-only status script to the tenant over standard input. The script installs nothing and prints one JSON object: the version marker, the runtime version, the configured models and whether the chat model is pinned to specific hosts, the routes registered on the gateway, the state of each service and the state of each messaging platform. It never prints secrets.

Diff. The set of pending releases is every release whose version is greater than the marker. Their steps are flattened in release order and deduplicated, so a step that appears in two pending releases runs once, at its first position.

Apply. Each step receives a fresh status object before it runs, so preconditions see the effects of earlier steps in the same run. Bundles are streamed as a compressed archive into a staging directory created with a random name under a private 0700 parent; the path returned by the tenant is validated against an expected pattern before it is used, and the directory is removed in a finally clause whether the run succeeds or not.

Quiesce. A gateway restart terminates in-flight work, including page builds that take minutes. The updater therefore reads the timestamp of the most recent message from the agent's state database, opened read only, and restarts only after 90 seconds of silence. It polls every 15 seconds, at most 40 times, which bounds the wait at 10 minutes. Restarts were moved to the end of the run, once, rather than after each step, for the same reason.

Restart and liveness. After the restart the updater waits up to 120 seconds for the gateway's local port to accept connections, then up to 120 seconds for the status probe to report both the gateway service active and the messaging channel connected. If the channel does not reconnect the run raises, and the version marker is not written. A tenant is never reported as updated while its owner cannot reach it.

Commit, notify, record. Only then is the marker written. If the tenant's update-text flag is set, the owner receives one message that begins "Queva updated to" the new version and joins every pending release's owner note; if that exceeds 480 characters, only the newest note is kept. The operator can rewrite the text from the console before the run, and the server rejects text that does not start with the required prefix. Client tenants currently update silently; only the test tenant texts. Each run appends one line to a per-tenant history log, success or failure, with the from and to versions, the releases applied and the log file name.

A plan mode runs the probe and diff and prints the result without taking the lock or touching the tenant, so the console can show what an update would do while another job is running.

5. Concurrency control

Every job that mutates a tenant takes an exclusive, non-blocking lock on a per-tenant lock file as its first action and holds it until exit. A second job on the same tenant exits immediately with status 75 and a message that the tenant is busy. The lock lives on a file rather than a process ID, so it survives a restart of the console service, and it is released by the kernel when the process dies, so a crashed job cannot wedge a tenant.

The lock was not always this wide. On 2026-10-22 it lived only inside the updater, the last phase of provisioning. A reinstall started by hand therefore held no lock during its first two phases, the console saw the tenant as idle, and a test of the console's wipe action started a second reinstall, which issued a second rebuild while the first was mid-way through phase A. Both runs were killed and a clean third run completed; about 15 minutes were lost and no data, since the tenant was being wiped anyway. Two rules came out of it: the installer takes the lock on its first line, and destructive actions are never tested against a real target, even to observe a refusal.

6. Backups

Fig. 3 · Backup pipeline

Backup pipelineTenant paths are streamed over SSH with caches excluded into a private partial file, verified, atomically renamed, and pruned to the newest 14. Restores are manual.Tenant pathsstate, pages, formsproxy + mail configStreamtar over SSHcaches excludedStage.part filemode 0600Verifyexit 0 or 1>= 10 KB, lists OKPublishatomic renamedated archiveRetainkeep newest 14append log lineverification fails: partial file deleted, run raisesrestore path: manual runbook today · archives stored private to the control plane
Write to a private partial file, verify, then rename. A reader of the backup directory never sees a half-written archive.

Each tenant is backed up nightly by streaming a compressed archive over SSH into the control plane. The archive covers the agent's state directory (settings, memory and the personality), the owner's pages, the forms database and its hooks, the reverse proxy and mail configuration, the gateway's service units, the owner-facing helper programs and their state. Downloaded tools, the runtime's own checkout, caches and logs are excluded, which keeps the archive to the state that cannot be regenerated.

Archives are treated as sensitive and are written with owner-only permissions on the control plane.

The job writes to a dot-prefixed partial file, accepts a tar exit status of 0 or 1 (1 means a file changed while being read, which is expected on a live tenant), rejects anything under 10 KB, lists the archive to prove it is readable, and only then renames it into place. Retention keeps the newest 14 archives and deletes the rest; every success appends a line with time, file and size.

Restores are manual. There is no restore button and no automated restore test yet, which is the largest gap in this design and is listed in section 9.

7. Observability

Every run writes a timestamped log and a small state file that the console polls. The state file is written to a temporary name and moved into place, so the console never reads a partial JSON document. It records the process, start and finish times, the from and to versions, the planned steps, the steps completed so far and the result. Together with the per-tenant history log, this lets the console show a version, a "what's in this update" view and a timeline per tenant without querying the tenant.

The status probe doubles as a configuration audit. Because it reports models, host pinning, routes and service states, drift between what the manifest says and what a tenant runs is visible from the console.

8. Failure analysis

Concurrent mutation. Described in section 5. Root cause: a lock scoped to one phase instead of the job.

Personality changes and live sessions. The runtime's gateway keeps the system prompt a conversation was built with. A release that patched the personality file therefore did not change the behavior of a conversation already open on the tenant: the owner's next message was answered under the old rules. Archiving the session was not enough, since the gateway resumed it; deleting the session and restarting while idle was. Releases that change the personality now carry that step, and the owner is told the conversation starts fresh. A gentler mechanism is listed as open.

Shared state during testing. Test builds run in the default profile on a live tenant share its conversation database with the owner. Tests now run in a scratch profile.

Generation one. The first-generation installer, started on 2026-09-28, wrapped the runtime in many services of its own and was validated by an acceptance run on a fresh server: installed twice, exit 0, 59 checks passed, 0 failed, 4 expected warnings. It was retired on 2026-10-21 in favor of the stock runtime plus this release layer, because most owner-facing problems traced to the wrapper rather than the runtime and every change had to be threaded through it.

9. Open problems

  1. Restore automation and restore testing. Backups are verified for readability, not for recoverability.
  2. Rollback. Steps are idempotent forward; there is no inverse. Recovery from a bad release is a forward fix or a rebuild.
  3. Staged rollout. Releases go to the test tenant first and to clients on an operator click. There is no automated canary or health-gated promotion across tenants.
  4. Session-preserving personality updates. Section 8.
  5. Drift detection. The probe exposes configuration, but nothing yet compares it to an expected state per version.

10. Claim table

ClaimEvidenceConfidence
21 numbered releases were published across 2026-10-22 and 2026-10-23, 18 of them on 2026-10-22.Release manifesthigh
The 21 releases reference 25 step entries, which resolve to 22 distinct step handlers.Release manifest and updater dispatch tablehigh
Version comparison is a numeric tuple over every digit group in the version string.Updater sourcehigh
The updater waits for at least 90 seconds without a message, polling every 15 seconds up to 40 times (10 minutes).Updater sourcehigh
After a restart it waits up to 120 seconds for the gateway port and up to 120 seconds for the messaging channel to report connected.Updater sourcehigh
Owner update texts are opt-in per tenant and capped at 480 characters.Updater source and tenant recordhigh
Backups keep the newest 14 archives and reject archives under 10 KB or that fail to list.Backup sourcehigh
Restores are performed by hand; no restore automation exists yet.Backup source and project noteshigh
One exclusive per-tenant file lock is shared by provisioning, updates and the operator console.Installer and updater source; incident note 2026-10-22high
The runtime is installed at a fixed upstream commit during phase A.Installer sourcehigh
The first-generation installer recorded 59 pass, 0 fail and 4 expected warnings on a fresh server, run twice.Project notes, September 2026medium
Personality changes do not reach conversations already open on a tenant until the session is reset.Incident note 2026-10-22high

11. Sources