Cloudfall is the operator's record for a small fleet run by an AI agent: the agent decides, Ansible executes, Cloudfall declares what the agent may do, checks every result and keeps the evidence behind every decision. The roadmap reaches that in two stories.
The first story, proven live in M0–M10:
A SaaS running on a PaaS such as Render gets one fresh Debian bare-metal or VPS host, runs Cloudfall, and within an hour has: a hardened baseline, firewall, monitoring, logging, PostgreSQL, Nginx with TLS, its application deployed from GitHub with health checks and rollback, and a DNS cutover checklist. An always-on operator then runs it there, with a receipt for every action.
The second story, under way (M15–M17, see the brownfield design): the operations catalog, the gate and the decision record are built and have run on the maintainer's own production fleet; safe edits and autonomy from the record are not built yet:
A team that already runs Ansible points its agent at its own repository. Cloudfall reads their inventory as the only config, exposes each of their playbooks as a declared operation with a risk level and a verify step, and writes an audit entry for every decision: what the fleet looked like, what was proposed, who approved, what verify reported.
Every milestone keeps the invariants that define Cloudfall: no action without declared config, no status without evidence, no compliance without audit, no decision without a record.
Milestone numbers are labels, not order. After M10 the priority is the brownfield track (M15–M17), because the record only compounds on fleets that exist; the density and PostgreSQL tracks (M11–M14) are demand-gated and follow.
- Public repository with license, CI, and contribution docs ✔
- Schemas, the validation CLI and Python API, audit pipeline, and generic engine roles ✔
One command turns an installed Debian host into a compliant Cloudfall server.
- Baseline packages, application users,
/srv/apps, UTC enforcement, SSH hardening from declaredSshPublicKeyresources, unattended security upgrades ✔ (task server:baseline) - nftables firewall role: default-deny inbound, allowances declared in the config, firewall evidence in observations and audit ✔
- Systemd timer units modeled and audited alongside services ✔
- Host metrics through the Loki/Grafana/Alloy stack ✔ — Alloy's embedded
unix exporter pushes to a loopback Prometheus over the existing mTLS
gateway, so no standalone
node_exporterport is exposed
Exit: the baseline run is idempotent (second run reports no changes) and
cloudfall audit reports the host compliant, including firewall and timers.
Met on 2026-09-08 on a disposable Hetzner Cloud Debian 13 server,
including live host metrics through the logging stack; see the
Hetzner proving-run report and
the earlier container dry run.
Each run caught and fixed a real bug (a firewall-template rendering error and
an observation-schema rejection of escaped systemd unit names), which is the
point of proving runs. Bare-metal specifics (software RAID, storage
provisioning) remain unexercised.
Installable, audited infrastructure services, starting with exactly two.
- A
Serviceschema: kind, version, servers, ports, bind policy, backup policy ✔ - PostgreSQL role: pinned install, localhost-only bind by default,
per-application databases and users from the config (peer authentication,
no database passwords), scheduled dumps, and a restore-proof command ✔
(
task services:deploy) - Nginx site role: virtual hosts rendered from
Domainresources, certbot with timer compliance, canonical client-address forwarding, and deployment receipts ✔ (task domains:deploy) - Audit integration: packages, units, config hashes, and listening-socket evidence proving each declared service binds only to its loopback address ✔
Catalog breadth (Redis, RabbitMQ, Elasticsearch, MySQL, Node runtimes) follows the same pattern afterward.
Exit: met on 2026-09-08 on a disposable Hetzner Cloud Debian 13 server — loopback-only PostgreSQL with a peer-authenticated application database, and an Nginx route with a real Let's Encrypt certificate issued through the role's webroot flow; see the M2/M3 proving-run report.
deploy() with rollback — the heart of one-command operation.
- Artifact builder on the management host (git ref → hashed tarball with
schema-valid release metadata) ✔ (
task artifact:build) - Deploy role: digest-verified transfer,
releases/unpacking, locked dependency materialization via pinned uv, environment-file installation, systemd unit fromComponent.service, health-check gate, symlink switch, automatic rollback on failed health checks, and release receipts ✔ (task deploy) - CLI and Python API operations
deploy(),rollback(),restart(),health()with structured JSON results ✔ (cloudfall deploy|rollback|restart|health, executing through the engine's process boundaries; explicit rollback to any retained release included) - Secrets v1: environment files generated from secret references (a local environment file per component is supported today; reference-driven generation shipped as M6's secrets v2)
Exit: the example backend component deploys end to end on a fresh host, and a
bad release rolls back automatically. Met on 2026-09-08 on the same
disposable Hetzner host as the M2 exit: a uv-locked ASGI component deployed
through cloudfall deploy with a passing health gate, and a broken release
failed its gate and rolled back automatically with the service staying
healthy; see the
M2/M3 proving-run report.
The run fixed six engine/CLI bugs. The false origin warning it left open for
single-host topologies was fixed in the M4/M5 run: domain evidence now
records an explicit origin skip when origin and proxy share a host.
cloudfall import render: maprender.yamlresources to Cloudfall applications, components, services, and domains, with environment files kept outside the config and a structured gap report for anything unmappable ✔ (blueprint files; the Render API and cron jobs are pending)- Data migration: managed-Postgres dump and restore with verification ✔
(
cloudfall data copy, the--datastep ofcloudfall migrate, and the confirm-gatedmigrate_databaseMCP tool: per-table row-count verification, a non-empty-target refusal guard, secret-file hygiene, and aDataMigrationReceipt; proven live in the guided data-migration report) - Cutover generator: DNS TTL lowering, parallel-run verification, switch, and a rollback window (DNS steps surface in the import report today)
- v1 limitation: native Python and Node builds; container runtimes are a tracked decision ✔ (documented and enforced — container services land in the gap report)
Exit: a real PaaS application migrated onto one host with its data, TLS,
monitoring, and rollback path. Met on 2026-09-08 in two stages: a
realistic blueprint migrated end to end
(M4/M5 report), then an
application actually hosted on Render — built by Render from this
repository, serving rows from its managed PostgreSQL — cut over with its
data to a byte-identical response on the Cloudfall host
(cutover report), and
finally the TTL-lowered DNS record cutover itself on an owned domain, with
the migrate plan pausing at dns-verify and resuming after the flip
(DNS cutover report). Every
wedge step is now proven live, including the guided data migration.
- MCP server over the Python API: read-only tools exposed freely; mutating tools
gated by explicit confirmation and always writing receipts ✔
(
cloudfall-mcpwith seventeen annotated tools covering import, artifact build, baseline/services/domains convergence, deploy/rollback/restart, and the full evidence pipeline) - A
migrateorchestrator chaining M1–M4 as a resumable plan ✔ (cloudfall migrateand themigrateMCP tool: persisted step progress, a DNS-verification pause at the cutover moment, and a final evidence pass requiring a compliant audit and healthy routes)
Exit: an AI agent completes the M4 migration through MCP without shell access
to the servers. Met on 2026-09-08: an AI agent drove the blueprint import
and the full ten-step migration (baseline through the final evidence pass)
purely through cloudfall-mcp with the confirmation handshake, taking a
fresh Debian host to a TLS-served, audit-compliant application in about
seven minutes, and a later session resumed the persisted plan as a no-op —
see the
M4/M5 proving-run report.
M0–M5 proved the wedge end to end. The next milestones make the migration routine and widen what it can carry, in rough priority order:
- Catalog breadth — Redis, MySQL, Elasticsearch, RabbitMQ, and Node runtime services following the PostgreSQL pattern: pinned installs, loopback-only binds, backup policies, audited evidence (Redis ✔ with RDB backups, restore checks, and Alloy metrics; the rest pending)
- Render API import ✔ —
cloudfall import render-apimaps a live workspace with real environment values and database-URL rewriting; cron jobs arrive with their schedules in the gap report, and modeling them as systemd timers stays tracked - Cutover generator ✔ — the migrate plan now carries explicit
ttl-lower(authoritative TTLs measured, pause while above 300s),parallel-run(the origin must serve every domain before DNS changes), the existingdns-verifyswitch pause, androllback-window(pre-switch answers recorded with an exact 24h revert recipe) - Secrets v2 ✔ —
cloudfall secrets render(and therender_secretsMCP tool) resolves declared secret references from a sops/age-encrypted secrets directory into per-component 0600 environment files, merging platform → application → component scopes; envelopes carry key names and hashes, never values, and the provider boundary admits managed backends later — see the secrets guide - Bare-metal provisioning — software RAID and storage layout for dedicated servers per the hybrid storage design, the one part of the server model still unexercised live
- Projects ✔ —
cloudfall initlays out a project (one directory per resource kind, README,.gitignore, and apyproject.tomlpinning Cloudfall to the running commit); only kind directories are read as resources, so a project carries its own playbooks, secrets, and docs.cloudfall add ssh-key|server-type|serverdeclares the fleet without hand-writing YAML, creating the Debian 13 baseline type on first use - Backup and restore as first-class operations ✔ —
cloudfall backup run|verifyand the confirm-gatedbackup_service/verify_backupMCP tools execute the declared backup and restore-proof scripts on the service's server as its service user, each writing a schema-validBackupReceipt
Exit: a second real application migrated by someone other than the author, using only the public documentation.
The prerequisite for everything after it: an operator acts on signals, and without alerting there are no signals. Layered on the existing Loki/Grafana/Alloy stack.
- Per-service metrics — exporters for each catalog service (PostgreSQL first) declared in config and audited like any other unit, flowing through the same mTLS gateway as host metrics
- Alert rules from declared config — thresholds and conditions as typed resources, rendered into the stack; an alert fires as evidence, not just a notification
- Notification channels — declared delivery (email/webhook to start), with alert delivery itself audited
Exit: an induced service failure on a proving host raises a declared alert
with evidence within one minute, and cloudfall audit reports alert-rule
compliance alongside everything else. Met on 2026-09-10 on a disposable
Hetzner Cloud Debian 13 server: stopping PostgreSQL raised the declared
alert as pending in 28 seconds and firing at 86 seconds per its declared
for: 1m dampening, the declared webhook received the payload, the
mid-failure audit separated drift (dead listener) from status (firing rule,
compliant), and recovery returned the host to 20/20 compliant — see the
M7 proving-run report. The run
found and fixed one real latency bug (Prometheus evaluation_interval was
stuck at its 1-minute default), which is the point of proving runs.
A persistent agent process that watches the alert stream and drift reports, diagnoses, and proposes — the human confirms through the existing MCP handshake. No autonomy yet: this milestone is the loop, not the license.
- Operator daemon — long-running process subscribed to alerts and scheduled drift checks, maintaining the fleet's observed state
- Diagnose-and-propose — for each incident, an evidence-backed diagnosis and a concrete proposed operation (existing CLI/MCP verbs only), delivered with the receipts that justify it
- Every proposal receipted — proposals, confirmations, executions, and outcomes all land in the same evidence pipeline as manual operations
Exit: an induced failure is detected, diagnosed, and remediated end to end with the human contributing only a confirmation — no human diagnosis, no shell access. Met on 2026-09-10 on a disposable Hetzner Cloud Debian 13 server, for both trigger kinds: a stopped PostgreSQL went from failure to receipted, verified remediation in about 2.5 minutes with one approval command, and induced package drift was detected by the scheduled audit, proposed, approved through the agent confirmation handshake, and verified compliant in 53 seconds — see the M8 proving-run report. The operator watched remotely over the gateway's new read-only mTLS alerts route with no shell access during detection, diagnosis, or verification.
Autonomy is granted per operation class, earned by evidence, never global. The order is fixed by the dependency chain: M7 provides the signals, M8 the loop, M9 the license.
- Level 2: autonomous for provenly reversible operations — failed-deploy rollback, service restart, certificate renewal; executed without asking, receipt delivered after the fact, reversibility backed by the operation class's receipt history
- Level 3: autonomy within declared policy — bounds live in the same typed state as everything else (which operations, which components, quiet hours, rate limits); the operator refuses outside them
- Confirm forever — DNS cutover, data deletion, and database promotion remain behind explicit confirmation regardless of level
Exit: on a proving host, a deliberately broken release is rolled back autonomously within declared policy — alert to healthy service with zero human involvement — and the receipt trail shows detection, decision, action, and verified outcome. Met on 2026-09-10 on a disposable Hetzner Cloud Debian 13 server: the first induced PostgreSQL failure was withheld from autonomy with a recorded reason (insufficient verified history) and needed one human approval — the receipt that earned the trust — and the second identical failure went from stopped service to receipted, verified, policy-licensed autonomous remediation in 2 minutes 6 seconds with zero human involvement; see the M9 proving-run report. An externally killed execution also demonstrated crash consistency: the receipt stayed truthfully open and the next autonomy pass completed it.
Two capabilities exist in code and receipts but have never run against the world: the backup restore drill and alert delivery to a real destination. An unexercised restore path is not a tested one, and an alert that has only ever reached a loopback listener has not proven it can wake anyone. This milestone closes both gaps with the same discipline as every other claim.
- Exercised restore drill ✔ —
cloudfall backup verifyrun on a proving host against a real scheduled dump: scratch-database restore, integrity check, and aBackupReceiptproving the dump is restorable, not merely present - Scheduled drill ✔ — the restore-check gains its own systemd timer
(declared as
backup.restoreCheckOnCalendar, implemented for PostgreSQL and Redis) so restorability is verified continuously and audited like any other unit, not only when someone remembers to ask - Real alert channel ✔ — a declared webhook receiver delivering beyond loopback, with the delivery captured as evidence; the email config path stays covered by template tests only
Exit: on a proving host, a scheduled backup is restore-verified by the
timer-driven drill with a schema-valid BackupReceipt, and an induced
failure produces an alert that arrives at a real external destination —
both trails visible in cloudfall audit alongside everything else.
Met on 2026-09-11 on a disposable Hetzner Cloud Debian 13 pair: the
drill timer's own unit restored the newest dump of a seeded database into a
scratch database (420 catalog relations) with a schema-valid
BackupReceipt, both timers audited as required units in a 21/21-compliant
baseline, and the induced PostgreSQL failure delivered its full firing
payload to a second machine across the public network at T0+113 s — see the
M10 proving-run report. The
run caught and fixed a real bug (the typed inventory silently dropped the
new drill schedule, so no timer was installed until the audit's
required-unit check exposed it), which is the point of proving runs.
The first milestone of the second story, and the product in its smallest form. Nothing here needs the fleet reader: the catalog can be declared against a Cloudfall project first and re-rooted in M16.
- Operations catalog — an
Operationresource that registers an existing playbook with typed inputs, a risk level (read,mutating,destructive), a target scope, preconditions and a verify playbook; the existing engine roles ship as a starter catalog - Risk as MCP annotations — the MCP tool list is generated from the
catalog, with
readOnlyHintanddestructiveHintset from the risk level, so any client gates on it without Cloudfall's help - Audit entry per decision — one schema-validated record per operation run holding the evidence snapshot it was based on, the check-mode diff, the approver (human, policy, or client handshake) and the verify result; the operator's proposal receipts become one kind of audit entry
- Verify as a first-class field — a run is not an outcome until its verify step reports per host; the existing health gate, restore drill and row-count checks are re-expressed as verify steps
cloudfall why— the question "why did the agent do that" answered from the record for any host, operation or time window, in JSON and as a page ✔ (also thewhytool oncloudfall mcp serve --repository)
Exit: an agent drives a declared mutating operation through the MCP server
on a proving host, the client gates it from the annotation alone, the run
is refused until its precondition holds, and afterwards cloudfall why
returns the audit entry with snapshot, diff, approver and verify result
without any human having written a note.
Re-roots M15 on an existing Ansible repository. One config, theirs; no import, no second copy.
- Fleet reader — hosts, groups and merged variables read through
ansible-core in process behind one wrapper module, with subprocess
fallback; the typed inventory model is populated from it instead of from
Serverresources - The
cloudfallblock — Cloudfall's declarations (environment, server type, owned subsystems, components) live under one key in the team's owngroup_varsandhost_vars, validated by the existing schemas; removing Cloudfall means deleting the key - Operations from their playbooks — the M15 catalog registers the
team's playbooks, not only the starter roles;
ownssays which subsystems Cloudfall's roles may touch on a host - Observation — facts, service state, disk and last-run result as one timestamped snapshot per host, referenced by every audit entry
- Greenfield on the same path —
cloudfall initemits a plain Ansible inventory, so both stories share one code path
Exit: on a proving repository with an INI or YAML inventory and playbooks
Cloudfall did not write, cloudfall get host returns the merged variables
and the file that wins, an operation declared over one of their playbooks
runs through the M15 gate and record, and no file outside the cloudfall
key has changed.
The parts that wait for a workload to need them.
- Safe edits —
get,set,unset,add host,remove hostover inventory, vars and playbook files: ruamel round-trip, schema and syntax-check validation before and after the write, diff and gate, restore on failure, audit entry; INI and dynamic sources refused with the file named - Hosted record — the audit log kept off the management host with retention and search across hosts, which is where autonomy-from-history is computed; local receipts stay the free path
- Autonomy from the record —
OperatorPolicymay license an operation class on evidence ("succeeded N times on these hosts") computed from the record, not only on a declared allow-list - Per-tenant policy — policy and approvers scoped per inventory group for teams that run many fleets from one repository
Exit: an agent edits a host variable in a proving repository through
cloudfall set with comments and ordering preserved, the record shows the
edit with its diff and approver, and an operation class is promoted to
unattended by policy on the strength of the record alone.
The fleet goals raise the ambition to 10–100 applications on shared nodes, and the first thing that fails at that density is unenforced sharing: one leaking application takes down its neighbors. This milestone makes fleet requirement 8 real.
- Per-component resource envelopes — memory, CPU, task, and I/O limits declared in config and validated against the node's declared capacity; placement that oversubscribes a node fails validation
- systemd enforcement — the deploy role renders the envelope into the
unit (
MemoryMax,CPUQuota,TasksMax,IOWeight), and the audit proves the running unit carries the declared limits - Capacity accounting — declared reservations against node capacity become fleet headroom evidence in the operations dashboard, so placement decisions are computed from config, not guessed
Exit: on a shared proving host, a deliberately leaking component hits its
declared envelope and is contained — its neighbors' health checks stay
green — and cloudfall audit reports envelope compliance alongside
everything else.
Makes "applications and components are movable" true for stateful applications (fleet requirements 9–11). Moving a component is already a config edit; this milestone makes its data and routing follow.
- Data movement as a verb — moving an application moves its databases (the existing verified dump-and-restore path) and its file storage, with receipts for both
- Routing follows placement — domain routes re-render and reconverge as a consequence of a placement change, never by hand-editing the proxy
- Node drain and decommission — evacuate every component, dataset, and route from a node by config change, prove the fleet healthy without it, then retire it
Exit: on a two-node proving fleet, an application with a database and uploaded files moves between nodes by config edit and convergence, serving byte-identical responses afterward; a drained node is then removed with the fleet audit staying compliant.
The severable single-host half of high availability: near-zero data loss and formation-ready installs, without paying for a standby.
- Continuous archiving —
pgBackRestWAL archiving to object storage, upgrading recovery from last-dump to point-in-time; the M10 restore drill extends to a receipted PITR drill - Availability schema — the
availabilityblock from the availability design lands, with per-kind mode validation; declaring intent becomes possible fleet-wide before any formation exists - Expansion-ready single — formation prerequisites installed dormant
at one node (
wal_level, replication role, TLS-ready binds, fixed identities) and audited, so a future standby is a config edit, not a reinstall
Exit: a PITR drill restores a proving database to a declared point in
time with a schema-valid BackupReceipt, and a fresh single-node
PostgreSQL passes an audit proving every formation prerequisite.
Built when a service's revenue justifies a standby, not before (fleet requirement 6). One primary, one hot standby, one witness. This amends the architecture stance that failover is manual — at the database layer only, and below the operator: the formation may promote automatically, but the operator still never initiates a promotion without explicit confirmation.
- Streaming replication — a declared two-node formation with native WAL streaming over a private network with TLS, replacing the loopback-only binding; synchronous or asynchronous as declared config
- Automatic failover —
pg_auto_failoverwith the monitor on a minimal witness host as the tiebreaker; formation state (primary, secondary, single) audited as evidence like any other unit - Connection routing — PgBouncer on each data node with multi-host
client connection strings (
target_session_attrs=read-write), so applications follow the primary with no extra routing layer - Multi-host alerting exercised — replication lag, formation degradation, and archiving failure become declared alert rules, the forcing function for alerting across more than one host; backups move to the standby
Exit: on a three-host proving formation, killing the primary promotes the
standby and the application recovers with zero human involvement, and the
receipt trail shows detection, promotion, and verified recovery — while
cloudfall audit reports formation compliance across all three hosts.