The simplest way of putting it that is still true.
Tillandsias, explained like you are five
A real tillandsia needs no soil and no pot — it drinks from the air, and borrows nothing.
A tiny cloud on your own table
When you open a website, you borrow a computer in a huge building far away — the cloud. Tillandsias builds a tiny cloud inside your own computer, so you can work without sending your things to its makers. A service you choose to use can still receive what you send it.24
It is a doll house. Your computer is the table. Tillandsias puts a clean pretend-computer on it, and inside that, little rooms, one program each.
Anyone may have it, read it and change it, for free.14
Nobody goes outside except the doorman
The rooms have no doors outside. They share one private hallway, and exactly one room — the doorman — may step out to the internet. Everyone else asks it.2
Robot helpers may only play inside the doll house.11 If one makes a mess in its room, the room can be thrown away — but anything it sends out of the doll house is real, so look at what it sends.26
Broken things get thrown away, not glued
If a room breaks, nobody glues it. It goes in the bin and Tillandsias builds a fresh one from the same recipe. That is on purpose: gluing one layer tends to break the layer underneath next.3
Your own project folder lives outside the doll house, and sweeping a room never touches it.12
Getting work out of the doll house has its own rule. The doll house has a post box, and it does not say done until the shelf far away really has your parcel. If it cannot reach the shelf, it says so, instead of saying done and quietly keeping the parcel in a drawer.26
Getting better, honestly
Every time the makers fix something, a machine checks that nothing that used to work got broken.23
Less broken every time does not mean perfect one day. It only means creeping toward some resting place, which may not be zero. They say so themselves.4
Things that are still broken
what happens next Making the folder private before the key goes in is the written next job. It is not done yet.18
How we know — 14 sources
Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.
- 2Only the doorman may step outside openspec/specs/enclave-network/spec.md#L10-L12
Only the proxy is dual-homed for external access; all other members communicate exclusively through the enclave.
- 3Throw away and rebuild, never patch by hand, and why methodology/philosophy.yaml#L84-L99
Therefore the DEFAULT response to a borked host/VM/guest/podman/stack layer is to DESTROY and RECREATE it, never to hand-patch it forward.
- 4Getting better every time means nothing gets worse and what is left shrinks, not that it ends up perfect methodology/math-foundations.yaml#L108-L120
Without that metric proof, "monotonic convergence" means ordered non-regression plus finite residual descent, not metric contraction.
- 11Robot helpers run only inside the doll house openspec/specs/forge-as-only-runtime/spec.md#L18-L19
Every coding agent, maintenance shell, and runtime utility executes inside the project's forge container; there is no host-side execution surface.
- 12Sweeping a room never touches your project folder openspec/specs/app-lifecycle/spec.md#L79-L81
the container is removed, project-specific cache data is deleted, but the project source directory in `~/src` is never touched
- 14The licence: anyone may have it, read it and change it LICENSE#L1-L16
the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program
- 15The key is locked so only its owner can read it crates/tillandsias-headless/src/main.rs#L3400-L3400
Clamp the CA private key to owner-only access (0600) — 755-qcxh.
- 17The key's folder sits inside the program's own folder crates/tillandsias-core/src/ca_path.rs#L36-L50
format!("{}/ca", ca_template())
- 18The written next job: make the folder private before any key is written plan/index.yaml#L26013-L26013
deliverable: CA material is created in a directory that is private by construction (XDG_RUNTIME_DIR, or DirBuilder::mode(0o700) before any key is written), and scripts/clamp-ca-material.sh gains a retirement condition or is deleted
- 20The program's own folder is inside your home folder images/default/ca-path.txt#L83-L83
${HOME}/.local/state/tillandsias
- 23After every fix, a test that used to pass may not start failing build.sh#L2454-L2456
a failure not named in scripts/test-known-red.txt is a new regression, and a listed test that PASSED is a stale entry that must be deleted
- 24A service you choose receives what you send it PRIVACY.md#L60-L63
If you instead configure a remote provider, the content you send is transmitted to that provider under their terms.
- 25The key's folder is made the ordinary way, with no private setting crates/tillandsias-headless/src/main.rs#L3468-L3468
std::fs::create_dir_all(&certs_dir)
- 26The post box says done only once the faraway copy really has the work images/git/pre-receive-hook.sh#L6-L8
Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.
Straight answers to what you are actually wondering: privacy, cost, and what breaks.Picks up where “like I’m 5” left off.
Tillandsias, explained simply
A tillandsia is an epiphyte, not a parasite: it rests on its tree and takes nothing from it.
What it is for, in one paragraph
The reason a grown-up wants one is narrower than "the cloud, but at home". Software you did not write — increasingly, AI assistants that write and run code for you — has to work somewhere. The choice is between your actual machine and a sealed room containing only the project folder you deliberately opened.1 Tillandsias is that room, plus the plumbing to prepare it and start your tools.
Is my stuff private, and who could see it?
The people who make Tillandsias operate no servers your copy talks to: no account, no sign-up, no usage tracking, no crash reporting, no identifier attached to you or your machine.2 Your logs stay on your own disk where you can read them, and leave only if you send them somewhere yourself. Any login you hand over goes into a secret store on your own machine — the program's own vault, whose key sits in your operating system's keychain, or on some computers in a local file — never on anyone else's server.316
This does not depend on trusting anyone's intentions: there is no server of theirs to send anything to.2 It covers their side only. A service you sign into yourself still sees what you tell it.18
Does it cost money? Does it need the internet?
It costs nothing: free software under a licence that lets anyone read, run, modify and pass it on.4 You have already paid for the only hardware involved.
The internet is needed only for ordinary, visible reasons: fetching the program, fetching updates, reaching a service you chose.17 Local work can continue without sending it to an AI provider: language models can run on your own machine.18 That does not make every action offline-ready. Fetching a missing tool or model needs a connection, and publishing your work to GitHub fails when GitHub cannot be reached.31
What happens when I turn it off? Can it break my computer?
Turning it off is the expected motion, not an interruption. Anything you saved is a real file on your real disk, untouched by the rebuild. Even the internal passwords the pieces use to talk to each other are replaced at every start, so yesterday's messy shutdown cannot jam today's start.5
Installing a new release is a bigger change than turning it off: the Linux, Mac and Windows installers all reset the program's own working state and set it up again.363738 On Linux this reset is gentle: it rebuilds the program's parts but deletes none of your stored data, so your downloads survive, and so do your sign-ins as long as your keychain can still unlock them.39 Windows runs the same gentle reset without telling you first.3840 The Linux and Mac installers tell you before they reset; to skip the part that deletes things, set TILLANDSIAS_DESTRUCTIVE_RESET_OK=0 before installing.37
Publishing your work follows the same idea. Inside the sealed room you push to a copy that lives on your own machine, not to the internet. That copy passes your work on and waits: it reports success only once the copy outside has really accepted the same changes, so a success you can see is one that survives the room being thrown away.32 It is also the only part that holds your GitHub login — it uses it when it publishes, and never hands it to the room where your tools run.33
As for damage: a misbehaving tool inside the sealed room sees only the folder you gave it.1 Wiping the installation and rebuilding is documented and supported, not a last resort.6 The cost you do pay is ordinary and reversible. It is a real virtual machine, so it holds real memory while running and gives it back when it stops. The system image it downloaded stays on your disk so the next start is quick; uninstalling clears it, though on a Mac you have to ask for that explicitly, and the uninstaller makes you type a confirmation first.203043
what happens next A fix is being written. It is not in any release yet.
Will an update break what already works?
Each change to Tillandsias has to pass its tests, and a machine refuses a change that makes a test fail that was not already known to fail.23
Two honest limits come with that. Passing tests are evidence, not proof: they show no problem was found, not that none exists.8 And "never worse" means settling toward some floor, not that the floor is zero problems.9
Where it falls short today
known limitation On a Mac, the app has not yet passed Apple's inspection, so a copy downloaded with a web browser is blocked the first time you open it. The recommended one-line install avoids that only because it does not go through a browser.10
what happens next The signing service has been chosen but is not set up yet.13
None of these touch the privacy answer above.
what happens next It is recorded as an open problem in this release.35
How we know — 34 sources
Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.
- 1What a workspace can reach, and why that isolation exists to protect you from the tools PRIVACY.md#L32-L40
Each workspace sees the project you opened, not your whole filesystem. That isolation exists to protect you from the tools, not to hide anything from you.
- 2"Tillandsias collects nothing" — no account, no telemetry, no analytics, no server PRIVACY.md#L5-L17
**Tillandsias collects nothing.** There is no account to create, no telemetry, no analytics, and no server operated by us that your installation talks to.
- 3Credentials in a local secret store on your machine; logs kept on your own disk PRIVACY.md#L41-L44
**Credentials you provide** (for example, a GitHub login you initiate) are stored in a local secret store on your machine.
- 4GNU General Public License, version 3 LICENSE#L1-L2
GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
- 5Internal passwords are removed and reissued at every start, so an unclean shutdown leaves nothing stale openspec/specs/ephemeral-secret-refresh/spec.md#L10-L25
The system SHALL check for existing podman secrets before creation. If a secret exists from a prior unclean shutdown, it SHALL be removed and recreated with fresh content.
- 6"Designed to be wiped and rebuilt freely" PRIVACY.md#L46-L47
You can remove all of it at any time by resetting or uninstalling; the application is designed to be wiped and rebuilt freely.
- 8Passing tests are evidence, not proof that the program is correct methodology/philosophy.yaml#L249-L252
Traceability, version matching, litmus success, and CRDT metadata convergence are evidence. They are not proof of semantic correctness by themselves.
- 9The project does not claim it will reach zero problems methodology/math-foundations.yaml#L107-L120
The methodology does not currently claim Banach-style contraction. It can report decreasing residuals, but it has not proven a contraction constant over a complete metric space of project states.
- 10The install instructions: a Mac download through a browser is blocked on first launch README.md#L27-L45
Tillandsias is signed but not yet notarized (Apple Developer enrollment is pending), and macOS tags anything a *browser* downloads with `com.apple.quarantine` — so the .dmg route hits a Gatekeeper block on first launch, while `curl` does not tag at all and the app opens normally.
- 11Apple signing and inspection options, and which steps need a paid membership plan/issues/macos-gatekeeper-signing-options-2026-08-29.md#L39-L48
| Developer ID Application certificate | **Yes** — no free-tier equivalent.
- 12An unsigned Windows installer package of this kind cannot be installed plan/issues/store-msix-submission-blockers-2026-08-31.md#L26-L40
**Consequence for the pending signing decision.** The unsigned MSIX (`0x800B0100`, packet 722-w7a2) blocks the **GitHub-release** channel only. It does not block the Store channel at all.
- 13The signing service chosen for the Windows downloads plan/issues/windows-signing-research-2026-08-16.md#L1-L25
**SignPath Foundation is the signing path for the GitHub-release channel.** Packet 722-w7a2 is reshaped, not closed: its deliverable changes from "an Azure Trusted Signing account" to the SignPath Foundation chain, with Azure **Artifact Signing** as the recorded fallback.
- 16Where no keychain is available, the vault's key falls back to a local file crates/tillandsias-headless/src/vault_bootstrap.rs#L2943-L2943
Fallback: file (populated by keychain_set_blocking when keyring unavailable,
- 17Software sources — package repositories and release downloads, reached at your direction PRIVACY.md#L51-L56
**Software sources** — package repositories and release downloads (for example GitHub, Linux distribution mirrors, and language package registries) to fetch the software it runs.
- 18AI providers only if you configure one; language models can run entirely on your own machine PRIVACY.md#L60-L63
**AI providers, only if you configure one.** Tillandsias can run language models entirely on your own machine. If you instead configure a remote provider, the content you send is transmitted to that provider under their terms.
- 20The downloaded system image is cached on the host between runs openspec/specs/vm-provisioning-lifecycle/spec.md#L48-L51
cached at `~/.local/share/tillandsias/rootfs-fedora-44-<sha256>.tar.xz` (on macOS: `~/Library/Application Support/tillandsias/rootfs-…`; on Windows: `%LOCALAPPDATA%\tillandsias\rootfs-…`).
- 23The test verdict: a failure not already on the known-failing list is refused as new build.sh#L2451-L2451
THE VERDICT IS A RATCHET, NOT CARGO'S EXIT CODE.
- 24The Mac build inspects and seals the app when given Apple's credentials scripts/build-macos-tray.sh#L267-L290
say "notarize: submitting (this waits for Apple's verdict)"
- 26That installer package is left out of a release when it is unsigned .github/workflows/release.yml#L751-L751
::warning::withholding unsigned MSIX from release assets: $($_.Name) (uninstallable without a signature; set TILLANDSIAS_SIGNING_ACCOUNT to publish it)
- 27The other Windows downloads are published unsigned .github/workflows/release.yml#L740-L740
::warning::TILLANDSIAS_SIGNING_ACCOUNT is unset — publishing UNSIGNED Windows artifacts (plan packet 722-w7a2)
- 30Uninstalling removes the cached image, except on macOS where it is preserved unless you ask for a full wipe scripts/uninstall.sh#L228-L228
Preserving the VM image in $DATA_DIR (use --wipe to remove it).
- 31A publish that cannot reach GitHub fails instead of half-succeeding openspec/specs/git-mirror-service/spec.md#L225-L230
the forge's `git push` SHALL return non-zero
- 32Success means the faraway copy really accepted the same changes images/git/pre-receive-hook.sh#L6-L8
Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.
- 33The GitHub login is read by the publishing service and never enters a workspace openspec/specs/git-mirror-service/spec.md#L15-L16
The git service reads the GitHub token from Vault at push time via Vault CLI; the token never crosses into a forge container.
- 34The version shown is the one written into the program when it was built crates/tillandsias-headless/src/main.rs#L161-L161
pub(crate) const VERSION: &str = include_str!("../../../VERSION");
- 35The open problem: the Linux program records no build commit plan/index.d/20260914t185710z-1188-mm9y-installed-launcher-has-no-provenance-lenovinha.yaml#L27-L43
the Linux launcher has no build.rs reading it.
- 36The Linux installer runs the reset scripts/install.sh#L340-L340
"$INSTALL_PATH" --reset-state --debug
- 37The Mac installer runs the reset, and the setting that skips its deleting part scripts/install-macos.sh#L290-L290
TILLANDSIAS_DESTRUCTIVE_RESET_OK=0 skips the destruction
- 38The Windows installer runs the reset without asking scripts/install-windows.ps1#L968-L968
--reset-state < NUL
- 39The Linux reset rebuilds the program's parts and deletes no stored data crates/tillandsias-headless/src/main.rs#L10474-L10490
A SOFT reset destroys DERIVED state only and deletes NO store under any
- 40The Windows installer's rule: the gentle reset, with no prompt scripts/install-windows.ps1#L951-L952
So SOFT reset is the default only and forever.
- 41The Mac reset deletes the saved vault keys and the vault data crates/tillandsias-macos-tray/src/reset_state.rs#L286-L292
remove_path(&cache_root.join("vault-data"))?;
- 42The project's rule: every platform's reset must keep your data openspec/specs/host-state-lifecycle/spec.md#L180-L182
`--reset-state` on every platform SHALL be a SOFT reset: it destroys every item of derived state and SHALL NOT remove, move or rewrite any item of operator data.
- 43The uninstaller asks for a typed confirmation and refuses without a terminal scripts/uninstall.sh#L184-L185
Type \"delete\" to uninstall, or press Enter to cancel:
The anatomy: what runs where, what survives a teardown, and where the sharp edges are.Assumes the two levels before it.
Tillandsias, for a power user
Its roots only grip; the leaves do the drinking — a plant that runs rootless.
What you get the moment a forge opens
tillandsias --headless /path/to/project --claude starts a container with your project in it and an agent at the prompt. Nothing below is configured by you or by the agent: the context file the agent reads first lists the plumbing under the heading "all transparent — zero configuration needed".16 Everything on this page describes the stable release, v56.10.9.1 — the one the install commands give you.
The forge container
A forge is a Fedora Minimal image in two layers: a heavy base carrying the toolchains — gcc, make, cmake, Rust with cargo and rust-analyzer, Go, Node, Java, Python, the debuggers — and a thin runtime layer with the entrypoints, cheatsheets and the agent's configuration.1718 The tag is a content hash of its sources, so an unchanged rebuild is a no-op.19 The coding agents are not baked in: every launch refreshes them into a per-project cache volume that outlives the container, rolling back to the last known-good version when upstream ships a broken one.2021 Cheatsheets, /tmp and the runtime directory sit on RAM-backed tmpfs with hard caps.22 The default clone-only lane now puts the source tree in RAM too — a tmpfs-backed volume the launcher creates for the lane, sized from the mirror’s pack; the opt-in host-mounted checkout keeps its host mount.30149 Allowlisted commands — tmux and lazygit among them — are shims that install userspace Homebrew on first use.23 On start the forge writes a context file into the checkout naming what is present, what is absent, and what needs no configuring.24
checked Works out of the box. The tool cache is a named volume, never a host path, so first-run installs survive the container's removal without opening a host-home surface.21
known limitation The storage spec names four RAM-backed roots and a host RAM gate with a matching container memory ceiling; the default lane now mounts all four roots, but the host RAM gate and the matching container memory ceiling remain library code nothing calls.2526 The Homebrew prefix is backed by no volume, so every on-demand tool is re-installed at every launch and its bootstrap needs live egress each time.27 And the context file tells the agent each such install verifies a signed attestation per bottle; the shim states that attestation is off, because fetching it would need a GitHub credential no forge may hold — what remains is integrity from one publisher.2829
what happens next The RAM gate and memory ceiling are a planned fix that has not been started: run the host RAM check before launch and set matching memory limits.26 The recorded remedy for the prefix asks for a decision between persisting it and baking the tools into the image.27 For the context file's wording there is a recorded remedy too: fetch and pin the attestation bundles when the image is built, so a lane can verify one with no GitHub credential present at all.145
The HTTPS proxy and its cache
Every container is born with its proxy variables set and the proxy's certificate composed into the system trust bundle before any network client starts; no per-client CA variable is set, and a regression test refuses a launcher that tries.34353637 The allowlist of developer hosts — registries, GitHub, cloud SDKs, model providers — is generous enough that npm, cargo, pip and flutter work unconfigured.38 It decrypts exactly one of them, GitHub's release-asset CDN, so a large asset is fetched once for the whole machine.3940 Everything else passes through encrypted, so the proxy never sees, and never caches, crates.io, npm, PyPI or api.github.com.41 What makes the second cargo build fast is not the proxy but a per-project cache volume for cargo, npm, pip and go that outlives the forge.42 A blocked host gets no 403: the connection is reset, and only the proxy log names the domain.43
checked The trust wiring is pinned by a test: the forge launcher mounts the single runtime CA input read-only and sets none of the per-client CA variables.37
known limitation The cache-hit claim for the one decrypted host has never been shown live. The fix for a rule order that made decryption unreachable shipped the config and static tests, and records that no real miss-then-hit trace was obtained.44
what happens next The next step is written down: on a Podman host, request one release-asset URL twice through the strict proxy and record a miss followed by a hit with equal checksums.44
Local inference
The launcher prepares a shared model server at http://inference:11434, started on launch if it is not already running and shared across projects.4546146 The first run pulls the engine and one small tool-capable chat model into a host directory that outlives the containers.47489 Readiness is best-effort: a failed readiness wait warns and the forge launches anyway; errors while preparing the service can still fail launch.46 The context file tells the agent, in a closed vocabulary, what the host's accelerators are and whether the endpoint is ready — ready means at least one model is cached, never "starting up".4950 Larger tiers are opt-in behind an environment variable; the CPU floor is unconditional by spec.5152
known limitation Two limits remain. GPUs reach the container only with a vendor stack — NVIDIA with a CDI spec, AMD when ROCm reports the card — and NPUs are named on every platform and used on none: no device argument for one exists in the launcher, and the spec itself says the engine cannot use them.54555657 The container's health check tests that the runner binary exists, not that a model loads.58
what happens next For the health check, no fix is planned yet. The entrypoint does run the real load check once at startup.58
The git mirror and push relay
For an agent, a refinement is a controlled data path: it receives selected source, rules, task context and tool results; it proposes a patch; local checks and review decide whether that patch becomes a commit. Git retains reachable commits, while the current tree is a snapshot that may replace or delete files. Calling the repository a "set of vectors" is therefore a useful description of the model's selected context, not Git's storage format and not a guarantee that every current file survives.616266
Several tasks can run this loop at once. Each needs its own declared target and measurement; the shared repository is the coordination point, not evidence that their measurements are already comparable. The relay makes an accepted push atomic,6263 which preserves the record of what was accepted, not the truth of the patch.
Why this is built rather than borrowed. The mirror has to satisfy two requirements at the same time. The credential for the upstream must stay on the server side, so a workspace can publish its own work without ever holding a token that could publish anything else — the git service reads that token when it pushes and it never crosses into a workspace container.152 And the push must be synchronously durable: the hook relays the proposed ref transaction upstream before accepting it locally, so a client's success means the upstream has durably accepted the same atomic ref set.153 The live relay performs that as a single atomic push.154
Tools in general offer one of those or the other. A managed push mirror holds the credential for you and copies asynchronously, reporting success before the copy lands — which is precisely the false success that strands an agent's work. A caching or redirecting git proxy relays synchronously but forwards the client's credentials, which is the isolation being bought in the first place. Needing both at once is the unusual requirement, and it is what the pre-receive relay exists to provide.
That no off-the-shelf component satisfies both requirements together is the project's survey, not a benchmark: read it as the reason the decision was taken, not as proof that no such tool exists.
Rolling the decision back is deliberately cheap: the relay is a bare repository plus a pre-receive hook, so undoing it means pointing the daemon back at the bare repository.
Git needs nothing from you inside a forge. A read-only global gitconfig redirects the project's GitHub URL to a bare mirror in its own named volume, so git remote -v still says github.com while clone, fetch and push hit the enclave.616 A push is not acknowledged until the mirror's hook has relayed exactly those refs upstream with one atomic push.6263 The token authorising that relay is read from the vault inside the git service at push time and never enters the forge.6465 Deletions and branch rewinds land nowhere;66 a project with no upstream keeps its pushes in the mirror.67
note The pairing is the argument, not novelty. The pattern of relaying a push
onward from a hook is documented elsewhere; what is specific here is refusing to
report success before the upstream has it, while the credential stays outside the
workspace.
checked Works out of the box once GitHub Login has run on the host; without a stored token the relay refuses the push and says so.68
known limitation The push path inside the enclave is anonymous: any peer that can reach the mirror may create branches or fast-forward them and have the privileged relay carry those updates upstream. Being on an internal-only network is placement, not client authentication.69 The authenticated SSH lane that would replace it is built but dark behind an environment variable that defaults to off.70 Offline, a push simply fails — nothing is queued.71
what happens next Turning the SSH lane on by default is planned and waits on a decision about the transport.72 For the offline push, no fix is planned yet.
The vault
Secrets live in a Vault container at vault:8200, reached over TLS with the enclave certificate already trusted.7475 A lane with a credentialed provider is handed a single-purpose token on tmpfs at /run/secrets/vault-token whose policy reaches only that provider's paths; the generic forge policy cannot read the GitHub token, which the git mirror, the tray and the login flow can.76777879 Across a stop, Vault unseals itself from one Shamir share held in your host keychain, with no passphrase prompt.8
what happens next No fix is planned yet.
The experts and the project index
The agent finds four tool servers already registered, for Claude Code and OpenCode alike: git tools, a project index, the host-browser bridge, and a plan expert.8384 The index answers project type, status, commands and layout for any checkout, citing what it indexed; the plan expert returns cited answers or a typed refusal, never a guess, and answers only on a checkout carrying Tillandsias' own plan crate — elsewhere it reports itself degraded, by design.858687 Both start in the background after the clone and never gate the session.88 The status now distinguishes the running expert’s capabilities from what a relaunch would build: waiting for a build, needing a relaunch, and losing capabilities on relaunch have different machine-readable verdicts.150 A ready index is separate from a usable answer engine. Deterministic project questions can work without local inference; synthesis still returns a typed refusal when the synthesis capability is absent, even if inference itself is ready.151 The "on-demand" spec describes tray-side spawning and health checks that do not exist; the harness launches each server itself.89
checked Works out of the box on any project, with no registration step.85
known limitation The context file tells the Claude lane about a conversational Local Experts mode, but only the OpenCode configuration carries an agent that reaches the grounded endpoint behind it.9091 And the pre-expert-binary trap is half closed: a forge seeded from a checkout without the expert sources now reports itself degraded instead of ready, but the fresh-forge criterion — an in-forge probe grading full marks with no manual step — is recorded open.92
what happens next No fix is planned yet for the Claude-lane mismatch. The fresh-forge check waits on a decision about how releases are branched.92
Sibling web containers
Ask the agent to publish the project and it does not start a server in the forge. It asks the host, over a socket that exists only for that lane, to launch a sibling web container beside it: read-only, capability-dropped, joined to the enclave network and given a router route.939495 The sibling sees the running forge's own worktree, resolved from the lane's live container and its RAM-backed source volume rather than from a path on the host, and it is given a selected public document tree rather than the repository: var/html, public or dist if one exists, otherwise the root index.html and the plainly static files and asset directories beside it; a Wrangler configuration at the project root selects a second, Wrangler-based runtime instead.160 The URL comes back as https://www.<project>.localhost:<port>, on a loopback TLS port the router listens on, with a leaf certificate issued for that name.96 The project is attributed from the socket the request arrived on, never from the request.95 From inside the forge the proxy forwards .localhost requests to the router;97 this page has not traced the HTTPS preview port through that rule.
checked A sibling never mounts .git, .env or the forge root, so the repository's history cannot be read through the router, as it could on the earlier publish path.16098 Publishing reads the running forge, so it no longer needs the project to live under ~/src on the host;160 the tray's project list still looks only there.100 And a sibling is not left running when its forge dies: a watcher stops it and removes its route once that forge is gone, and the next launch of the same lane clears a departed one; the stack-wide sweep still matches no sibling, so this cleanup is the preview's own.16199 All of this is read in the code, not shown live.
known limitation The feature's own spec still calls itself a draft whose verification is design-only, with executable coverage and live evidence pending.162 The only end-to-end test of publishing is still on the list of tests that no suite has ever run.104 Only www is routed, not the apex.96 The static web image is built by initialisation rather than by a forge launch, which ensures only four images;101102 a host that never initialised now gets an explicit error telling it to initialise the managed runtime image, where the earlier path hit a phantom registry pull that was fixed only for the status-check path.164103 The Wrangler runtime needs a second image that the list of images initialisation builds does not contain, so it stops with the same error.101164
what happens next Planned: prove the preview works live, then make its spec binding.163
what happens next For the apex route and the Wrangler image, no fix is planned yet.
Chromium
Two browser images are built at initialisation: a minimal headless core, and a framework layer on top of it with GUI Chromium, Node and Playwright.101105106 The runtime launches the framework image as a read-only, capability-dropped, incognito app window — but only for the OpenCode-web and observatory modes, and only on a machine with a display.107108 The browser tool the agent sees — open, screenshot, click, type, close; eval switched off — is a different thing: it drives a host Chrome from the user's cache directory that nothing shipped installs, so a fresh host answers "browser unavailable".109110111112
known limitation The headless core is built and never run,105 and the on-demand Chrome download the browser tool depends on still has no caller.111 The browser script now defaults to the enclave network,115 and the tool's allowlist accepts dotted project names while checking the project label exactly.113 Neither change installs the missing host Chrome or demonstrates proxy-only egress for every browser path.114
what happens next Planned and not started: give the Chrome installer a real caller, or remove it with its test and spec.112
what happens next For the remaining browser egress paths, no fix is planned yet.
The anatomy
The region is one Fedora guest — a VM, or a WSL2 distro on Windows — running a rootless Podman stack. Among the members:
- a forge, the container your coding agent runs in, one per project;
- a git mirror, a bare repo on the internal network;
- a vault holding your tokens;
- a forward proxy, the single outbound door;
- a router, a Caddy reverse proxy giving web services stable names;
- an inference container, the shared model server.
The roster is longer — a Nix cache, a catalog service, the observatory's web container, an SSH-lane sidecar — and most of those do not run in an ordinary session.1 Membership is not a hand-kept list: the spec names the attach sites by function symbol, and a guard compares the attach sites it finds in the source with that list, refusing the build if either side drifts. It reads code, not a live network.132
One rule carries the isolation: the containers share a Podman network created with --internal — no gateway, no route out.1 Only the proxy is dual-homed onto an egress network, and the launcher creates both networks on the first launch and reuses them after.131
The router publishes to one loopback address — port 80 by default, with fallbacks, or the port you pass with --port — and maps <service>.<project>.localhost onto internal ports. Nothing binds 0.0.0.0.2116
Starting one is a curl on Linux, an installer script on macOS, a tray executable on Windows. Then tillandsias --headless /path/to/project --claude.
known limitation One thing the spec asks for is missing; another is enforced more narrowly than it reads. The spec requires the network to be removed at application exit when empty, and nothing implements that — the only removal is the destructive reset.133 And the membership guard, which does run, counts only builder and launcher functions by name, so an attach site written any other way is invisible to it.32
what happens next No fix is planned yet.
checked The stack script, the tray launcher, the per-project runner and the proxy diagnostic all create the network with --internal, and the stack script refuses to reuse a network that lacks it.311731120121 A build check finds each tracked shell network-create command and checks it; its Rust check covers less, one file at a time.118
What the walls are made of
Agent tooling exists only inside the forge image, never on your host $PATH; a missing image makes the launcher refuse rather than fall back to a host binary.4122 Mounts are enumerated — your project, a read-only CA certificate, the tmpfs roots, a per-launch temp dir, the persistent tool-cache volumes and a Tillandsias-written read-only gitconfig — so $HOME, ~/.config and your keychain are simply not addressable from inside, and a regression test asserts no user home is ever mounted.5
The forge does not touch your working tree by default: it clones fresh from the enclave mirror, and pushes travel back through the mirror on their way upstream.123124 An opt-in escape hatch bind-mounts your real checkout read-write and prints a reduced-isolation warning.125 Hence the blunt operating rule: a finding you did not push is a finding you destroyed.7
What survives, exactly
Idempotence is what makes destroy-and-recreate a repair procedure rather than a loss. Every layer comes up clean from scratch, so the boundary worth memorising is not "will it break" but "what is on which side of the wipe":
- Survives a stop: the bare mirror in its per-project volume, with everything you pushed to it.6
- Survives a stop: vault data, auto-unsealed from a single Shamir share held in your host keychain, with no passphrase prompt. The spec requires the share to reach the vault only through a tmpfs-mounted secret;8 the shipped guest also keeps a fallback copy of the share on its own disk when no keyring is reachable, and inside a VM guest that file is written on every initialisation.126127 The lifecycle spec says survival of the store requires an unlocking keyring and allows no persisted fallback share, so that code path is ahead of the spec in the other direction: the fallback writer is still present at this release.158
- Survives even
podman system reset, on Linux: the model cache, because it is a host bind mount and not a named volume. The spec spells that distinction out, having once cost someone a wrong answer.9 The spec now moves that directory to~/.tillandsias/downloads/models, with a one-time migration from the old cache path; the launcher at this release still resolves the model directory under the cache root, so the move is specified and not yet in the code.9159 - Does not survive
--reset-guest: it removes every Tillandsias container, volume (the per-project git mirrors among them) and secret and the enclave and egress networks, then re-initialises. On Linux at this release it keeps the model cache and no longer deletes the Vault store, which is a host directory; whether your sign-ins then survive depends on the keyring still holding the unseal share. Push first.10128129--reset-stategoes further: it also destroys every Podman image, and still keeps the models and the keyring entries.156
known limitation The spec also promises that your host working copy is fast-forwarded after every successful push. No code implements it at this release: the file the spec names does not exist, and the tray-managed host checkout it fed was removed. After a forge push, your host checkout moves only when you pull.130143
what happens next No fix is planned yet.
known limitation On macOS the "cache survives" line is not yet proven. The VM at this release already boots with a second shared directory for the model cache, and the guest's first boot mounts it, so the durable path exists.131132 What remains open: guests provisioned before that change are not migrated, because the mount is written on first boot only, and survival across a VM rebuild has not been demonstrated end to end.11133 The shipped uninstaller now preserves the VM unless asked to wipe, and asks for a typed confirmation before it removes anything.134135157 A partially restored cache is worse than none — the inference service answers a version check and then fails every request.11
known limitation On Windows, wiping the guest used to leave the old unseal share in Credential Manager, and the tray pushed that dead key into the fresh guest, permanently breaking GitHub login. The wipe paths now clear the stale share; a host that already holds one still delivers it, and the guest cannot tell the host it was rejected.12137
what happens next The reconcile half is blocked: the delivery reply carries no accept-or-reject signal, so a delivered share that fails to authenticate cannot yet lose to the guest's own secret.138 Until then, remove the stale credential by hand.
known limitation A host's container engine used to carry a global configuration line routing all registry traffic through the enclave proxy unconditionally, so with the stack down podman build and podman pull died on a raw DNS error that names nothing. Initialising a host now removes that line and hands the proxy setting to each container instead; a host never re-initialised still carries it, and a build that needs the absent proxy still fails without naming it.14014114213
How Tillandsias itself is tested
The checking the earlier levels describe happens in a local gate, run on the developers' own machines: specs declare intent, litmus tests turn each requirement into an executable check, traces show the check ran, and the gate refuses a push that breaks the chain.
Read a passing gate with care. Eleven litmus files have never been run by any suite; they sit on a list that may only shrink, so that switching the gate on did not turn them all red at once.14
known limitation No server checks a change when it is pushed or proposed. The cloud runs one job, the release build, because the signing keys exist only there; everything else is checked only by the local gate.15
what happens next This is a deliberate choice to save cost, not a gap waiting for a fix.15
How we know — 159 sources
Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.
- 1Enclave network requirements — `--internal` creation, reuse, cleanup on exit, proxy-only egress, and the symbol-anchored membership list the guard enforces openspec/specs/enclave-network/spec.md#L19-L75
A new enclave member MUST be added to this list in the same commit that attaches it; `scripts/lua/check-enclave-membership-documented.lua` refuses the divergence in both directions.
- 2Loopback-only publish and the host-port fallback chain openspec/specs/subdomain-routing-via-reverse-proxy/spec.md#L3-L43
What did NOT change, and what the requirement is actually protecting, is the loopback-only invariant: the publish is `127.0.0.1:{host_port}:8080` in every branch.
- 3The stack orchestration path now creates an internal network scripts/orchestrate-enclave.sh#L90-L103
--internal
- 4Forge-as-only-runtime: the agent binaries must resolve inside a fresh forge, and the host must not need them openspec/specs/forge-as-only-runtime/spec.md#L48-L68
`command -v claude codex opencode bash` MUST print four valid paths from inside a freshly built forge container.
- 5Regression test asserting no user home is mounted into a forge, and the mounts it deliberately allows crates/tillandsias-headless/src/main.rs#L24432-L24432
must not mount a host .config dir into the forge; got source in: {arg}
- 6Bare mirror per project in a named volume; forge pushes persist there openspec/specs/git-mirror-service/spec.md#L19-L39
The system SHALL create and maintain a bare mirror repository for each project at `/srv/git/<project>` inside the git service container, backed by the named Podman volume `tillandsias-mirror-<project>`.
- 7The ephemerality rule, stated for agents AGENTS.md#L61-L61
**IN A FORGE, A FINDING YOU DID NOT PUSH IS A FINDING YOU DESTROYED.**
- 8Auto-unseal from the host native keychain, no passphrase prompt, share on tmpfs only openspec/specs/tillandsias-vault/spec.md#L109-L128
The unseal secret SHALL be loaded into a podman secret and mounted at `/run/secrets/vault-unseal` on tmpfs only.
- 9Model cache is a host bind mount, NOT a named volume — and why that decides what `podman system reset` destroys; the spec now names `~/.tillandsias/downloads/models/` openspec/specs/inference-container/spec.md#L30-L32
Models SHALL be stored in a HOST DIRECTORY (a bind mount, NOT a named Podman volume)
- 10What `--reset-guest` wipes and what it keeps crates/tillandsias-headless/src/main.rs#L1706-L1706
keeps the model cache) and re-initialize.
- 11macOS model cache and engine payload inside the VM disk image; the destroyers, the outage a lost engine caused, and the ~2.47 GB figure as filed (planned, not started) plan/index.yaml#L27667-L27667
every VM-directory deletion forces a ~2.47 GB re-download — the macOS twin of 518
- 12Windows Credential Manager retains a stale unseal share across a guest wipe; the wipe half landed, the reconcile half still open (planned, not started) plan/index.yaml#L26930-L26930
every Windows reset path (--reset-guest, installer -Purge, the e2e smoke) wipes the guest vault but never clears vault-shamir-share-v1 from Credential Manager
- 13Global container config hardcodes the enclave proxy, so builds fail on DNS when the stack is down; the open deliverable is a verdict naming the proxy (planned, not started) plan/index.yaml#L37615-L37615
~/.config/containers/containers.conf hardcodes http_proxy=http://proxy:3128 unconditionally, so when the enclave is down EVERY image build fails on DNS — and with it the release gate
- 14The ratchet list of litmus files that have never run, and why they are exempt openspec/litmus-tests/unbound-grandfathered.txt#L1-L25
THE ONLY LEGITIMATE EDIT TO THIS FILE IS A DELETION
- 15The cloud runs one job, the release; every other check runs locally methodology/ci.yaml#L286-L324
GitHub Actions runs EXACTLY ONE workflow: the release. Nothing else may consume cloud minutes — no push CI, no PR CI, no scheduled jobs, no cache warming, no webhooks. Every other gate runs locally.
- 16The startup context's infrastructure section: everything transparent, nothing to configure images/default/lib-common.sh#L4978-L5007
You never need to configure git remotes, tokens, SSH keys, proxy settings, or CA certs.
- 17The heavy base layer: Fedora Minimal with the toolchains in one package set images/default/Containerfile.base#L5-L35
rust cargo clippy rustfmt rust-analyzer cargo-deny
- 18The thin runtime layer on top of the base images/default/Containerfile#L1-L6
Builds upon the heavy base image to inject configuration,
- 19Image identity is a content hash of the sources openspec/specs/default-image/spec.md#L100-L104
The default forge image SHALL use a content-hash canonical tag derived from the image source set.
- 20Harnesses refresh at every launch into the persistent per-project tool cache, and a freshly installed binary that fails its contracts is replaced by the last good one openspec/specs/default-image/spec.md#L196-L237
A freshly installed OpenCode that violates any of those contracts SHALL be rejected and replaced by that last-good binary.
- 21The tool cache is a podman named volume, not a host bind-mount, so it cannot become a credential-leak path crates/tillandsias-headless/src/main.rs#L17589-L17589
A named volume — not a host bind-mount —
- 22The three RAM-backed roots the launcher mounts at this release crates/tillandsias-headless/src/main.rs#L18125-L18125
.tmpfs("/tmp:size=256m,mode=1777")
- 23The on-demand tool allowlist in full: every listed command becomes a PATH shim that installs on first use images/default/brew-tools-allowlist.txt#L1-L122
Commands listed here get a PATH shim in the forge: running the command
- 24Where the startup context file is written images/default/lib-common.sh#L4679-L4679
local ctx_file="$project_dir/.forge-startup-context.md"
- 25The hot/cold spec: exactly four RAM-backed roots, the source tree among them openspec/specs/forge-hot-cold-split/spec.md#L11-L24
Only these four path roots are HOT. "Maybe a hot path" is a HARD NO.
- 26The planned fix for storage: source tmpfs, host RAM gate and memory ceiling still to wire (planned, not started) plan/index.yaml#L8207-L8207
production launch runs check_host_ram, derives compute_memory_ceiling_mb, and emits equal --memory and --memory-swap limits
- 27The Homebrew prefix is ephemeral; every launch re-installs on-demand tools (planned, not started) plan/index.yaml#L12407-L12407
The brew prefix is ephemeral, so every forge launch re-installs and re-attests every tool — that is the rate-limit multiplier
- 28The startup context's claim of a Sigstore attestation per bottle images/default/lib-common.sh#L5079-L5084
it verifies a Sigstore attestation per bottle
- 29What the shim verifies today: integrity from one publisher, attestation off images/default/brew-shim-exec.sh#L12-L21
Sigstore verification is OFF because it requires a GitHub credential to FETCH
- 30The source tmpfs budget is calculated from the mirror pack size crates/tillandsias-headless/src/main.rs#L17625-L17625
format!("/home/forge/src:size={budget}m,mode=0777")
- 31The launcher creates the enclave network with `--internal` on first launch, after ensuring the egress network crates/tillandsias-headless/src/main.rs#L3218-L3221
command.args([ "network", "create", "--internal",
- 32The membership guard (now a Lua decider): every attach site's enclosing function must be in the spec and vice versa; it counts only builder and launcher functions scripts/lua/check-enclave-membership-documented.lua#L60-L81
local BUILD_LAUNCH = [[^(build|launch)_]]
- 33The only network removal in the launcher: the destructive reset crates/tillandsias-headless/src/main.rs#L10642-L10642
command.args(["network", "rm", "-f", &name]);
- 34Proxy variables injected into every enclave container from one source crates/tillandsias-headless/src/main.rs#L2015-L2015
"http_proxy=http://proxy:3128".into(),
- 35The forge composes vendor roots plus the proxy CA into the system-default bundle before any network client starts images/default/lib-common.sh#L12-L23
Compose the per-install CA into that target atomically, before any network
- 36No per-client CA variables; trust flows through the distribution's standard path openspec/specs/transparent-https-caching/spec.md#L57-L62
launchers and entrypoints SHALL NOT select CA files with `GIT_SSL_CAINFO`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, or `NODE_EXTRA_CA_CERTS`.
- 37A regression test pins the CA mount and the absence of CA overrides crates/tillandsias-headless/src/main.rs#L24542-L24542
typed forge launcher must mount the single runtime CA input
- 38Package-manager workflows must work with no configuration openspec/specs/proxy-container/spec.md#L157-L157
common development workflows (npm install, cargo build, pip install, flutter pub get) work out of the box without configuration.
- 39Proxy purpose: allowlisted caching forward proxy, bump only the release-asset CDN, splice all other TLS openspec/specs/proxy-container/spec.md#L8-L14
HTTPS interception is deliberately exceptional: the proxy bumps only the exact GitHub release-asset CDN hostname and splices all other TLS traffic end to end.
- 40The three ssl_bump rules: peek once, bump one exact host, splice everything else images/proxy/squid.conf#L133-L139
ssl_bump peek ssl_bump_step1 ssl_bump bump github_release_assets ssl_bump splice all
- 41Spliced HTTPS is never cached by the proxy openspec/specs/proxy-container/spec.md#L44-L49
the HTTPS response SHALL NOT be cached (tunneled traffic is opaque to squid)
- 42cargo, Go, npm and pip caches all redirected into the per-project cache images/default/lib-common.sh#L2227-L2259
export CARGO_HOME="$PROJECT_CACHE/cargo"
- 43Blocked hosts get a TCP reset, not a 403 images/proxy/squid.conf#L151-L156
deny_info TCP_RESET strict_deny_acl
- 44The live miss-then-hit trace for the bumped host was never recorded, and the next action that would record it (planned, not started) plan/index.yaml#L7533-L7576
no rebuilt-image parser result or real identical-key MISS-to-HIT trace is claimed
- 45The consumer contract: one endpoint at inference:11434, downloads through the proxy openspec/specs/inference-container/spec.md#L13-L14
Forge containers SHALL access it via `OLLAMA_HOST=http://inference:11434`. The inference container SHALL use the proxy for model downloads.
- 46Inference readiness is best-effort, never a launch gate crates/tillandsias-headless/src/main.rs#L17226-L17226
Local inference readiness (order 392) is BEST-EFFORT, not a launch
- 47The single default chat model pulled on first run images/inference/entrypoint.sh#L120-L120
DEFAULT_MODELS="${TILLANDSIAS_DEFAULT_MODELS:-qwen2.5:0.5b}"
- 48The engine download pins a version and architecture-specific digests images/inference/entrypoint.sh#L329-L335
OLLAMA_VERSION="v0.34.0"
- 49The closed-vocabulary accelerator line every forge receives crates/tillandsias-headless/src/accel_probe.rs#L4402-L4402
accel_class=<workstation-gpu|mobile-npu|hybrid-gpu-npu|cpu-only>
- 50Ready means the endpoint answers and at least one model is cached; there is no "starting up" images/default/lib-inference-state.sh#L14-L24
Reason vocabulary (CLOSED SET — there is deliberately no "starting up"):
- 51Larger tier models are opt-in, not default images/inference/entrypoint.sh#L795-L798
tier pulls opt-in (set TILLANDSIAS_INFERENCE_TIER_PULLS=1) — skipping runtime tier pulls
- 52The CPU floor is unconditional by spec openspec/specs/inference-container/spec.md#L154-L159
Tier-S SHALL be available on every host regardless of GPU, NPU, driver, or engine availability, and SHALL NOT be gated on any device probe.
- 53A missing verifier or wrong engine digest prevents execution images/inference/entrypoint.sh#L371-L383
FATAL: sha256sum unavailable — cannot verify the engine payload, refusing to execute it
- 54NVIDIA delivery is gated on a CDI spec; without one the host degrades to CPU with a loud remedy crates/tillandsias-headless/src/main.rs#L6059-L6059
a GPU host without CDI degrades to CPU with a LOUD remedy
- 55The AMD lane is selected only when ROCm reports a graphics agent crates/tillandsias-headless/src/main.rs#L5171-L5171
let rocm = std::process::Command::new("rocminfo")
- 56The spec: NPU rows are never attempted on the shipped engine openspec/specs/inference-container/spec.md#L186-L193
N rows SHALL NOT be attempted on the `ollama` engine kind. Ollama cannot use any NPU
- 57On Windows the NPU cannot be seen from inside the guest at all scripts/windows-host-capability-probe.sh#L12-L14
The NPU is not merely undetected in the guest, it is STRUCTURALLY invisible:
- 58The health check tests that the runner binary exists; the real load check runs once at startup images/inference/Containerfile#L112-L121
A real generate would be the strongest assertion but is far too expensive at a
- 59NPU passthrough for the container is planned (planned, not started) plan/index.yaml#L9263-L9263
Impl: NPU /dev/accel passthrough plumbing for the inference container (AMD XDNA2 first lane)
- 60The host-native sidecar for Metal and the NPUs is planned (planned, not started) plan/index.yaml#L9419-L9419
Impl: host-native inference sidecar registry (macOS Metal/MLX, AMD XDNA2 flm/Lemonade, Intel NPU OpenVINO) behind the enclave proxy
- 61Agents configure nothing and still see the original GitHub URL openspec/specs/git-mirror-service/spec.md#L470-L476
`git push`, `git fetch`, and `git clone` SHALL work with zero agent-side configuration
- 62One atomic push of explicit refspecs images/git/relay-refs.sh#L298-L298
git push --atomic "$PUSH_URL" "$@"
- 63The mirror acknowledges only after upstream durably accepts the ref transaction images/git/pre-receive-hook.sh#L765-L765
Push rejected: configured upstream did not durably accept the ref transaction
- 64The GitHub token is read from Vault at push time inside the git service and never crosses into a forge openspec/specs/git-mirror-service/spec.md#L14-L16
The git service reads the GitHub token from Vault at push time via Vault CLI; the token never crosses into a forge container.
- 65The credential helper fetches the token from Vault over git's credential protocol images/git/git-credential-tillandsias.sh#L93
TOKEN="$(vault-cli read -field=token secret/github/token 2>/dev/null || true)"
- 66Every start re-applies receive hardening: no deletes, no rewinds, object checks openspec/specs/git-mirror-service/spec.md#L98-L106
the git service SHALL set `receive.denyNonFastForwards=true`, `receive.denyDeletes=true`, and `receive.fsckObjects=true`.
- 67A project with no upstream keeps pushes durably in the local-only mirror images/git/relay-refs.sh#L157-L157
No upstream configured; accepting as a durable local-only mirror update
- 68Without a stored token the relay refuses and names the remedy images/git/relay-refs.sh#L193-L193
HTTPS upstream credential is unavailable; run GitHub Login before pushing
- 69The mirror daemon's push path is anonymous inside the enclave, and the repo says so in the same breath as the hardening that limits the damage images/git/entrypoint.sh#L504-L504
The daemon remains anonymous inside the enclave: any reachable peer can still
- 70The authenticated SSH push lane is dark behind one flag until the default flip crates/tillandsias-headless/src/main.rs#L13186-L13186
std::env::var("TILLANDSIAS_MIRROR_SSHD")
- 71Offline or credential-less, the forge's push returns non-zero and nothing is partially applied openspec/specs/git-mirror-service/spec.md#L224-L230
the forge's `git push` SHALL return non-zero
- 72The SSH-lane default flip is planned (planned, not started) plan/index.yaml#L20538-L20538
T11+T12 — flip one lane behind a flag once the §4a matrix is green
- 74Vault is the default and only Linux secrets backend; GitHub token and short-lived per-container tokens openspec/specs/tillandsias-vault/spec.md#L11-L14
Run a HashiCorp Vault container as the default and ONLY Linux secrets backend for Tillandsias.
- 75The shipped Vault listener is TLS-only images/vault/vault.hcl#L16-L24
tls_cert_file = "/run/secrets/tillandsias-vault-tls-cert"
- 76Every OAuth-credentialed agent lane mounts one scoped Vault token crates/tillandsias-headless/src/main.rs#L18186-L18186
Every OAuth-credentialed agent lane mounts a scoped Vault token so its
- 77Forge containers get no broad Vault token, only a provider-scoped short-lived one openspec/specs/tillandsias-vault/spec.md#L348-L353
Forge containers SHALL NOT receive a broad Vault token.
- 78The generic forge policy cannot read the GitHub token images/vault/policies/forge.hcl#L1-L8
Explicitly NO github or token access — forge containers must remain
- 79The tray policy reads the whole secret tree images/vault/policies/tray.hcl#L1-L6
path "secret/*" {
- 80The startup context advertises http for a TLS-only Vault images/default/lib-common.sh#L5005-L5005
http://vault:8200
- 81The in-image helper defaults to https and the tmpfs token path images/default/vault-cli.sh#L23-L24
VAULT_ADDR="${VAULT_ADDR:-https://vault:8200}"
- 82Client tokens default to a one-hour TTL openspec/specs/tillandsias-vault/spec.md#L252-L256
Every client token, including tokens minted by Vault Agent, SHALL default to TTL 1h with a maximum TTL of 24h.
- 83Four MCP servers registered for Claude Code inside the forge image images/default/config-overlay/claude/mcp.json#L2-L23
"command": "/home/forge/.config-overlay/mcp/forge-plan.sh"
- 84The same four servers registered for OpenCode images/default/config-overlay/opencode/config.json#L66-L87
"command": ["/home/forge/.config-overlay/mcp/forge-plan.sh"],
- 85Spec: the generic project expert bootstraps for any project with no registration or configuration openspec/specs/forge-environment-discoverability/spec.md#L171-L173
The forge MUST bootstrap a project expert surface for an ARBITRARY mounted project without manual registration, configuration, or repository-type knowledge from the caller.
- 86The plan expert's refusal rule: zero citations means unsupported images/default/config-overlay/mcp/forge-plan.sh#L1355
An answer with zero citations is returned as confidence=unsupported — the expert refuses rather than guesses.
- 87The plan expert is degraded by design on a project without the plan crate images/default/lib-common.sh#L4996-L4996
(this project has no plan expert — expected off-tillandsias)
- 88Discovery, expert build and the grounded endpoint all backgrounded and fail-soft after the clone images/default/lib-common.sh#L1652-L1658
ensure_forge_experts >>/tmp/forge-lifecycle.log 2>&1 || true
- 89The on-demand spec obliges the tray to detect and spawn servers openspec/specs/mcp-on-demand/spec.md#L32-L37
the tray MUST detect that the filesystem MCP server is not running
- 90The startup context every lane reads names Local Experts mode images/default/lib-common.sh#L5015-L5015
Local Experts mode (the
- 91Only the OpenCode configuration carries an agent bound to the grounded endpoint images/default/config-overlay/opencode/config.json#L18-L30
"model": "tillandsias-experts/all",
- 92The pre-expert-binary trap: criterion 2 closed, the fresh-forge criterion still open (planned, not started) plan/index.yaml#L6796-L6873
criterion 1 (release/branch decision) still OPEN — packet intentionally stays `ready` (partial reduction)
- 93The catalog is an allowlist: WEB only, refused host-side otherwise openspec/specs/enclave-service-catalog/spec.md#L22-L26
Requests outside the catalog are refused host-side; the guest cannot mint categories.
- 94The web image: busybox httpd on 8080, document root /var/www openspec/specs/web-image/spec.md#L24-L25
The image MUST serve static files from `/var/www` on port 8080 using busybox httpd with no additional packages or configuration.
- 95One socket per lane, bind-mounted into the forge; attribution comes from the listener that accepted the connection, never from the request openspec/specs/mcp-tool-socket/spec.md#L31-L64
The project (and instance) a request acts on SHALL be derived from WHICH LISTENER accepted the connection. The tray SHALL NOT read the project from the request body, from the peer process's environment, or from any other peer-supplied source.
- 96The live publish path hands back an https URL for the www name on the router's TLS port crates/tillandsias-headless/src/local_web_preview.rs#L730-L730
Ok(format!("https://www.{project}.localhost:{}", tls_port()?))
- 97From inside the forge, .localhost requests are forwarded by the proxy to the router images/proxy/squid.conf#L183-L188
cache_peer_access tillandsias-router allow localhost_subdomain
- 98What could be read through the router when the earlier path served the repository root plan/index.yaml#L44421-L44421
/.git/config returned 200 through the router (full history + remote URLs reconstructible)
- 99The stack sweep's name patterns: a published sibling matches none of them crates/tillandsias-headless/src/main.rs#L7698-L7698
name.starts_with("tillandsias-git-")
- 100Projects must live under ~/src on the host; discovery hardcodes it (planned, not started) plan/index.yaml#L42106-L42106
tray discover_projects hardcodes ~/src and run_opencode_mode starts no lane listener
- 101The declarative image set initialisation builds: both Chromium images and the web image among them crates/tillandsias-headless/src/main.rs#L10190-L10192
"forge-base", "forge", "web",
- 102An ordinary forge launch ensures four images, not the web or browser ones crates/tillandsias-headless/src/main.rs#L17144-L17144
let images = ["router", "git", "inference", "forge"];
- 103The phantom pull the status-check path was fixed for crates/tillandsias-headless/src/main.rs#L11005-L11005
phantom registry pull (125) on any version handover.
- 104The end-to-end publish litmus has never been run by any suite openspec/litmus-tests/unbound-grandfathered.txt#L21-L21
litmus:publish-local-e2e
- 105The headless core image and its entrypoint images/chromium/Containerfile.core#L36-L37
ENTRYPOINT ["/usr/lib64/chromium-browser/headless_shell", "--headless=new"]
- 106The framework image extends the core with GUI Chromium, Node and Playwright images/chromium/Containerfile.framework#L15-L35
RUN npm install -g --prefix=/usr \ playwright \
- 107The hardened, ephemeral browser container the runtime launches crates/tillandsias-headless/src/main.rs#L15944-L15944
Hardened browser boundary: read-only rootfs, CAP_DROP=ALL, no-new-privileges,
- 108Browser launch requires a graphical session crates/tillandsias-headless/src/main.rs#L15902-L15902
OpenCode Web browser launch requires a graphical session (DISPLAY or WAYLAND_DISPLAY)
- 109The browser tool spawns a host Chromium from the cache directory, not a container crates/tillandsias-browser-mcp/src/launcher.rs#L78-L82
root.join("current/chrome"),
- 110A missing bundled Chromium returns BROWSER_UNAVAILABLE crates/tillandsias-browser-mcp/src/server.rs#L431-L435
BROWSER_UNAVAILABLE: bundled chromium not yet downloaded
- 111The only Chromium installer has no consumer scripts/install-chromium.sh#L10-L16
INTEGRATION STATUS (2026-08-16 freshness audit, 774-cfw8): NO consumer
- 112Give the unused Chrome installer a caller, or remove it (planned, not started) plan/index.yaml#L22074-L22074
scripts/install-chromium.sh has ZERO consumers while its header claimed two integrations; a litmus pins a shape nothing ships — wire it or tombstone it
- 113The allowlist accepts dotted project labels and compares them exactly crates/tillandsias-browser-mcp/src/allowlist.rs#L250-L257
let host_project_label = labels[1..labels.len() - 1].join(".");
- 114Safe variant: proxy-only egress is the spec openspec/specs/chromium-safe-variant/spec.md#L16-L19
Full network isolation inside the enclave, with allowlist enforcement via the proxy only; no host-gateway internet fallback
- 115The browser script defaults to the enclave network scripts/launch-chromium.sh#L93-L98
"--network=${TILLANDSIAS_BROWSER_NETWORK:-${TILLANDSIAS_ENCLAVE_NET:-tillandsias-enclave}}"
- 116The router's host-port candidates: an explicit --port first, then 80 and the fallbacks crates/tillandsias-headless/src/main.rs#L6915-L6915
let mut candidates = vec![80, 8080, 18080, 28080, 38080, 48080, 58080];
- 117Stack orchestration script now passes `--internal` and refuses to reuse an unisolated network scripts/orchestrate-enclave.sh#L82-L126
Network $ENCLAVE_NET EXISTS BUT IS NOT INTERNAL — it was created without --internal, so every member has NAT egress and the proxy is not the only way out (order 972-a8vh, spec:enclave-network).
- 118The gate discovers tracked shell launchers and names its Rust limit scripts/check-enclave-network-internal.sh#L113-L135
The shell side IS invocation-scoped: continuations are folded first, and the flag must appear on the same logical line as the create verb.
- 120The per-project runner creates an internal network scripts/run-forge-project.sh#L127-L130
"$PODMAN_CTL" network create --driver bridge --internal --subnet "$ENCLAVE_SUBNET" "$ENCLAVE_NET" >/dev/null
- 121The proxy diagnostic creates an internal network scripts/diagnose-proxy.sh#L94-L97
podman network create --driver bridge --internal --subnet "10.0.42.0/24" "$ENCLAVE_NET"
- 122A missing forge image makes the launcher refuse, never fall back to a host binary openspec/specs/forge-as-only-runtime/spec.md#L114-L115
The tray MUST refuse to launch an agent if the forge image is missing — it MUST NOT silently fall back to a host binary.
- 123Clone-only by default: the host checkout is the opt-in path crates/tillandsias-headless/src/main.rs#L18039-L18039
Order 437: clone-only by default.
- 124Forge containers clone from the mirror and push through it openspec/specs/git-mirror-service/spec.md#L54-L61
Forge containers SHALL clone from `git://git-service/<project>`
- 125The reduced-isolation warning the host-mount escape hatch prints crates/tillandsias-headless/src/main.rs#L7435-L7435
WARNING: WORKSPACE ENCLAVE ISOLATION IS REDUCED
- 126Inside a VM guest the share is written to a fallback file on every initialisation crates/tillandsias-headless/src/vault_bootstrap.rs#L2851-L2851
Inside the VM there is no OS keychain, so these files are the only durable
- 127Where no OS keyring is reachable the share falls back to a file in the cache directory crates/tillandsias-headless/src/vault_bootstrap.rs#L4907-L4907
using fallback file (expected in VM guest and headless environments)
- 128What the reset wipes on the host side: nothing; the function that used to name the vault data directory returns an empty list crates/tillandsias-headless/src/main.rs#L10397-L10399
fn reset_guest_wipe_paths(_cache_dir: &Path) -> Vec<PathBuf> {
- 129The pin test: the model cache must never be in the reset wipe set crates/tillandsias-headless/src/main.rs#L33303-L33303
the inference model cache must never be in the reset wipe set
- 130The host working-copy fast-forward is specified, and specified against a file that does not exist openspec/specs/git-mirror-service/spec.md#L344-L363
The tray SHALL trigger a fast-forward attempt on the host working copy at `<watch_path>/<project>` for every successful push to the enclave bare mirror
- 131The VM boots with a second shared directory for the model cache crates/tillandsias-vm-layer/src/vz.rs#L2907-L2907
tag: "model-cache".to_string(),
- 132The guest's first boot persists the model-cache mount crates/tillandsias-vm-layer/src/vz.rs#L1078-L1078
model-cache /root/.cache/tillandsias/models virtiofs nofail 0 0
- 133The shared directory exists; migration and end-to-end survival are unproven plan/index.yaml#L27804-L27804
guest wiring verified by source scan, MIGRATION UNVERIFIED.
- 134The shipped uninstaller preserves the VM unless asked to wipe scripts/uninstall.sh#L228-L228
Preserving the VM image in $DATA_DIR (use --wipe to remove it).
- 135The uninstaller fix the macOS cache work waited on plan/archive/packets-2026-08.yaml#L23430-L23436
shipped uninstall.sh deletes the 11.83 GiB VM directory with no --wipe, no root and no prompt
- 136The cold measurement and its limit for a populated model cache plan/index.yaml#L27838-L27838
Re-fetched from the network: the Fedora Cloud qcow2 only, 514 MB.
- 137The wipe half landed; a host already holding a stale share is unaffected plan/index.yaml#L26924-L26924
a host that ALREADY holds a stale credential is unaffected
- 138The delivery reply carries no accept-or-reject signal (planned, not started) plan/index.yaml#L37937-L37937
DeliverCredentials reply says "I received it", never "I accepted it"
- 140Initialisation removes the orphaned global proxy block crates/tillandsias-headless/src/main.rs#L9222-L9222
Remove the orphaned `[engine] env` proxy block.
- 141What initialisation prints when it converges the file crates/tillandsias-headless/src/main.rs#L9383-L9383
removed the orphaned [engine] env proxy block from {} (923-rmtw); containers receive proxy env per-container
- 142The global-config fix, archived as completed plan/archive/packets-2026-08.yaml#L46813-L46818
containers-conf-env-line-is-orphaned-and-never-converges
- 143The decision that removed the tray-managed host checkout plan/index.yaml#L16185-L16185
It is REMOVED ENTIRELY.
- 145Fetch and pin the attestation bundles at image build time so no lane needs a GitHub identity (planned, not started) plan/index.yaml#L12320-L12320
a forge lane installs an allowlisted tool with attestation verified and NO GitHub credential present anywhere in the lane
- 146The shared inference container is created only when it is not already running, so sibling forges share one crates/tillandsias-headless/src/main.rs#L17277-L17278
inference is recreate-if-not-running (a --replace would drop loaded models and interrupt a sibling's inference mid-flight).
- 147The test pinning the scoped lease for every credentialed lane, and its absence from the credential-free ones crates/tillandsias-headless/src/main.rs#L29042-L29042
Credential-free lanes never mount a provider lease.
- 148The check gate invokes the membership guard and fails the build when it refuses build.sh#L2922-L2922
if ! _run_lua_decider "scripts/lua/check-enclave-membership-documented.lua" 2>&1; then
- 149Only the clone-only lane mounts the RAM-backed source volume; the host-mount arm binds the checkout instead crates/tillandsias-headless/src/main.rs#L18051-L18062
forge_ram_workspace_volume(project_name),
- 150The context distinguishes waiting, relaunching and relaunch regression images/default/lib-common.sh#L4994-L5003
\`relaunch-regresses\` means the running binary has MORE than the checkout and a relaunch would REMOVE capability.
- 151Ready inference alone does not provide the project index with a synthesis tier images/default/config-overlay/mcp/project-info.sh#L789-L789
no synthesis tier is wired into project_answer yet
- 152The upstream token is read by the git service at push time and never enters a workspace container openspec/specs/git-mirror-service/spec.md#L15-L16
The git service reads the GitHub token from Vault at push time via Vault CLI; the token never crosses into a forge container.
- 153The relay is synchronous: local acceptance follows upstream acceptance images/git/pre-receive-hook.sh#L6-L8
Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.
- 154The live relay pushes the refs as one atomic transaction images/git/relay-refs.sh#L298-L298
if OUTPUT="$(GIT_TERMINAL_PROMPT=0 git push --atomic "$PUSH_URL" "$@" 2>&1)"; then
- 156What a soft reset destroys and what it keeps crates/tillandsias-headless/src/main.rs#L10557-L10567
<cache>/tillandsias/models and every other download in the cache
- 157The uninstaller asks for a typed confirmation before removing anything scripts/uninstall.sh#L184-L184
Type \"delete\" to uninstall, or press Enter to cancel:
- 158The lifecycle spec's amendment: store survival requires an unlocking keyring, no persisted fallback share openspec/specs/host-state-lifecycle/spec.md#L13-L18
survival of the Vault store REQUIRES an unlocking keyring — no persisted fallback share
- 159The launcher resolves the model directory under the cache root crates/tillandsias-headless/src/main.rs#L5954-L5957
&tillandsias_core::cache_root::cache_root().join("models"),
- 160The preview never mounts .git, .env or the forge root; it selects a document tree or named assets crates/tillandsias-headless/src/local_web_preview.rs#L544-L551
Static compatibility selects a public document tree, never mounts .git/.env/the forge root into a sibling.
- 161A departed lane's preview and route are removed crates/tillandsias-headless/src/local_web_preview.rs#L1289-L1291
Called only after the launcher proves no forge remains for this lane.
- 162The preview spec's own status openspec/specs/local-web-preview/spec.md#L7-L10
verification: S0 (design; executable coverage and live evidence pending)
- 163The plan item to prove the preview live and activate the scoped specs plan/index.d/20261007-local-web-preview-design-yoga.yaml#L79-L79
title: Prove live tillandsias.org preview and activate scoped specs
- 164Publishing refuses when the managed runtime image is absent crates/tillandsias-headless/src/local_web_preview.rs#L1038-L1041
initialize the managed runtime image before publication
The architecture interrogated rather than described — boundaries, egress, provenance, and what the tests do not actually test.Assumes the three levels before it.
Tillandsias, for an IT / security reader
Silvery leaf scales open to take water in, then trap air to keep it: every exchange across one surface.
Where the boundary actually is
The anatomy does not answer the security question: when an agent does something you did not intend, how far does it get?
First, a correction. The region was described as one Fedora guest — a VM, or WSL2 on Windows. True on two platforms, false on the third: Linux provisions no VM, and the orchestrator drives rootless containers directly on your host.1 The promise is that this costs nothing but Podman; the price is that the enclave is a hypervisor boundary on macOS and Windows and a namespace boundary on Linux — where an escape lands as your own uid, with your $HOME.2
Inside, every launch is meant to carry four flags — userns mapped to your uid, all capabilities dropped, no-new-privileges, and label-disable, which is to say SELinux labelling off on every container — audited by a policy module that rejects privileged mode, a non-identity userns, or a blanket capability add.498 Why this envelope: keep-id makes an escape land as you rather than root, and a checker that never runs Podman is one small place to ask whether a launch still carries it. The compromise is the fourth flag, bought so bind mounts work on an SELinux-enforcing host.9294 Read the envelope as defaults on a struct, not an invariant — and not quite every container: the vault adds one capability back, omits --rm, and never passes the checker at all.53
checked The threat model is written down, including the parts that make the design look bad — the architecture notes state plainly that the allowlist is generous by design and that an attacker can encode data in DNS queries or headers to an allowed domain.3
The litmus now derives its launch flags from the product's declared envelope.58 Its repaired user-namespace probe checks ownership of a host-owned mounted file, because inspecting the namespace label or comparing the process uid alone could give a misleading answer.100 That is evidence about the declared envelope and Podman on the tested host, not proof that every production launch uses it.
checked Production hardening checks now survive release compilation: the checked serializer returns an error for a stripped envelope, and both attached and delegated forge launch paths refuse it.6555657 The general capability-add escape hatch has also been removed from the typed container builder.27 Before this release the check was only a debug assertion, and the security litmus printed success on both branches.7
known limitation Enforcement is still incomplete across launch paths: proxy, mirror, observatorium and a host-browser launch assemble or serialize arguments without the policy check, and the Podman client accepts raw arguments.6490959697 The typed builder retains private boolean fields, although its public constructor turns all four on and exposes no setters to turn them off.63 A separate test defect remains: the capability probe treats empty output as success without checking whether the inspection ran, so a failed podman exec can still print CAPABILITIES_DROPPED.101
what happens next The checked serializer and forge callers are the enforcement mechanism that exists; extending it to every launch is not implemented.5565 The project's build rejects a litmus test whose success and failure print the same thing,6061 but that check does not catch the empty-output failure above,62 and the project's other test-quality checks are partial too.854
known limitation SELinux confinement is disabled on every forge, so on a Fedora or RHEL host the agent's container is less confined than a default one. The project does confine elsewhere — the vault carries a loadable policy module,50 the Windows guest embeds policies for the control daemon and the vault51 — but on a rootless Linux host the vault too runs unconfined.52
what happens next MCS labelling of the forge is recorded as a planned later phase — intent, with no implementation and no litmus behind it.5 For the missing hypervisor on Linux no design tradeoff is recorded, only the blast radius of an escape — your uid, your $HOME2 — mitigated by defence in depth rather than a stronger boundary.
not yet shown Defence in depth is argued layer by layer below; nothing demonstrates that the layers compose to bound an escape.
Egress: intercepted versus passed through
The intended egress boundary is network placement plus proxy environment variables. The proxy configuration supplies hostname rules; it does not establish that the host or container networking stack uses no packet filters. An agent can unset HTTP_PROXY; what stops it is that the namespace has nowhere to send the packet. Then the question everyone asks on hearing "trusted CA in the container":
checked This is not a blanket MITM. The proxy peeks at the handshake and then splices — passes through undecrypted — everything but one exact hostname, GitHub's release-asset CDN, bumped so large binaries can be cached. Registries, GitHub's APIs and every auth endpoint stay end-to-end encrypted.9
shown The policy is three lines of a config file you can read, and the bump is scoped to one client-requested server name.9
The cost of that restraint: with one host decrypted the proxy does no payload inspection — hostnames, not content — and its default-deny allowlist admits anyone-can-publish namespaces, so an allowlist hit is no evidence of a benign destination.330 What it buys is a denial that looks like a network fault: a refused runtime request gets a TCP reset, not a page to negotiate with — the config's behaviour; the spec still promises a 403.3738
checked Where it does terminate TLS it still verifies upstream against the system trust store, and the config carries a written rule never to disable peer or domain verification — that would hide a real origin MITM behind a proxy-issued certificate.10
checked The stable release now puts the CA bundle under per-user persistent state instead of /tmp, so reboot cleanup no longer removes its declared location.284391 On Unix, new key material is clamped to owner-only before publication, and the existing-key path attempts the same repair; the non-Unix helper is a documented no-op.394041 This addresses the code side of a defect whose earlier fix had not reached material already created on hosts.11
known limitation Moving the directory does not make its permissions private by construction: the launcher still creates it with ordinary recursive directory creation.102
what happens next The planned fix is to create the directory privately before any key is written.45 It has not landed.
known limitation The published proxy spec describes something the code does not do: a fresh two-level EC P-256 chain generated per launch, held only on tmpfs, no CA key ever touching disk.12 The shipped launcher generates one self-signed RSA-2048 certificate with a 30-day life, stored in persistent per-user state and reused until refresh is needed.1328102
what happens next No fix is planned yet: the spec has not been marked stale, and nothing reconciles it with the code.
Secrets, and the channel that guards them
The GitHub token never enters the agent container: the mirror holds short-lived role credentials, relays pushes on the agent's behalf, and passes the credential on stdin rather than argv, closing a process-listing leak. The relay can invoke its helper for an HTTPS origin,48 but the helper now checks the credential request's protocol and exact host before returning a token.49
does not hold "Exfiltrating a token requires compromising both the mirror and the proxy." The architecture says this,3 but the proxy forwards permitted destinations even though its permissive port now binds to loopback.3031 A compromised mirror could use an allowed destination without compromising the proxy process. Network placement restricts routes; it does not prove that an allowed request cannot carry a secret.
known limitation The write path the agent pushes to is an anonymous git daemon receive-pack listener. The spec is blunt: network placement SHALL NOT be described as client authentication — any process on the enclave network can write to any mirror.14
Why the relay is bespoke, in security terms. The design needs the upstream credential to stay server-side and the push to be synchronously durable, and those two requirements pull against the available tools. A managed push mirror holds the credential and copies asynchronously, so it reports success before the copy lands; a caching or redirecting git proxy relays synchronously but forwards the client's credentials, dissolving the separation this section is about. The hook takes the third option: it relays the proposed ref transaction upstream before accepting it locally, so a client's success means the upstream durably accepted the same atomic ref set.107
That is a security property and not only a convenience. A false success is indistinguishable, to the agent that received it, from work that is safely published — so an asynchronous mirror would put unpublished work behind a green light, on exactly the disposable machine the design expects to be thrown away.
what happens next The spec labels it interim and constrains what may be built on it, but records no dated authenticated replacement.14 That replacement is built and dark: an SSH-CA push lane exists in the mirror image and the launcher, behind an environment variable that defaults off and that nothing in shipped packaging sets.4647 Separately, the mirror's push hook can now refuse a push to the default branch, or to a new branch outside a ref grammar, when a project's branch-discipline seed says to enforce it; that is policy over anonymous writers, not authentication of them, and the seed is read from the repository's own integration branch.117
The host↔guest control channel now uses a Noise handshake by default. Its key is derived through HKDF from the guest binary's SHA-256, separated by build version, wire version and hop.15103 This binds compatibility to known release material; it is not remote attestation that the peer runs unmodified code.
note This says what the relay refuses to report, not that the credential
boundary is complete. The RED above about the anonymous receive-pack listener is
the live gap on the agent-facing side of the same path, and it is not closed by
anything in this paragraph.
A release exposed a second boundary: the tray and guest are different executables, so hashing each process's own binary produced different keys even at the same version. The host now derives its key from the guest digest embedded at build time, while the guest hashes itself.103104 The Windows caller refuses a missing embedded digest and includes tests for matching and mismatched guest digests.105106 A one-process round-trip test could never have detected the original two-binary mismatch; that limitation is now recorded beside the implementation.104
checked An absent secure-wire setting now selects encryption, blank or invalid values are refused, and the Windows tray uses the same parser as the other callers.16666769 A build check refuses any caller that reads the setting on its own.68 The macOS host writes the resulting value into the guest unit.93 Windows and the VM image need no explicit environment line to obtain the new default from the listener.7071
known limitation Encryption is on by default, but caller identity remains weaker than operator authentication: anyone with the published guest bytes and version inputs can derive the same key. The guest's debug path instead uses a public constant.1529103 Explicit off still enables plaintext.16 Neither possession of the digest nor a completed handshake proves the caller is the legitimate orchestrator.
Supply chain and provenance
checked Release artifacts are signed with Sigstore cosign keyless — no long-lived key, identity asserted by the CI OIDC token, inclusion recorded in a public transparency log — and the Linux and Windows asset sets are checked for transitive coverage as a property over whatever is staged, not over a remembered list.197274
known limitation The signature proves less than it looks like. Verification matches an identity regexp pinning only the repository name — not the owning organization, the workflow, or the ref — so a fork of the same name signing through the same CI provider satisfies it, in the published command and in the shipped verifier alike.1978 Beyond that there is no SBOM, no build-provenance attestation, no SLSA or in-toto predicate, no container-image signing; base images are pinned by mutable tag rather than digest, so the image you build tomorrow is not the one you built today.20
what happens next No fix is planned yet.
known limitation Nothing a user runs by default checks a signature; only the hand-run verifier does. The install scripts fetch the checksum manifest over the same channel as the binary and on Linux continue without it;757677 no install path verifies a cosign bundle, and the updater's spec asks for no verification.79 The Windows installer verifies a SHA-256 taken from the manifest it fetched beside the zip and nothing else.76
what happens next An open defect asks the Windows installer to verify the manifest's cosign bundle before trusting it and to refuse a bad signature; it is not fixed in this release.115 Another asks that the install entry point either verify its own bundle or state in writing that transport trust is the anchor.116 No fix is planned for the updater.
Where ephemerality stops being a control
Ephemerality resets compute, not identity. Of what survives teardown, the security-relevant item is the Vault unseal share, which lives in the host OS keychain — outside every boundary the enclave draws.22 The reason is fair — no passphrase prompt, never wipe a vault the host can still open — but destroying the region does not destroy the ability to open what it held.
known limitation The application's Linux --reset-state no longer clears host-held Vault credentials. It asks the keyring first, announces what it keeps, and leaves the Vault store and both keyring entries in place under every outcome.81111 That is deliberate: a reset renews compute and keeps identity; the only code that deletes the unseal material is a clearer reserved for an uninstall command that no code calls yet.110 A bare podman system reset still clears only Podman state. We have not re-checked the macOS and Windows resets for this release.
what happens next The clearer's own documentation names the uninstall command as its caller, and that command is not in the binary; until it lands, nothing shipped removes the share on request.110
known limitation The keychain is not the only copy. The vault spec now says the share lives only in an unlocking keyring and that there is no persisted fallback share file,109 yet where no OS keyring is available the launcher still writes the share to a plaintext file in its cache directory,83 and inside a VM guest it writes that file on initialization.84 Only a hand-run clean-room script clears those fallback files;23 the ordinary key lookup can still use a persistent file8586 while the spec says persistent on-disk copies are deleted immediately.87
what happens next Replace the persistent fallback with the ephemeral handoff the spec now describes, or revise the spec to state the actual lifetime; no completed remedy for ordinary operation is recorded here.
Blast radius, autonomy, auditability
An agent's reach: its forge, every mirror on the enclave network, any allowlisted hostname with nothing inspecting the payload. Not your keychain, $HOME, or real checkout — unless you took the host-mount escape hatch.
The project's record of what it checked is committed and candid. It is also written by its own agents and unsigned, with no commit signing in evidence; the local gate binds a one-shot pass token to the tree it checked.99 With nothing in the repository showing server-side validation of a push, that record's provenance rests on the honesty of the process that wrote it.
checked The one MCP surface that lets a contained agent drive a host browser ships with arbitrary JavaScript evaluation disabled — the tool is advertised but returns an explicit refusal, so it is visible without being live.24
What the assurance claim actually is
The convergence argument you already have is not a security argument, and the project does not offer it as one. What carries the weight is a pair of invariants — verification claims must be falsifiable, and evidence is not proof25 — plus the methodology's refusal to read its completion score as a probability.26 So: a passing suite is a bounded signal over the defects someone thought to write a litmus test for, and, as the hardening case shows, only over those whose tests distinguish the relevant failure from success. Finite litmus coverage is not proof of absence of defects; the repo says so before you do.
How we know — 107 sources
Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.
- 1Podman the only host dependency on Linux; macOS/Windows provision a VM README.md#L52-L53
Podman is the only host dependency on Linux (auto-detected). macOS and Windows provision a lightweight Fedora-based utility VM; no host Podman required.
- 2Escape lands as the invoking user's UID on the host, with `$HOME` only openspec/specs/podman-idiomatic-patterns/spec.md#L209-L210
- **THEN** the escaped process runs as the invoking user's UID on the host, not as root - **AND** it has access only to `$HOME` and user-owned resources
- 3Enclave architecture — attack scenarios and stated limits docs/cheatsheets/enclave-architecture.md#L217-L225
The proxy allowlist is generous by design — a determined attacker could encode data in DNS queries or HTTP headers to an allowed domain.
- 4The mandatory hardening envelope and the argv policy checker crates/tillandsias-podman/src/policy.rs#L14-L22
pub const MANDATORY_HARDENING_FLAGS: [&str; 4] = [ "--userns=keep-id", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--security-opt=label=disable", ];
- 5SELinux MCS labelling recorded as a planned future phase openspec/specs/default-image/spec.md#L768-L769
future release will apply SELinux MCS labels to the forge domain, adding mandatory access control at the boundary layer.
- 6Checked serialization refuses an invalid launch in every build profile crates/tillandsias-podman/src/container_spec.rs#L424-L425
crate::policy::validate_launch_argv(&argv)?; Ok(argv)
- 7Historical false-pass defect and its corpus-wide guard openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L7-L13
Both branches printed what the harness was looking for, so the greps decided nothing
- 8The false-pass scanner: advisory, no caller gates on it, skips over half the corpus, keyed on a non-zero exit scripts/scan-litmus-false-pass.sh#L8-L48
Advisory — no caller gates on it.
- 9Peek at step 1, bump one exact hostname, splice everything else images/proxy/squid.conf#L122-L139
ssl_bump peek ssl_bump_step1 ssl_bump bump github_release_assets ssl_bump splice all
- 10Upstream verification is kept, with a written rule against disabling it images/proxy/squid.conf#L88-L97
verification MUST remain enabled. Never add DONT_VERIFY_PEER or DONT_VERIFY_DOMAIN: doing so would hide an origin MITM behind a proxy-issued certificate that enclave clients trust.
- 11Host-side CA re-clamp, with the reasoning for why a code fix was only half the job, and the two hosts found world-readable scripts/clamp-ca-material.sh#L15-L43
Two hosts were found with a world-readable CA private key days after the packet closed
- 12Spec: per-launch EC P-256 root-and-intermediate chain on tmpfs openspec/specs/proxy-container/spec.md#L63-L74
The system SHALL generate a fresh two-level CA chain on every proxy launch. The chain SHALL consist of a self-signed Root CA and an Intermediate CA signed by the root, both using EC P-256 keys. All key material SHALL be stored on tmpfs
- 13Launcher generates self-signed RSA-2048 CA with a 30-day life crates/tillandsias-headless/src/main.rs#L3523-L3526
"req", "-x509", "-newkey", "rsa:2048",
- 14Anonymous receive-pack listener; network placement is not authentication; owners named, no date openspec/specs/git-mirror-service/spec.md#L52-L72
Network placement SHALL NOT be described as client authentication: any enclave peer can still create or fast-forward refs and cause the privileged relay to carry them upstream.
- 15Noise PSK derived from the binary's own hash crates/tillandsias-secure-channel/src/lib.rs#L112-L114
let exe = std::env::current_exe().expect("current_exe for self-hash"); let bytes = std::fs::read(&exe).expect("read self binary for hash"); Sha256::digest(&bytes).to_vec()
- 16Absent means encrypted; plaintext requires explicit off crates/tillandsias-control-wire/src/secure_wire_mode.rs#L85-L131
Err(std::env::VarError::NotPresent) => Ok(SecureWireMode::On),
- 17The maturity ladder from default-OFF to secure-by-default to removing the plaintext path — status active plan/issues/secure-channel-maturity-ladder-2026-07-04.md#L1-L12
implement the e2e encrypted socket channel in ALL places, enable it at runtime with a flag, then advance through STABLE VERIFIABLE MATURITY GATES to secure-by-default and finally to removal of the insecure path.
- 18Per-boot key hardening — approved, deferred, unimplemented plan/issues/encrypted-channel-perboot-key-hardening-2026-07-01.md#L1-L22
This packet is the operator-approved **later hardening**: mix in a **per-boot secret** the host controls, so a leaked release secret alone no longer lets an attacker with a matching-release binary attach across a different VM boot.
- 19Keyless signing recorded in the Rekor transparency log, and the verification identity regexp that pins the repo name only .github/workflows/release.yml#L355-L355
--certificate-identity-regexp "https://github.com/.*/tillandsias/"
- 20Base images pinned by mutable tag images/git/Containerfile#L19-L21
FROM docker.io/hashicorp/vault:1.18 AS vault-agent FROM docker.io/library/alpine:3.20
- 21Research: such provenance verifies with no verifier identity; it recommends bundles plus a pinned root (not adopted) plan/index.yaml#L12318-L12318
verifying a Sigstore/SLSA attestation requires NO identity from the verifier. Reproduced with no token, no gh config, and inside a network namespace with no interface at all: exit 0, real certificate chain, correct signer; one flipped byte gives exit 1.
- 22Vault unseal share stored in the host OS keychain openspec/specs/tillandsias-vault/spec.md#L115-L118
The unseal key (the single Shamir share generated by Vault during initialization) SHALL be stored directly in the host OS's native secure keychain (Secret Service/KWallet on Linux, Credential Manager on Windows, Keychain on macOS) under the versioned name `vault-shamir-share-v1`.
- 23Linux clean-room reset clears host credentials and probes the result scripts/e2e-step2-linux.sh#L15-L38
"$SCRIPT_DIR/clear-vault-host-credentials.sh" 2>&1 | tee "$LOG_DIR/02-clear-credentials.log" || true
- 24Host-browser tool dispatcher refuses arbitrary JavaScript evaluation crates/tillandsias-browser-mcp/src/server.rs#L367-L379
"browser.eval" => Self::tool_error( id, "EVAL_DISABLED: browser.eval is disabled in v1; see follow-up change", ),
- 25Falsifiability and evidence-is-not-proof invariants methodology/philosophy.yaml#L69-L71
- verification_claims_must_be_falsifiable: true - convergence_requires_stability: true - evidence_is_not_proof: true
- 26Bounded ranking function, explicitly not a probability methodology/math-foundations.yaml#L91-L125
CentiColons are a finite bounded ranking function.
- 27General capability-add field and builder method removed crates/tillandsias-podman/src/container_spec.rs#L85-L100
there is deliberately no `cap_add`. The field existed as an unvalidated pass-through
- 28Shared CA path accessor composes the ca leaf under the state root crates/tillandsias-core/src/ca_path.rs#L37-L51
format!("{}/ca", ca_template())
- 29Debug builds derive the channel key from a fixed, public seed crates/tillandsias-secure-channel/src/lib.rs#L63-L68
Fixed, non-secret dev seed used in debug builds. It lets a locally-built host + guest of the *same* tree interoperate without a release build. It is intentionally NOT a secret
- 30The allowlist table: package registries and cloud namespaces anyone can publish to docs/cheatsheets/enclave-architecture.md#L144-L156
| Package registries | `registry.npmjs.org`, `crates.io`, `pypi.org`, `rubygems.org` | Package installation |
- 31The permissive port binds only to loopback in the proxy container images/proxy/squid.conf#L70-L78
http_port 127.0.0.1:3129 ssl-bump
- 32Permissive port: allow everything, no source restriction images/proxy/squid.conf#L146-L149
http_access allow CONNECT SSL_ports build_port http_access allow build_port
- 33Spec: on the permissive port domain filtering does not apply openspec/specs/proxy-container/spec.md#L114-L114
- **AND** domain filtering SHALL NOT apply
- 35The port is kept until a decision is made, not deleted images/proxy/squid.conf#L39-L44
That is an operator decision, not a cleanup. Until it is taken, this comment is the honest state.
- 36Network audit: the permissive-port decision still open plan/issues/network-architecture-audit-2026-07-09.md#L531-L533
**STILL OPEN as a decision**, with the false claim removed and the agreement now enforced
- 37Denied runtime traffic gets a TCP reset rather than a 403 images/proxy/squid.conf#L148-L153
Send a TCP reset instead of an HTTP 403 error for strictly-denied runtime traffic.
- 38The spec still promises an HTTP 403 for denied domains openspec/specs/proxy-container/spec.md#L109-L109
- **AND** all other domains SHALL be denied with HTTP 403
- 39The owner-only clamp on the CA key, in the stable release's source crates/tillandsias-headless/src/main.rs#L3408-L3410
fn enforce_ca_key_mode(key: &Path) -> std::io::Result<()> { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(key, std::fs::Permissions::from_mode(0o600))
- 40On non-unix targets the clamp is a documented no-op crates/tillandsias-headless/src/main.rs#L3428-L3430
This is deliberately NOT a security regression on Windows: the key is not protected by mode bits there in the first place, and the enclave's Windows path receives it as a podman secret rather than through this file.
- 41Pre-fix keys are healed down to owner-only on every pass crates/tillandsias-headless/src/main.rs#L3612-L3614
Heal DOWN to 0600 every call so keys generated before this fix (deliberately world-readable 0644) are repaired without requiring a CA rotation.
- 43State root is persistent and explicitly shared between consumers images/default/ca-path.txt#L1-L83
${HOME}/.local/state/tillandsias
- 45The planned fix: make the CA directory private by construction plan/index.yaml#L26013-L26013
CA material is created in a directory that is private by construction
- 46The authenticated push lane is gated on an environment variable that defaults to off crates/tillandsias-headless/src/main.rs#L13186-L13188
std::env::var("TILLANDSIAS_MIRROR_SSHD") .map(|v| v == "1") .unwrap_or(false)
- 47The mirror's sshd starts only behind that flag images/git/entrypoint.sh#L524-L525
Behind TILLANDSIAS_MIRROR_SSHD=1 until the T11 staged migration flips the default.
- 48The relay wires the GitHub credential helper for any HTTPS origin images/git/relay-refs.sh#L154-L183
case "$REMOTE_URL" in https://*)
- 49The helper checks exact GitHub hosts and refuses others images/git/git-credential-tillandsias.sh#L69-L83
github.com|api.github.com) ;;
- 50The vault container carries an embedded SELinux policy module crates/tillandsias-headless/src/vault_bootstrap.rs#L4091
const VAULT_SELINUX_CIL: &str = include_str!("../../../images/selinux/vault_container.cil");
- 51The Windows guest embeds policies for the control daemon and the vault crates/tillandsias-windows-tray/src/wsl_lifecycle.rs#L311
const SELINUX_VAULT_TE: &str = include_str!("../../../images/selinux/tillandsias_vault.te");
- 52On a rootless Linux host the vault too runs unconfined crates/tillandsias-headless/src/vault_bootstrap.rs#L4190
`label=disable` runs the vault container unconfined on the host
- 53The vault launch adds a capability back and omits `--rm` crates/tillandsias-headless/src/vault_bootstrap.rs#L4407-L4410
"--cap-drop".into(), "ALL".into(), "--cap-add".into(), "IPC_LOCK".into(),
- 54A test-quality check that examines only new tests plan/index.yaml#L14455-L14455
the check is DIFF-SCOPED BY CONSTRUCTION (examines added steps in the outgoing change only) and is structurally incapable of flagging the existing corpus
- 55Serialising a launch now refuses an argv that violates the envelope, in every build profile crates/tillandsias-podman/src/container_spec.rs#L424-L425
crate::policy::validate_launch_argv(&argv)?; Ok(argv)
- 56The attached forge launch refuses on a hardening violation crates/tillandsias-headless/src/main.rs#L14990
"refusing to launch {container_name}: hardening envelope violation: {err}"
- 57The delegated launch path refuses too crates/tillandsias-headless/src/main.rs#L14818
[security] refusing to launch delegated {container_name}: {err}
- 58The rewritten litmus derives its launch flags from the product's declared envelope openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L23-L25
So the launch argv is now DERIVED from the product's own declared envelope, `MANDATORY_HARDENING_FLAGS` in crates/tillandsias-podman/src/policy.rs
- 59The ninth step, cleanup, still prints its token unconditionally openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L131-L134
expected_behavior: "CLEANED"
- 60A second scanner refuses litmus steps that print the same token on both branches scripts/check-litmus-steps-can-fail.sh#L5-L6
Refuse a litmus step whose success and failure branches print the SAME token, because such a step passes whether the system works or not.
- 61The build gate fails on it build.sh#L5330
a litmus step prints the same token on success and failure
- 62The second scanner refuses only the identical-token shape, by design scripts/check-litmus-steps-can-fail.sh#L26-L30
Only identical tokens are refused, because only then is the branch a decoration.
- 63Private flags initialized on; serialization remains conditional crates/tillandsias-podman/src/container_spec.rs#L134-L137
userns_keep_id: true, cap_drop_all: true, no_new_privileges: true, label_disable: true,
- 64A host-browser container still launches through the unchecked serialiser crates/tillandsias-headless/src/main.rs#L14408
let args = spec.build_run_args();
- 65The repo's own admission that enforcement depends on call order crates/tillandsias-headless/src/main.rs#L14799-L14801
An invariant that depends on call order is one refactor from being false, and this one decides whether a container runs unhardened.
- 66The one shared reader of the flag: a blank value is refused crates/tillandsias-control-wire/src/secure_wire_mode.rs#L85-L126
is set but empty. Blank does NOT mean
- 67Windows tray uses shared mode reader and refuses bad values crates/tillandsias-windows-tray/src/hvsocket.rs#L74-L87
let mode = match tillandsias_control_wire::secure_wire_mode::secure_wire_mode() { Ok(mode) => mode, Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, err)), };
- 68A build check allows no independent reader of the secure-wire setting scripts/lua/check-secure-wire-single-reader.lua#L6-L8
BASELINE is 0 today: every reader was converted (972-umik commit B), so the ratchet is now effectively a refusal.
- 69Test pins the encrypted default through the shared parser crates/tillandsias-control-wire/src/secure_wire_mode.rs#L159-L165
parse_secure_wire_mode(Err(VarError::NotPresent)).unwrap(), SecureWireMode::On,
- 70The Windows guest's service unit carries no secure-wire line crates/tillandsias-windows-tray/src/wsl_lifecycle.rs#L1938-L1939
Environment=TILLANDSIAS_VAULT_API_BASE_URL=https://vault:8200 {low_power_env}ExecStart=/usr/local/bin/tillandsias-headless --listen-vsock 42420
- 71The VM image's default service unit carries no environment at all images/vm/bootstrap/20-tillandsias.sh#L92-L104
ExecStart=/usr/local/bin/tillandsias-headless --listen-vsock 42420
- 72The Linux lane asserts transitive coverage as a property over what is staged .github/workflows/release.yml#L320-L322
This asserts the PROPERTY over whatever is actually staged, so an asset added later cannot slip through unverifiable just because nobody added a line for it.
- 73The macOS lane signs an allow-list of three assets, and its checksum manifest is not among them .github/workflows/release.yml#L562
for artifact in tillandsias-tray-*-macos-arm64.tar.gz Tillandsias.dmg install-macos.sh; do
- 74The Windows lane's coverage check, why it follows the transitive path, and the allow-list it blames for an installer shipping bare .github/workflows/release.yml#L863-L864
The check follows the TRANSITIVE path rather than counting unsigned files
- 75The Linux installer fetches the manifest over the same channel and continues without it scripts/install.sh#L264
say "SHA256SUMS not available; skipping checksum verification."
- 76The Windows installer compares a hash from the fetched manifest only scripts/install-windows.ps1#L746
Say "Verifying SHA-256..."
- 77The macOS installer likewise scripts/install-macos.sh#L162-L162
curl -fsSL "$SHA_URL" -o "$TMP/SHA256SUMS-macos"
- 78The shipped verifier uses the same repository-name-only regexp scripts/verify.sh#L24-L25
CERTIFICATE_IDENTITY_REGEXP="https://github.com/.*/tillandsias/"
- 79The in-app updater's spec: artifact selection, no verification requirement openspec/specs/update-system/spec.md#L1-L53
The updater SHALL select the correct artifact for the current platform and handle platform-specific execution constraints.
- 80Sigstore/in-toto provenance studied for another channel plan/issues/homebrew-harness-distribution-research-2026-07-11.md#L27-L31
every bottle built by Homebrew CI carries a **Sigstore/in-toto build-provenance attestation** (SLSA Build L2)
- 81The Linux soft reset keeps the Vault store and both keyring entries crates/tillandsias-headless/src/main.rs#L10559-L10559
the keyring entries vault-shamir-share-v1 and vault-root-token-v1 — never cleared by a reset
- 83Where no keyring is available the share is written to a fallback file in the cache directory crates/tillandsias-headless/src/vault_bootstrap.rs#L4907
using fallback file (expected in VM guest and headless environments)
- 84Inside a VM guest both fallback writes are always attempted crates/tillandsias-headless/src/vault_bootstrap.rs#L2873-L2874
BOTH writes are always ATTEMPTED — a token failure must never skip the share, because the share is the half that arms the wipe.
- 85The "share present in keychain" check falls through to the file crates/tillandsias-headless/src/vault_bootstrap.rs#L2943-L2944
Fallback: file (populated by keychain_set_blocking when keyring unavailable, e.g. in a VM guest or headless environment without D-Bus)
- 86The preserve guard and the log line the smoke runs observed crates/tillandsias-headless/src/vault_bootstrap.rs#L4297
[tillandsias-vault] preserving existing data volume (Shamir share present in keychain)
- 87Spec: every persistent on-disk copy of the share is deleted immediately openspec/specs/tillandsias-vault/spec.md#L132
delete all persistent on-disk copies immediately
- 90The Podman client runs whatever argv it is handed, with no policy call crates/tillandsias-podman/src/client.rs#L1011-L1013
let mut full_args = vec!["run".to_string()]; full_args.extend_from_slice(args); match self.execute(OperationKind::Container, &full_args).await {
- 91Why the CA directory moved: /tmp is volatile by design and swept on reboot images/default/ca-path.txt#L36-L38
/tmp works everywhere and is the bug — volatile by design, cleared on reboot and swept by systemd-tmpfiles
- 92Why label=disable: no `:z`/`:Z` relabelling, so bind mounts work under SELinux hosts openspec/specs/podman-orchestration/spec.md#L110-L110
no `:z` or `:Z` suffix is needed because `--security-opt=label=disable` disables SELinux confinement for the container process
- 93The macOS host writes the secure-wire flag into the guest's service unit crates/tillandsias-vm-layer/src/vz.rs#L1269
Environment=TILLANDSIAS_SECURE_CONTROL_WIRE=__SECURE_CONTROL_WIRE__
- 94Why the flag is applied — SELinux relabelling would otherwise break the bind mount docs/audits/browser-isolation-audit-2026-04-30.md#L58
| `--security-opt=label=disable` | ✅ | Required for Silverblue bind mounts |
- 95The observatorium web launch serialises without the policy call crates/tillandsias-headless/src/main.rs#L14359
.build_run_args()
- 96The proxy launch is assembled as a raw argv the checker never sees crates/tillandsias-headless/src/main.rs#L3787
fn build_proxy_run_args(certs_dir: &Path, image: &str) -> Vec<String> {
- 97The mirror launch is assembled the same way crates/tillandsias-headless/src/main.rs#L4933
fn build_git_run_args(
- 98What the checker refuses beside a missing flag crates/tillandsias-podman/src/policy.rs#L189-L193
if arg == "--privileged" || arg.starts_with("--privileged=") || flag_value(arg, next, "--userns").is_some_and(|value| value != "keep-id") || flag_value(arg, next, "--cap-add").is_some_and(|value| value == "ALL")
- 99The gate's pass token names the tree it checked plan/archive/packets-2026-08.yaml#L48763
a green gate now issues a one-shot pass token naming the tree it validated
- 100Ownership probe distinguishes keep-id from misleading inspect labels openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L41-L64
The bind-mounted file discriminates — 1000 with keep-id, 0 without
- 101Capability probe ignores command exit status before declaring success openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L111-L114
expected_behavior: "CAPABILITIES_DROPPED"
- 102CA directory creation uses ordinary create_dir_all, followed by refresh decision crates/tillandsias-headless/src/main.rs#L3468-L3469
std::fs::create_dir_all(&certs_dir) .map_err(|e| format!("Failed to create CA directory: {e}"))?;
- 103Host derives the control key from the build-time guest digest crates/tillandsias-secure-channel/src/lib.rs#L157-L182
derive_psk(guest_binary_sha256, build_version, wire_version, hop)
- 104Why single-process tests could not reveal the host/guest digest mismatch crates/tillandsias-secure-channel/src/lib.rs#L205-L228
The only integration proof is two DISTINCT binaries: a release-built tray and the release guest reaching Ready on a real cold provision.
- 105Windows refuses an absent embedded guest digest crates/tillandsias-windows-tray/src/hvsocket.rs#L25-L43
EMBEDDED_GUEST_SHA256.as_ref().ok_or_else(|| {
- 106Windows handshake negative control rejects a different guest digest crates/tillandsias-windows-tray/src/hvsocket.rs#L525-L610
a_guest_digest_mismatch_is_refused_not_carried
- 107Local acceptance follows upstream acceptance, so a reported success is a durable one images/git/pre-receive-hook.sh#L6-L8
Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.
- 109Spec: the share lives only in an unlocking keyring, with no persisted fallback share file openspec/specs/tillandsias-vault/spec.md#L425-L428
There is NO persisted fallback share file: where the keyring is unavailable the share stays in process memory or on tmpfs for the life of the process and is gone with it
- 110The credential clearer is uninstall-only and has no caller yet crates/tillandsias-headless/src/vault_bootstrap.rs#L3524-L3528
UNINSTALL-ONLY (order 1437-qza3, operator directive 2026-09-27). No reset may call this: the store and the unseal material are operator data that survive every destructive reset
- 111The Linux soft reset runs no credential clearer and deletes no store crates/tillandsias-headless/src/main.rs#L10522-L10524
No credential clearer and no store deletion: the store is a host directory the podman reset cannot reach.
- 115Open defect: the Windows installer never verifies a cosign signature plan/index.d/20261008t234534z-1562-leaked-test-creds-and-installer-cosign-yolanda-windows.yaml#L66-L66
install-windows.ps1 checks the downloaded zip against SHA256SUMS-windows but never verifies a cosign signature
- 116Open defect: nothing verifies the install entry point itself plan/index.d/20260922t152446z-1361-pwjz-the-entry-point-verifies-everything-but-itself-pirria.yaml#L23-L24
(a) the install path verifies its own published cosign bundle before executing, or
- 117The mirror hook can refuse pushes by a seed read from the integration branch, when the seed enforces images/git/pre-receive-hook.sh#L169-L177
enforced the push is rejected BEFORE the relay, so nothing reaches upstream
And you would like me to be condescending about it. Very well.Assumes everything before it. Mathematics from here down.
Level 5 — For the MathWiz / Hacker
A monocot bromeliad flowers once and dies, leaving offsets behind — the pup is never the parent.
The lower levels gave you the object and its defects. What is left is the only part that can be attacked with a pencil: the formal content, stated as claims with hypotheses, sorted into (a) standard theorems being invoked, (b) what the repository actually asserts, (c) what follows, and (d) what does not. The security findings of level 4 are not repeated; they enter here only where a mathematical hypothesis depends on them.
The short verdict: the theory is small, finite, and unusually candid about the theorems it declines to claim. Its model now has generative property tests and stable requirement identifiers. Connecting those checks to live validation and justifying dependent iteration remain separate obligations.
The lattice, and the one step worth checking
An obligation is a named, auditable requirement, invariant, litmus signal, trace signal, provenance binding, or environment assumption carrying a stable identifier1. Its state ranges over a finite chain, seven long2:
Fix the convention, since authors differ: the height of a finite poset is the maximum number of strict order steps in a chain — so a chain of \(n\) elements has height \(n-1\), and the one-element poset has height \(0\).
Proposition 1. \(S_{\text{proj}}\) is a finite complete lattice of height \(6|\mathrm{Obl}|\). Each \(\mathcal{O}_i\) is a finite chain, hence a complete lattice; a finite product of complete lattices is complete with \(\vee,\wedge\) computed componentwise; a chain of seven elements has height six and heights add over products3. Nothing here is in dispute — it is the least interesting true thing in the file, and the repository correctly declines to inflate it: "this proves monotonicity of the model, not truth of the modeled requirement or adequacy of the chosen obligation set"4.
Two hypotheses do quiet work. The order is componentwise after aligning stable IDs and tombstones1, so a renamed coordinate yields a different lattice, not a lower point in the same one; and \(|\mathrm{Obl}|\) must be finite and known, which is what makes the height bound a bound rather than an ordinal.
The fixed point, in full
Let \(\mathrm{refine}: S \to S\) be validation followed by semantic distillation at a fixed bar \(B\). The repository's stated claim is idempotence5:
Theorem 2 (Knaster–Tarski). A monotone map on a complete lattice has a complete lattice of fixed points, in particular a least one6. Applied here it gives existence, non-constructively, and says nothing about how to reach it.
Theorem 3 (finite monotone inflationary iteration). If \(f: L \to L\) is monotone and inflationary (\(x \le f(x)\)) on a finite lattice of height \(h\), then for every \(x\) the chain \(x \le f(x) \le f^2(x) \le \cdots\) stabilises after at most \(h\) strict increases, and its limit is the least fixed point \(\ge x\). The bound is immediate: each strict step rises at least one level in a poset of height \(h\). Leastness is a two-line argument worth doing, because it is the step people skip: if \(y = f(y)\) and \(y \ge x\), then monotonicity gives \(f^n(x) \le f^n(y) = y\) for every \(n\), so the limit is \(\le y\).
Note where Kleene's theorem proper — least fixed point as \(\bigsqcup_n f^n(\bot)\) for a Scott-continuous \(f\) on a CPO7 — would otherwise be invoked outside its hypotheses. Continuity is not assumed, it is free: in a finite poset every directed subset is finite and contains its own supremum, so every monotone map is Scott-continuous. That is the first place a referee probes, and it survives.
So validator stability is a computable predicate: \(\mathrm{refine}(x) = x\). It is not completion: the identity operator is stable even with unmet obligations. The methodology makes this distinction explicitly40. The comparison bar \(B_v\) is fixed within a release, rising only by explicit operator decision, with the automation forbidden from self-escalating it8. A continuously rising bar makes \(\mathrm{refine}\) a moving operator and destroys stabilisation; discrete, externally gated raises preserve it. That is the standing hypothesis of Theorem 3, not a premise that forces every obligation to close.
And now the gap. Theorem 3 needs monotone and inflationary; the repository asserts idempotent. These are the three axioms of a closure operator, and the repository states one of them. Idempotence does not imply monotonicity: a map that sends \(a \mapsto \top\), \(b \mapsto b\) with \(a < b\) is idempotent and not monotone. Nor does it imply inflationarity. The validation program that would discharge the missing two is written down — enumerate the allowed transitions and property-test that each is order-preserving for every affected obligation9 — which is the right shape of obligation. That program now has executable tests over a committed rule set; it does not establish the same properties for every live validator41.
known limitation The refinement model has generative tests for monotonicity, inflationarity and idempotence, but they exercise a committed rule set rather than every live validator41. They establish a model boundary, not semantic correctness of the running system.
what happens next The staged validation program names the remaining obligation: enumerate permitted transitions and check every affected coordinate, treating tombstones and scope changes separately9.
checked Requirement identifiers now exist and a gate checks presence and uniqueness, including tombstoned specifications42. Whether an edit preserves meaning and should keep its identifier remains author judgement; the guard cannot decide that. The evidence-credit table still lists stable identifiers as a scoring input10, which by itself does not establish that credit is computed.
checked The negative results — no contraction, no Galois connection, no probabilities — are first-class claims carrying their own discharge conditions, not caveats appended to positive ones. Each names the object a future claim would have to construct.
The moving target, and what would force the floor to zero
Write \(T_v\) for the specifications at version \(v\), \(I_v\) for the implementation, \(d_v = d(I_v, T_v) \ge 0\). Because the specifications evolve, \(T\) moves and no final target exists11. The mechanically checkable release rule is non-increase.
Proposition 4 (monotone convergence). A non-increasing real sequence bounded below converges to its infimum12. Here:
That is the whole result, and it is correct.
What does not follow is \(d_* = 0\), and the repository says so in those words11. A sufficient extra hypothesis is a uniform progress premise, in either of two forms:
Either excludes positive residual fixed points and forces \(d_* = 0\) — the additive form in finitely many steps, the multiplicative one geometrically. The repository's phrase — "a validated progress premise that excludes positive residual fixed points"11 — can be discharged by either sufficient condition; neither is established here as a fact about the project.
What CentiColons make measurable
A concrete residual can replace an unspecified distance without becoming a metric in the sense needed by Banach. Fix a finite obligation set \(S\), positive integer weights \(w_i\), and a versioned closure predicate \(C_i(x) \in \{0,1\}\) requiring the declared evidence for obligation \(i\). A proposed binary accounting contract is
This is a measurable residual of declared, unmet evidence obligations, not all uncertainty about the program. Its unit, scope and evidence policy must stay fixed across a comparison. Preserving each retained obligation's credit implies non-increase of \(R_S\); non-increase of the total does not imply preservation of each credit, because one gain can hide another loss. New obligations, retirements and evidence invalidation must be reported explicitly rather than called progress.
The termination statement now has a precise price: if there is no regression and, whenever \(R_S>0\), a decrease of at least one occurs within the next \(K\) cycles, then \(R_S=0\) within at most \(K R_S(x_0)\) cycles. There are at most \(R_S(x_0)\) such decreases to wait for. This is a conditional cycle bound, not a wall-clock bound or a proved property of the project. Non-increase alone permits \(R_S=10\) forever.
Lua makes extraction and grading executable. That closes part of the measurement gap, not the progress premise. At this release the Lua grader reports an unweighted count at the recorded positive-test bar47, while the methodology declares a richer weighted policy15. The regression reporter is advisory and stores an untracked, per-checkout baseline, not a durable accepted-release baseline48. The capability split between cacheable and observing predicates46 also cannot prove that an assertion tests the intended behavior or that a pass belongs to the current implementation and every required platform. A pure grader can give the same wrong credit on every evaluation.
known limitation CentiColons make declared residuals observable, but the stable release does not yet establish enforced monotonic reduction under a complete evidence contract. The observed grader selects results by test name and test digest, not required platform; its spec-digest check is conditional and it does not match implementation inputs47. Scenario candidates are shared at requirement level49.
what happens next Fix assertion, subject and platform attribution; align the scoring policy; then pilot a durable per-obligation non-regression check before enabling enforcement. Invariant extraction is separate from the current grading loop50. This is the proposed program, not a claim that it has shipped; strict progress remains a separate hypothesis even after these checks exist.
The Banach alternative, itemised. A contraction claim13 would owe a complete metric space \((X,d)\), an operator \(F: X \to X\), and a constant \(0 \le c < 1\) with
Missing: (i) a metric, (ii) completeness, (iii) any bound on \(c\). The repository lists all three14. The binding one is (i), and it is worse than "unproven". \(d\) as used is not a function on \(X \times X\) at all — it is only ever evaluated at the single pair \((I_v, T_v)\), giving a sequence of numbers indexed by release, not a binary function on a set. Ask which axiom fails and the honest answers are:
The first two are undefined rather than false, because \(d(x,y)\) for two arbitrary project states is never given a value; identity of indiscernibles fails outright, since two materially different implementations can close the same obligation set and score residual zero. Absent (i), items (ii) and (iii) are not merely unproven — they are not well-posed. Until a metric exists, "monotonic convergence" means ordered non-regression plus finite residual descent, which is what the file claims and no more14.
Be careful about the scope of that verdict, because it is easy to overstate and I will not. Everything above concerns the \(d\) the repository actually documents. It is not an argument that no suitable metric exists on project states, and nothing here forecloses one: a genuine metric — with a completion, and a contraction constant — may well be constructible, and if it were, the Banach route would reopen exactly as written. The claim is the narrow one. The contraction argument cannot be discharged by the object currently on offer, and the work of building a better one is open rather than blocked.
There is a further boundary. Under a fixed denominator, a non-increasing integer residual is eventually constant; strict progress while positive would force zero. Changing the obligation scope breaks that comparison. A ratio alone cannot repair it: adding easy obligations can improve the percentage without closing an old gap.
what happens next The validation program requires denominator changes to be reported separately9. A complete cross-release comparison must align the obligation set and weights; no general normalisation rule is established by the cited implementation.
A repository refinement is a model, not a theorem
Here is the engineering picture in a form precise enough to attack. Let \(R_k\) be the repository state visible to a task, \(p_k\) its prompt and \(C_k\) the selected context built from source, rules, retrieved material and task state. A model proposes a delta \(Delta_k\); review and validators decide whether to commit it:
"Vectors" is a useful picture of the representations the model constructs from \(C_k\); it is not Git's data model. A commit can retain reachable history while its new tree replaces or deletes files. Nor is an accepted patch automatically an improvement. The acceptance step is where tests, review and evidence enter; the finite obligation model says exactly what those validators can record, and no more.140
For a workstream \(i\) with a fixed specification \(T_i\), write \(d_i(R_k,T_i)\) for its declared, measured residual. Accessibility and language, marketing, dashboards and customer support may each have their own \(d_i\). A shared residual is meaningful only after the obligation identities, weights and scope have been aligned; tombstoning already marks the shipped scorer's comparison as broken.4445 Thus the diagram's local traces are engineering telemetry, not samples from a common distribution.
not yet shown Repeated small, checked refinements can reduce observed local residuals faster than one large unreviewed change, while commits preserve an inspectable history of accepted evidence. This is a useful operating method, not a proof that the residual reaches zero.
The tempting probabilistic upgrade is to treat \(Delta_k\) as one random refinement with a finite-step skew, then invoke a weak or strong law over many refinements. That argument needs a stochastic model of the quality functional, a target mean and conditions on the joint process. Large context windows, high-dimensional embeddings and a fresh prompt do not supply independence. The next input contains \(R_{k+1}\), which was made using the previous output; this is feedback, not a negligible detail. The possible formal routes are the ones below: establish a dependence structure that supports an ergodic or martingale theorem, or leave the claim empirical.161819
Iteration: the law of large numbers at full strength
The methodology now withdraws its strong-law citation for dependent iterations16. The mathematics below explains why that correction matters. Set it up properly. Let \((X_k)_{k\ge1}\) be random elements on a probability space, \(X_k\) the value of a real quality functional on iteration \(k\), with \(\mathbb{E}|X_k| < \infty\) and \(\mathbb{E}[X_k] = \mu + b_k\), where \(b_k\) is the per-iteration skew. Write \(\bar{X}_N = N^{-1}\sum_{k \le N} X_k\) and \(\bar b = \lim_N N^{-1}\sum_{k\le N} b_k\) when that limit exists.
Theorem 5 (Kolmogorov's SLLN). Under independence, identical distribution, and integrability17:
The iterations here are not identically distributed, so a candidate generalisation is Kolmogorov's variance criterion, which drops identical distribution and keeps independence18:
Same family as the three-series theorem; independence and the summable-variance condition are the whole price of admission.
Weak versus strong is convergence in probability versus almost-sure convergence; almost-sure convergence implies convergence in probability, but the converse need not hold. Under the criterion above and existence of the mean-bias limit, bounded bias bounds the limiting error. Bounded bias alone does not imply convergence, and unbounded skew does not by itself prove divergence. The methodology retains small, fast iterations as engineering guidance while withdrawing the almost-sure claim16.
The objection the methodology now acknowledges is the load-bearing one. Every form of Theorem 5 quoted above requires independence. Iterations of an agent that reads its own prior output are not independent: \(X_{k+1}\) is a measurable function of \(X_k\) and the accumulated context. The architecture makes the dependence explicit and deliberate — retrieval is a cache of converged knowledge, updated on commits, with commits acting as a Lamport clock ordering the corpus against the code19. That is a feedback loop by construction, and it invalidates every independence hypothesis in the section above.
Two possible substitutes remain unestablished. Either the sequence is stationary and ergodic and one invokes Birkhoff's pointwise ergodic theorem20 — but stationarity is implausible for a process whose corpus grows monotonically, and ergodicity is exactly what fails if the chain can be absorbed. Or the increments form a martingale difference sequence with respect to the natural filtration \(\mathcal{F}_k = \sigma(X_1,\dots,X_k)\), giving an \(L^2\) or Azuma-type law — but that requires
which is precisely the assertion that yesterday's output does not bias today's, and the cache design says it does. Under positive feedback an absorbing state is available: the stream converges almost surely to whatever the loop locked onto, and \(\bar X_N\) converges beautifully to a number with no relation to \(\mu\). Bounding per-prompt skew does not repair this; it is a hypothesis about marginals, and the failure is in the joint.
Be precise about what this objection kills. It does not show the iteration scheme is wrong, and it does not show the stream fails to converge — small fast iterations may well be the right engineering, and dependent sequences converge under plenty of other hypotheses. What it kills is the invocation of the strong law as stated: the theorem named is being applied to a sequence that does not satisfy its independence hypothesis, so the almost-sure conclusion is unearned rather than false. The repair is cheap in words and real in work — name the dependence structure, then cite the theorem that covers it.
checked The methodology explicitly withdraws the strong-law citation because the cache makes successive iterations dependent16. It names ergodic and martingale hypotheses as work needed for a future theorem, and presents the iteration practice as an empirical track record rather than an almost-sure guarantee.
What the repository renounces, and what that costs
No Galois connection. Specs and scores are described as abstractions that deliberately forget detail — the Cousot framing21 — and the file concedes that no adjunction is defined between program semantics and spec obligations22. State the missing object precisely. With \(C\) the lattice of sets of concrete executions and \(A\) the obligation lattice, a Galois connection \(\alpha \dashv \gamma\) requires monotone \(\alpha: C \to A\), \(\gamma: A \to C\) with
What is forfeited is the transport theorem, and only that — but that is everything. Given the adjunction and a sound abstract operator (\(\alpha \circ F \sqsubseteq F^\# \circ \alpha\)), one gets \(\alpha(\mathrm{lfp}\,F) \sqsubseteq \mathrm{lfp}\,F^\#\): a fixed point computed in the abstract over-approximates the concrete, so a property proved upstairs holds of every concrete execution downstairs. Without it, closure in the obligation lattice does not inherit that transfer theorem. Tests can still provide empirical evidence about the executions they observe; they do not thereby establish a sound abstract interpretation of all program behavior.
Credit means reaching an obligation's configured evidence bar, not necessarily the top of the lattice: a passing CI check does not imply runtime observation or bundled evidence51. Applicable, discriminating tests add evidence about behavior; closure alone still proves neither that the obligation set is adequate4 nor that all concrete executions satisfy it22.
A ranking function, not a measure. The CentiColon map \(c: S_{\text{spec}} \to \mathbb{N}\) is bounded, with separately reported denominator and residual, and monotone only for transitions preserving obligation IDs and introducing no penalties, ambiguity, or denominator scope change1. Earned credit ascends; a residual derived from it can be a Floyd-style descent witness23 only with an additional strict-progress premise. Boundedness and non-increase alone do not prove termination.
It is not a measure. There is no \(\sigma\)-algebra on \(S_{\text{spec}}\); \(c\) is not additive, because obligations overlap and sixteen penalties plus six cap rules make the rollup non-linear in its parts15; and the normaliser is policy rather than a fixed total mass. A bounded ranking function into a finite ordinal therefore induces no probability, and the repository says so outright24. What that forecloses is concrete: two scores may not be combined by Bayes; there is no complement rule, so 89% closed is not an 11% chance of a defect; and no calibration statement is available, because there is no event space on which to calibrate. Shafer25 and Walley26 are cited as the layer a confidence model would have to occupy separately — correctly, since a belief function would at least supply the monotone-capacity structure that \(c\) lacks.
That is a statement about \(c\) as defined, and about nothing more. It does not show that no transformation, extension or added structure could turn part of this model into a measure or another probabilistic object — the citation of belief functions is precisely an admission that such a layer is available to be built. What is claimed is that \(c\) does not already carry that structure, so probabilistic language applied to today's scores is unlicensed. Building the layer that would license it is open work, and the repository files it as such.
known limitation The shell scorer now delegates arithmetic to the obligation model43, but still supplies weights over CI check names27. A passing check earns its weight at the positive-test bar; it is not runtime observation or bundled evidence. This wiring does not implement the full methodology table of multipliers, evidence credits, caps and penalties15. The committed dashboard remains a historical 890/990 snapshot28, not a measurement of this release.
what happens next The framework specification still names a separate scoring implementation29. The shipped path centralises the arithmetic; completing and evidencing the richer scoring contract remains distinct work. Lua adds scenario/requirement extraction and recorded positive-test grading, not yet integrated invariant credit or full evidence attribution474950. The advisory residual and the weighted CI score are distinct projections, not interchangeable units.
known limitation The methodology's own complexity constraint — methodology-to-codebase ratio below 0.15, with a red flag at 5000 lines of CI validators30 — has no enforcing check that we found, while the script that computes the score dispatches a large shell validator corpus.
what happens next The rule names two measurement procedures30; no enforcing implementation was found in the scripts searched.
CRDTs as algebra
The plan ledger is a genuine convergent replicated data type. State is \(\text{base} \oplus \mathrm{fold}(\text{fragments})\); the carriers are two join-semilattices31: grow-only sets of packets and of events keyed by \((\text{packet id}, \text{event identity})\), with \(x \sqcup y = x \cup y\); and last-writer-wins registers per \((\text{packet id}, \text{field})\), the winner chosen by the total order \((\text{timestamp}, \text{host})\).
The algebra: a commutative, associative, idempotent \(\sqcup\) induces a partial order \(x \le y \iff x \sqcup y = y\) under which \(x \sqcup y = \sup\{x,y\}\) — the join is the least upper bound, which is why the three properties are a definition rather than a checklist. Convergence follows because the fold is then a function of the set of delivered updates, not their sequence: idempotence kills duplicates, commutativity and associativity kill order.
State exactly what that buys, because it is strictly less than the folklore suggests32. It gives: replicas having delivered the same update set hold equal state. It does not give delivery — liveness is someone else's problem; it does not give correctness of the converged value; and it does not preserve concurrent intent, which is where LWW does its damage. Applying LWW to a list silently discards the loser's entries, which is why events are a set and not a register — a distinction the file calls the whole correctness argument31. Determinism is pinned by folding in \((\text{timestamp}, \text{filename})\) order rather than directory order33, and two of the three properties — commutativity and idempotence — are tested by name34.
The status field is not a plain register: it composes a monotone join over a closure rank with LWW as tiebreak at equal rank, treats terminal-but-lateral values as non-ladder moves, and permits descent only under an explicit falsification flag35 — a lexicographic join with a deliberate non-monotone escape hatch, so that monotonicity cannot force retention of a certainty later shown false.
checked The repository retracts its own CRDT claim where it fails, and names the property that failed: a deduplication routine's header states plainly that it is a seen-set dedup, not a CRDT, because first-wins keeps whichever duplicate arrives first, so commutativity is false36. Most projects keep the word.
checked Componentwise maximum gives version tuples a join-semilattice. The document uses that algebra to motivate mergeable version coordinates37. Its claim that SemVer has no natural total order is too broad: version precedence and preservation of branch causality are different properties.
checked Semantic merges are typed honestly as "semantic cache with CRDT preconditions" rather than as CRDTs, with the anti-pattern named: calling a lossy cache a CRDT manufactures false convergence claims32. The claim registry holds the lineage at strength "external analogy" until generative property tests exist38.
known limitation That same versioning document defines its two leading components as a contract version and a feature phase39. The live scheme is a temporal anchor throughout — years since epoch, month, day, build. The join algebra survives untouched, since componentwise max does not care what the coordinates mean; the documented semantics of two of four components are false.
what happens next The release-shape test was corrected; the versioning document still gives the old meanings.
Verdict
The formal core is a finite product lattice, a finite monotone-inflationary stabilisation under declared validators at an operator-gated bar, a candidate residual ranking function, and a moving-target result whose zero-floor corollary needs a progress premise. A metric is a separate missing hypothesis of the Banach argument. Contraction, Galois connections and probabilistic readings are named absent rather than assumed40. As a thesis position that is defensible, and I did not expect to write that sentence.
Two boundaries remain. Generative tests cover the committed refinement model, not the semantic correctness of all live validators. And dependent iteration still lacks a convergence theorem, although the methodology now retracts the unsupported strong-law claim. Stable identifiers and centralised score arithmetic make the bookkeeping more concrete; they do not turn closure into probability or a dashboard snapshot into release evidence. CentiColons supply an executable accounting direction; complete attribution, an aligned scoring policy and an enforced durable comparison are still needed for the non-regression guarantee. The stated progress premise suffices for eventual zero and the cycle bound.
How we know — 51 sources
Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.
- 1Formal objects — obligation, the state chain, product lattices, and the ranking function's monotonicity conditions methodology/math-foundations.yaml#L13-L43
A named, auditable requirement, invariant, litmus signal, trace signal, provenance binding, or environment assumption with a stable ID.
- 2Stated purpose — separating defensible mathematics from analogy methodology/math-foundations.yaml#L4-L11
Separate the math the methodology can defend from analogies it must not overclaim.
- 3Davey & Priestley, *Introduction to Lattices and Order*, 2nd ed. — product orders, heights, completeness doi.org/10.1017/CBO9780511809088
- 4The lattice-model claim and its stated limit methodology/math-foundations.yaml#L46-L59
This proves monotonicity of the model, not truth of the modeled requirement or adequacy of the chosen obligation set.
- 5The fixed-point claim: idempotence of refine, and its limit methodology/math-foundations.yaml#L61-L74
Define the refinement operator over a finite artifact snapshot and check idempotence: refine(refine(state)) == refine(state).
- 6Tarski, *A lattice-theoretical fixpoint theorem and its applications*, Pacific J. Math. 5 (1955) — the Knaster–Tarski theorem, generalising Knaster's 1928 powerset case doi.org/10.2140/pjm.1955.5.285
- 7Kleene, *Introduction to Metamathematics* (1952), cited for the iterative least-fixed-point construction archive.org/details/introductiontome00klee
- 8Bar-raise governance — the bar is fixed within a release, rises only by operator decision, and the automation must not self-escalate methodology/convergence.yaml#L502-L504
is a one-off scope expansion that The Tlatoāni MUST approve every time. Recurring automation (the meta-orchestration loop) MUST NOT self-escalate the bar.
- 9The staged validation program: the monotonicity property tests and the denominator scope-change check methodology/math-foundations.yaml#L175-L198
- allowed_evidence_transitions_are_monotone - tombstone_and_scope_change_transitions_are_explicitly_non_monotone
- 10The evidence-credit table containing `requirement_has_stable_id` methodology/proximity.yaml#L88-L97
requirement_has_stable_id: 0.10
- 11Multi-version convergence: the moving target, the residual floor, and the refusal of the zero-floor claim methodology/philosophy.yaml#L134-L137
Release-over-release non-increase of d_v guarantees convergence to some residual floor d_* >= 0; it does not by itself prove d_* = 0. A zero-floor claim would additionally require a validated progress premise that excludes positive residual fixed points
- 12Rudin, *Principles of Mathematical Analysis*, 3rd ed., Thm. 3.14 — monotone bounded sequences converge archive.org/details/principlesofmath0000rudi
- 13Banach, *Sur les opérations dans les ensembles abstraits*, Fund. Math. 3 (1922) doi.org/10.4064/fm-3-1-133-181
- 14Contraction explicitly not claimed, with the metric/operator/constant debt itemised methodology/math-foundations.yaml#L108-L120
Without that metric proof, "monotonic convergence" means ordered non-regression plus finite residual descent, not metric contraction.
- 15The cap rules, the sixteen penalties, and the rollup that makes the score non-additive in its parts methodology/proximity.yaml#L100-L135
requirement_cc = weighted_obligation * earned_credit + penalties bounded to [0, weighted_obligation].
- 16The strong-law citation withdrawn for dependent iteration methodology/philosophy.yaml#L29-L47
So the almost-sure claim is not available as stated and is withdrawn rather than weakened.
- 17Kolmogorov, *Grundbegriffe der Wahrscheinlichkeitsrechnung* (1933) — the strong law and its variance criterion doi.org/10.1007/978-3-642-49888-6
- 18Durrett, *Probability: Theory and Examples*, 5th ed. — SLLN, Kolmogorov's three-series theorem, and the ergodic and martingale substitutes doi.org/10.1017/9781108591034
- 19Retrieval as a cache; commits as the Lamport clock versioning it — the mechanism that makes iterations dependent methodology/philosophy.yaml#L54-L57
That cache is updated on COMMITS — a commit hitting the relevant bits retrains the RAG. Commits are therefore the LAMPORT CLOCK of the RAG models: they causally order and version the cached knowledge against the code
- 20Birkhoff, *Proof of the ergodic theorem*, PNAS 17 (1931) doi.org/10.1073/pnas.17.2.656
- 21Cousot & Cousot, *Abstract Interpretation*, POPL 1977 doi.org/10.1145/512950.512973
- 22The concession: no Galois connection is defined — "an abstraction discipline, not a formal abstract interpreter" methodology/math-foundations.yaml#L76-L89
Tillandsias does not yet define a full Galois connection between program semantics and spec obligations. Until then, this is an abstraction discipline, not a formal abstract interpreter.
- 23Floyd, *Assigning Meanings to Programs* (1967), cited for ranking-style progress reasoning doi.org/10.1090/psapm/019/0235771
- 24Scores are not probabilities; finite coverage is not proof of absence methodology/math-foundations.yaml#L122-L135
Evidence bundles and CentiColon scores should not be interpreted as probabilities. If probabilistic or belief-function confidence is later added, it must be a separate layer from obligation closure.
- 25Shafer, *A Mathematical Theory of Evidence* (1976), cited as a possible separate confidence layer press.princeton.edu/books/paperback/9780691100425/a-mathematical-theory-of-evidence
- 26Walley, *Statistical Reasoning with Imprecise Probabilities* (1991) doi.org/10.1007/978-1-4899-3472-7
- 27The shell still supplies weights over CI check names scripts/local-ci.sh#L413-L418
check_weight() { case "$1" in spec-cheatsheet-binding) echo 100 ;; spec-code-drift) echo 120 ;; spec-trace-coverage) echo 90 ;; version-monotonicity) echo 40 ;;
- 28The committed dashboard's earned and total figures docs/convergence/centicolon-dashboard.json#L75-L76
"total_cc": 990, "earned_cc": 890,
- 29The framework specification names its intended scoring implementation methodology/litmus-framework.yaml#L88-L96
files: - crates/tillandsias-litmus/src/convergence/mod.rs - crates/tillandsias-litmus/src/convergence/centicolon.rs
- 30The uninstrumented complexity constraint and its 5000-line red flag methodology/convergence.yaml#L420-L424
ratio_constraint: "methodology_complexity / codebase_complexity < 0.15" anti_pattern: > A validation system that requires more code to understand than the code being validated. Red flag: CI validators exceed 5000 lines or require specialized training to understand.
- 31The three CRDT primitives, and why each field uses the one it does crates/tillandsias-plan/src/fragments.rs#L28-L39
Applying LWW to a LIST would silently discard the loser's entries, which is why events are a set and not a register.
- 32Semantic merge typed as a cache with CRDT preconditions, plus the anti-pattern methodology/cheatsheets.yaml#L88-L114
Calling a lossy semantic cache a CRDT without stable IDs, tombstones, deterministic merge, and property tests creates false convergence claims.
- 33Determinism rules: fold order and idempotence crates/tillandsias-plan/src/fragments.rs#L41-L49
Fragments fold in `(ts, filename)` order, never directory order — the filesystem does not promise an order, and two hosts folding differently would compute different states from identical inputs, which presents as corruption rather than as a sorting bug.
- 34Commutativity and idempotence of the fold pinned as named tests crates/tillandsias-plan/src/fragments.rs#L3229-L3230
fn the_fold_is_commutative_the_defining_crdt_property() { Order of arrival must not change the result.
- 35Rank-aware status join with a falsification escape hatch crates/tillandsias-plan/src/fragments.rs#L386-L400
The closure ladder implemented<completed<verified<done is a monotone lattice: you climb UP freely and move DOWN only through a `falsified` event.
- 36A retracted CRDT claim, with the failed property named crates/tillandsias-plan/lua/collect.lua#L7-L11
This is a SEEN-SET DEDUP, not a CRDT — the earlier header's CRDT claim (commutativity in particular) was false: first-wins keeps whichever duplicate arrives first, so order matters.
- 37The join-semilattice argument for the version scheme, and the SemVer critique methodology/versioning.yaml#L104-L124
SemVer (Major.Minor.Patch) allows arbitrary resets: Patch can go 1→2→1 if someone hotfixes a branch. There is no natural total order.
- 38CRDT preconditions filed at claim strength "external analogy" methodology/provenance.yaml#L167-L180
claim_strength: external_analogy
- 39The documented component semantics the project retired methodology/versioning.yaml#L9-L20
Major: meaning: "Contract version — breaking changes only"
- 40Thesis defence position: finite ordered convergence under declared validators, with unknown-event intake as the escape hatch methodology/math-foundations.yaml#L200-L206
The defensible claim is finite ordered convergence under declared validators: stable obligations form a finite lattice; evidence-improving transitions are checked for monotonicity; CentiColons are a bounded ranking function over that model; fixed points mean validator stability; and unknown-event intake is the escape hatch
- 41Generative property tests for monotonicity, inflationarity and idempotence of the refinement operator, over a committed rule set crates/tillandsias-plan/src/obligation_props.rs#L172-L219
fn refine_is_monotone_on_the_real_rules((x, y) in comparable_pair()) {
- 42The gate that refuses a missing or duplicated requirement identifier, tombstoned specifications included scripts/lua/check-requirement-ids.lua#L5-L10
and no identifier appears twice anywhere in the corpus. Tombstoned specs are checked too
- 43The shell scorer hands its weights to the model instead of summing them itself scripts/local-ci.sh#L571-L573
What LEFT is the arithmetic: earned/denominator/residual are now computed by `tillandsias-plan score-checks`, which runs obligation::centicolon_function over a SpecState.
- 44The ranking function in code names which side of the monotone band a score is on crates/tillandsias-plan/src/obligation.rs#L675-L708
Regime::Broken("denominator scope changed: an obligation was tombstoned")
- 45The shell prints non-comparability when the scorer reports a broken regime scripts/local-ci.sh#L624-L624
this score is NOT comparable with the previous run
- 46Cacheable and observing predicate capabilities crates/tillandsias-plan/src/lua_predicate.rs#L74-L95
Pure: no shell, no clock. Results MAY be cached.
- 47Observed grading: latest test/digest record, conditional spec freshness and raw positive-test residual scripts/lua/centicolon-grade-observed.lua#L38-L105
- 48Advisory regression comparison and local snapshot, advanced even after a loss scripts/lua/check-centicolon-ratchet.lua#L4-L138
the advisory CentiColon R line, ported from check-centicolon-ratchet.sh without changing its protocol stdout or status.
- 49Static grading assigns requirement-level candidates to each scenario scripts/lua/centicolon-grade-static.lua#L180-L201
- 50Invariants are emitted separately from scenario/requirement obligations scripts/lua/centicolon-extract.lua#L258-L266
- 51A configured positive-test bar does not witness the whole evidence chain crates/tillandsias-plan/src/obligation.rs#L640-L646
a CI check that passes establishes `PositivelyTested` and says nothing about `RuntimeObserved` or `EvidenceBundled`.


