fly-deployment — The container image is a projection of the repository model, and its smoke is its evidence
One canonical deployment object under .ai/repo/deployments/ describes the image; a typed Rust model parses and validates it; the Dockerfile, its ignore file, the GHCR image that docker-image.yml and release.yml build, its smoke derived from the route registry, the doctor checks, the documentation and the cockpit view are projections of that object and of the capability registry, each checked for drift. The published image runs as a non-root user, carries no secret and no developer path, answers live and ready in its smoke, and has measured size, cold-start and memory baselines. Re-scoped 2026-10-14 from a Fly.io deployment, which the owner disconnected on 2026-10-12.
ACTIVE 5 of 29 issues done
- ready
- 9
- blocked
- 15
- active
- 0
- verify
- 0
- done
- 5
- cancelled
- 9
Problem
Majordomus has no deployment at all: no container, no hosting provider, no way for anyone who is not standing in front of this checkout to see the layer it serves. The obvious repair is the wrong one. A Dockerfile and a fly.toml written by hand would be a second description of facts the repository already holds — the port the server binds, the routes it registers, the assets the site build produces, the binary the workspace names — and every one of those pairs would drift the first time one half changed. The repository already refuses that shape everywhere else: capabilities are declared once and projected into CLI, HTTP, MCP, OpenAPI, docs and the site, and generate --check fails when a projection is stale. A deployment must arrive the same way or not at all.
Outcome
One canonical deployment object under .ai/repo/deployments/ describes the image; a typed Rust model parses and validates it; the Dockerfile, its ignore file, the GHCR image that docker-image.yml and release.yml build, its smoke derived from the route registry, the doctor checks, the documentation and the cockpit view are projections of that object and of the capability registry, each checked for drift. The published image runs as a non-root user, carries no secret and no developer path, answers live and ready in its smoke, and has measured size, cold-start and memory baselines. Re-scoped 2026-10-14 from a Fly.io deployment, which the owner disconnected on 2026-10-12.
Current state
Measured 2026-10-14 on master e28daccd9b: .ai/repo/deployments/majordomus.yaml is the canonical object; deploy/Dockerfile and its ignore file are generated from it (Alpine, uid 10001, exec-form entrypoint); docker-image.yml builds and smoke-tests the image on pull requests and on master, and release.yml publishes it to GHCR; .github/actions/image-smoke checks the image runs as non-root and answers /api/v1/live and /api/v1/ready; deploy.list, deploy.get, deploy.check and deploy.observed are capabilities. Missing: a smoke derived from the route registry, a secret and developer-path scan of the image, an SBOM, size, cold-start and memory baselines, a documentation page. The object still carries region, machines and resources, which nothing reads since Fly.io was disconnected on 2026-10-12.
Desired state
The GHCR image is a projection of the canonical object like every other artifact: generated, checked for drift, smoke-tested from the route registry, scanned for secrets and developer paths, measured against recorded budgets, documented and shown in the cockpit. Whether region, machines and resources leave the deployment/v1 schema is decided and recorded.
Scope
- .ai/repo/deployments
- .ai/repo/adrs
- .ai/repo/rules/project
- .ai/repo/policy.yaml
- apps/majordomus-cli/src/deploy
- apps/majordomus-cli/src/capability/builtin
- apps/majordomus-cli/src/http
- deploy
- share/kinds.yaml
- share/allow
- share/schemas
- docs
- site
- test/cases
- .github/workflows
Out of scope
- Any dependency, in code, script, documentation, CI or default, on ~/dev/prismatic-platform
- A provider abstraction covering hosts other than the one this repository deploys to
- Kubernetes, Nomad, buildpacks, Nixpacks or a second container implementation
- A persistent volume, a database or any stateful service, unless a measured requirement forces one
- Mutating deployment operations exposed to unauthenticated HTTP or to the browser
- Profile-guided optimisation
Acceptance criteria
- One canonical deployment object is the only place the port, the region, the resources, the Machine count, the health routes, the build inputs and the budgets are stated
- Every image artifact — the Dockerfile and its ignore file — is generated, marked as generated with its source and its regeneration command, and fails generate --check when edited by hand
- The deployment reads are declared once in the capability registry and appear in the CLI, the HTTP API, MCP, OpenAPI and the generated documentation without a second registration
- The smoke suite is derived from the route registry; no second list of endpoints exists anywhere
- The production image runs as a non-root user, contains no build toolchain, no repository history, no secret and no absolute developer path
- The published image answers live and ready in its smoke, run from outside the container on a free host port
- Image size, binary size, build context size, cold start, resident memory and endpoint latency are measured, recorded as baselines and gated against unexplained regression
- No credential is written to the repository, to a Docker layer, to a generated file, to the site or to a log, and the image is published with the workflow's own token
- Every rule this milestone states is enforced by a check that CI executes
Validation
- bash test/run.sh
- bin/majordomus doctor
- cargo test --workspace --all-features
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- majordomus generate --check
Evidence required
- canonical_object
- typed_model
- generated_projections
- registry_derived_smoke
- container_minimal
- image_smoke
- budgets_measured
- secrets_isolated
- rules_enforced
Risks
- Generation can hide the Dockerfile from the people who must read it. The generated file has to stay idiomatic and reviewable; the source describes intent, the output describes mechanics, and a contributor who edits the output is told which file to change instead.
- The route topology and the served surfaces are being rewritten on other branches under ADR 0013. A smoke suite pinned to today's paths would be stale on arrival, so it must read the route registry rather than a path list.
- 256 MB is a hypothesis. It is held only as long as a measured run under that limit passes; the evidence, not the preference, decides the final number.
- A deployment touches money. The first deploy must not leave two Machines running, and the Machine inventory is asserted rather than assumed.
- The bootstrap credential comes from outside this repository. It may be used to obtain an authenticated session and nowhere else; anything else would make Majordomus depend on Prismatic Platform, which is forbidden.
Issues
Each one is an execution contract with its own acceptance criteria, validation command and required evidence. The wave column is the layer of the dependency graph it sits in.
| issue | status | wave | depends on | title |
|---|---|---|---|---|
| I0901 | DONE | 0 | — | The deployment description is a kind of the layer, not a provider file |
| I0902 | DONE | 1 | I0901 | The deployment object becomes typed Rust that refuses nonsense |
| I0903 | DONE | 2 | I0902 | Deployment reads are declared once and projected everywhere |
| I0904 | DONE | 2 | I0902 | The listen address is one fact with two projections |
| I0905 | DONE | 2 | I0902 | Liveness and readiness are registered capabilities, not router hacks |
| I0906 | READY | 3 | I0904 | Termination is graceful and proved against the real container |
| I0907 | READY | 2 | I0902 | The running process says what it is, from an allowlist |
| I0908 | READY | 2 | I0902 | A hosted process has no repository, and says so instead of failing |
| I0909 | READY | 3 | I0902, I0904, I0905 | The production image is a generated projection of the build specification |
| I0910 | BLOCKED | 4 | I0909 | The release profile is chosen by measurement, not by folklore |
| I0911 | BLOCKED | 4 | I0909 | A source change does not rebuild the world, and the context stays small |
| I0912 | BLOCKED | 4 | I0909 | The image serves the site the canonical pipeline produced |
| I0913 | CANCELLED | 3 | I0902, I0903 | The provider configuration is generated, and the cheap profile is declared |
| I0914 | CANCELLED | 4 | I0903, I0909, I0913 | One deployment core, one command, and a dry run that mutates nothing |
| I0915 | CANCELLED | 5 | I0914 | Deployment joins the diagnostics that already exist |
| I0916 | BLOCKED | 5 | I0905, I0914 | The smoke suite is derived from the route registry, never written twice |
| I0917 | CANCELLED | 5 | I0914 | A bad deployment can be undone deterministically |
| I0918 | CANCELLED | 2 | I0902 | The bootstrap credential is used once and belongs to nothing |
| I0919 | BLOCKED | 4 | I0909 | Nothing secret and nothing local leaves in the image |
| I0920 | BLOCKED | 4 | I0909 | The image's dependencies are known and its advisories are policy |
| I0921 | BLOCKED | 5 | I0910, I0911 | Size is measured first and budgeted second |
| I0922 | CANCELLED | 6 | I0930 | The memory hypothesis is tested, not asserted |
| I0923 | BLOCKED | 6 | I0930 | Cold start and endpoint cost are benchmark targets like everything else |
| I0924 | CANCELLED | 1 | I0901 | The decision is recorded where architecture decisions are recorded |
| I0925 | BLOCKED | 6 | I0915 | Every deployment rule is a check that runs |
| I0926 | BLOCKED | 4 | I0903, I0913, I0924 | The deployment documentation is generated and reachable |
| I0927 | READY | 3 | I0903 | The cockpit shows the deployment from the same data |
| I0928 | BLOCKED | 6 | I0919, I0921, I0930 | A pull request cannot merge a deployment that would not work |
| I0929 | CANCELLED | 8 | I0918, I0931 | Production deploys from CI with a token scoped to one application |
| I0930 | BLOCKED | 5 | I0906, I0909, I0912 | The real image runs under the real limits before anything is deployed |
| I0931 | CANCELLED | 7 | I0913, I0914, I0916, I0917, I0918, I0922, I0930 | The application exists, one machine runs it, and the URL answers |
| I0932 | BLOCKED | 9 | I0920, I0923, I0925, I0926, I0927, I0928, I0929, I0931 | The milestone is accepted on its evidence |
| I3244 | READY | 0 | — | The generated Dockerfile copies every path crate cargo metadata names, so a new workspace crate needs no edit of the image |
| I3245 | READY | 0 | — | The image build's cold and warm durations and its context size are read from docker-image runs, and the deployment records say what is built today |
| I3315 | READY | 0 | — | The image smoke completes an MCP initialize on /mcp and a WebSocket upgrade on /events, not only a ready probe |
| I3820 | READY | 0 | — | A deployment names the provider that hosts it, and the provider's files are generated from it |
| I3821 | BLOCKED | 1 | I3820 | The tree a provider receives is assembled from the deployment object, and master publishes it after green ci |
| I3822 | BLOCKED | 2 | I3821 | A deployment that names a provider is observed at its own URL, and is active only while it serves master |
The graph
flowchart LR
I0901["I0901<br/>The deployment description is a kind of the layer, not a provider file"]:::done
I0902["I0902<br/>The deployment object becomes typed Rust that refuses nonsense"]:::done
I0903["I0903<br/>Deployment reads are declared once and projected everywhere"]:::done
I0904["I0904<br/>The listen address is one fact with two projections"]:::done
I0905["I0905<br/>Liveness and readiness are registered capabilities, not router hacks"]:::done
I0906["I0906<br/>Termination is graceful and proved against the real container"]:::ready
I0907["I0907<br/>The running process says what it is, from an allowlist"]:::ready
I0908["I0908<br/>A hosted process has no repository, and says so instead of failing"]:::ready
I0909["I0909<br/>The production image is a generated projection of the build specification"]:::ready
I0910["I0910<br/>The release profile is chosen by measurement, not by folklore"]:::blocked
I0911["I0911<br/>A source change does not rebuild the world, and the context stays small"]:::blocked
I0912["I0912<br/>The image serves the site the canonical pipeline produced"]:::blocked
I0913["I0913<br/>The provider configuration is generated, and the cheap profile is declared"]:::cancelled
I0914["I0914<br/>One deployment core, one command, and a dry run that mutates nothing"]:::cancelled
I0915["I0915<br/>Deployment joins the diagnostics that already exist"]:::cancelled
I0916["I0916<br/>The smoke suite is derived from the route registry, never written twice"]:::blocked
I0917["I0917<br/>A bad deployment can be undone deterministically"]:::cancelled
I0918["I0918<br/>The bootstrap credential is used once and belongs to nothing"]:::cancelled
I0919["I0919<br/>Nothing secret and nothing local leaves in the image"]:::blocked
I0920["I0920<br/>The image's dependencies are known and its advisories are policy"]:::blocked
I0921["I0921<br/>Size is measured first and budgeted second"]:::blocked
I0922["I0922<br/>The memory hypothesis is tested, not asserted"]:::cancelled
I0923["I0923<br/>Cold start and endpoint cost are benchmark targets like everything else"]:::blocked
I0924["I0924<br/>The decision is recorded where architecture decisions are recorded"]:::cancelled
I0925["I0925<br/>Every deployment rule is a check that runs"]:::blocked
I0926["I0926<br/>The deployment documentation is generated and reachable"]:::blocked
I0927["I0927<br/>The cockpit shows the deployment from the same data"]:::ready
I0928["I0928<br/>A pull request cannot merge a deployment that would not work"]:::blocked
I0929["I0929<br/>Production deploys from CI with a token scoped to one application"]:::cancelled
I0930["I0930<br/>The real image runs under the real limits before anything is deployed"]:::blocked
I0931["I0931<br/>The application exists, one machine runs it, and the URL answers"]:::cancelled
I0932["I0932<br/>The milestone is accepted on its evidence"]:::blocked
I3244["I3244<br/>The generated Dockerfile copies every path crate cargo metadata names, so a new workspace crate needs no edit of the image"]:::ready
I3245["I3245<br/>The image build's cold and warm durations and its context size are read from docker-image runs, and the deployment records say what is built today"]:::ready
I3315["I3315<br/>The image smoke completes an MCP initialize on /mcp and a WebSocket upgrade on /events, not only a ready probe"]:::ready
I3820["I3820<br/>A deployment names the provider that hosts it, and the provider's files are generated from it"]:::ready
I3821["I3821<br/>The tree a provider receives is assembled from the deployment object, and master publishes it after green ci"]:::blocked
I3822["I3822<br/>A deployment that names a provider is observed at its own URL, and is active only while it serves master"]:::blocked
I0901 --> I0902
I0901 --> I0924
I0902 --> I0903
I0902 --> I0904
I0902 --> I0905
I0902 --> I0907
I0902 --> I0908
I0902 --> I0909
I0902 --> I0913
I0902 --> I0918
I0903 --> I0913
I0903 --> I0914
I0903 --> I0926
I0903 --> I0927
I0904 --> I0906
I0904 --> I0909
I0905 --> I0909
I0905 --> I0916
I0906 --> I0930
I0909 --> I0910
I0909 --> I0911
I0909 --> I0912
I0909 --> I0914
I0909 --> I0919
I0909 --> I0920
I0909 --> I0930
I0910 --> I0921
I0911 --> I0921
I0912 --> I0930
I0913 --> I0914
I0913 --> I0926
I0913 --> I0931
I0914 --> I0915
I0914 --> I0916
I0914 --> I0917
I0914 --> I0931
I0915 --> I0925
I0916 --> I0931
I0917 --> I0931
I0918 --> I0929
I0918 --> I0931
I0919 --> I0928
I0920 --> I0932
I0921 --> I0928
I0922 --> I0931
I0923 --> I0932
I0924 --> I0926
I0925 --> I0932
I0926 --> I0932
I0927 --> I0932
I0928 --> I0932
I0929 --> I0932
I0930 --> I0922
I0930 --> I0923
I0930 --> I0928
I0930 --> I0931
I0931 --> I0929
I0931 --> I0932
I3820 --> I3821
I3821 --> I3822
classDef done stroke:#16a34a,fill:#dcfce7,color:#14532d,stroke-width:2px
classDef active stroke:#2563eb,fill:#eff6ff,color:#1e3a8a,stroke-width:2px
classDef verify stroke:#7c3aed,fill:#f5f3ff,color:#4c1d95,stroke-width:2px
classDef ready stroke:#0891b2,fill:#ecfeff,color:#164e63,stroke-width:2px
classDef planned stroke:#0891b2,fill:#ecfeff,color:#164e63,stroke-width:2px
classDef blocked stroke:#b45309,fill:#fffbeb,color:#78350f,stroke-width:2px
classDef cancelled stroke:#6b7280,fill:#f9fafb,color:#374151,stroke-width:2px
classDef superseded stroke:#6b7280,fill:#f9fafb,color:#374151,stroke-width:2pxLinked from this diagram (38)
- I0901 The deployment description is a kind of the layer, not a provider file
- I0902 The deployment object becomes typed Rust that refuses nonsense
- I0903 Deployment reads are declared once and projected everywhere
- I0904 The listen address is one fact with two projections
- I0905 Liveness and readiness are registered capabilities, not router hacks
- I0906 Termination is graceful and proved against the real container
- I0907 The running process says what it is, from an allowlist
- I0908 A hosted process has no repository, and says so instead of failing
- I0909 The production image is a generated projection of the build specification
- I0910 The release profile is chosen by measurement, not by folklore
- I0911 A source change does not rebuild the world, and the context stays small
- I0912 The image serves the site the canonical pipeline produced
- I0913 The provider configuration is generated, and the cheap profile is declared
- I0914 One deployment core, one command, and a dry run that mutates nothing
- I0915 Deployment joins the diagnostics that already exist
- I0916 The smoke suite is derived from the route registry, never written twice
- I0917 A bad deployment can be undone deterministically
- I0918 The bootstrap credential is used once and belongs to nothing
- I0919 Nothing secret and nothing local leaves in the image
- I0920 The image's dependencies are known and its advisories are policy
- I0921 Size is measured first and budgeted second
- I0922 The memory hypothesis is tested, not asserted
- I0923 Cold start and endpoint cost are benchmark targets like everything else
- I0924 The decision is recorded where architecture decisions are recorded
- I0925 Every deployment rule is a check that runs
- I0926 The deployment documentation is generated and reachable
- I0927 The cockpit shows the deployment from the same data
- I0928 A pull request cannot merge a deployment that would not work
- I0929 Production deploys from CI with a token scoped to one application
- I0930 The real image runs under the real limits before anything is deployed
- I0931 The application exists, one machine runs it, and the URL answers
- I0932 The milestone is accepted on its evidence
- I3244 The generated Dockerfile copies every path crate cargo metadata names, so a new workspace crate needs no edit of the image
- I3245 The image build's cold and warm durations and its context size are read from docker-image runs, and the deployment records say what is built today
- I3315 The image smoke completes an MCP initialize on /mcp and a WebSocket upgrade on /events, not only a ready probe
- I3820 A deployment names the provider that hosts it, and the provider's files are generated from it
- I3821 The tree a provider receives is assembled from the deployment object, and master publishes it after green ci
- I3822 A deployment that names a provider is observed at its own URL, and is active only while it serves master
Canonical record: .ai/repo/project/milestones/fly-deployment.yaml. Read it back with majordomus plan show fly-deployment.