How things arrive
Packages, records, and why adding software to a computer should execute no code—plus settings as a live CRDT syntax tree.
The previous post covered what a tilo application is. This one is about how one gets onto a machine, along with your settings and, eventually, your documents. Three different kinds of thing, which every desktop platform currently pushes down one or two pipes and gets wrong in a different way each time.
Three platforms, three broken models
On Windows, installing software means finding a website, downloading an exe, and answering a dialog that says the program would like to make changes to your computer. The dialog does not say which changes. It does not bound them, and afterwards there is no way to find out what they were. You have a logo and a feeling, and you click yes, because you wanted the program.
macOS runs the same trick with better manners. Download a dmg, drag an icon into a folder, and the application is installed in the sense that it now exists on disk. Uninstalling is dragging it to the trash, which leaves behind the preference files, the caches, the launch agent, the login item, and whatever helper the app dropped into /Library. There is a small industry of applications whose entire job is cleaning up after other applications, which is a fairly direct admission that the platform never modelled removal at all.
Linux is in genuinely better shape and it is worth being fair about that. A distribution’s package manager is a real distribution model: signed, versioned, with a dependency graph and an uninstall that removes what it installed. Flatpak added sandboxing and a portal system that neither Windows nor macOS has a real equivalent of. The remaining problems are mostly ones of degree. Flatpaks are large and bundle runtimes that are already installed three times over. More importantly, the permission set is decided at build time by whoever did the packaging, so a great many applications ship with broad filesystem access and everybody stops reading the list. The mechanism is sound; the defaults are far too loose and the user is not really in the loop.
That is the shortest of these three sections on purpose. Linux mostly works.
Three kinds of thing arrive, and they are not alike
Some things act. Applications, codecs, themes, input methods, the runtime itself. They change what the machine does.
Some things describe. Settings, keybindings, the sources a machine follows, an employer’s policy. They change what the machine is configured to do.
Some things are acted upon. An article, a book, a spreadsheet, a route, a song, a message from a friend. They change what is in someone’s world and can do nothing on their own.
Every system in use today sends two of these down one pipe and has no pipe at all for the third. A distribution ships a browser and a wallpaper with identical ceremony, so the ceremony fits neither. The web ships executing code with no ceremony whatsoever and calls it a page. Nix models the first two with real rigour and treats the third as somebody else’s department. The result is a mechanism that has to be a security boundary and a content pipe at the same time, and nobody can answer the question of what they actually let onto their machine.
The line tilo draws is not the obvious one. It runs between authority and inertness, not between code and content.
A package changes what the machine does: an application, a codec, a theme, a font, an input method, a configuration layer, the OS image. Packages are signed, addressed by the hash of their contents, immutable, and replaced whole.
A record changes what is in someone’s world and can act on nothing: an article, a playlist, an annotation, a file. (A file is simply a record whose identity is a path.) Records are alive. They are CRDT documents that converge, are never versioned, and are never installed.
The interesting cases are the ones that look inert and are not. A PDF that runs script, a spreadsheet with macros, a font that is really a program for a hinting virtual machine, a page that is really an application. tilo does not adjudicate those, it deletes the category: every parser is a package with no authority. A format provider is a one-shot process with no network, no filesystem, and no persistence, taking bytes in and handing structure out. Opening a hostile file becomes a question about the parser’s confinement, not about the file’s intentions, and the answer is the same for every format.
Adding is not installing
“Install” is a word from an era when software modified your computer. In tilo it cannot. There is no shared prefix, no global mutable state, no ambient filesystem, and one rule that does most of the work:
Adding a package executes no code. No pre-install hook, no post-install hook, no trigger, no compilation step. Adding is checking a signature and unpacking bytes. A program that wants something to happen on first run does it on first run, inside its sandbox, like everything else it does.
That rule removes install-time code execution as a category, and it is what makes everything else affordable. If adding cannot hurt you, adding does not have to be a decision, so it stops looking like one: no administrator password, no progress theatre, no warning. Beside the Add button, one sentence says what the package expects to be able to do, and it is the only claim on that screen the system will actually hold it to.
Removal is total, because nothing global was ever created. It takes the software away and leaves what the software made, which is one directory per workspace, to keep or delete. There is nowhere else to look.
Updates are silent, atomic and reversible. Old content is still content, so going back to yesterday’s version is a lookup, not a reinstall. Silent updating is only defensible because of a rule further down: an update can never acquire authority the previous version did not have.
Where you got it from is not a security property
A source in tilo is a subscription, not a store. It publishes an append-only signed log mapping names to package hashes, and it hosts nothing at all.
A package’s identity is the hash of its signed root, which covers the manifest, the payload, and the publisher key together:
signed root = manifest_hash[32] || payload_root_hash[32] || publisher_identity[32]
PackageId = blake3(signed root)
Change one byte of the manifest and the id moves. Re-sign it and the id moves. So a name in a source log points at exactly one arrangement of bytes and nothing else.
Two proofs fall out of a log that is append-only and signed. A machine can prove that a name resolves to a particular hash, and that today’s log extends yesterday’s with nothing rewritten in between. A name cannot be quietly re-pointed at different bytes.
Neither proof helps if a source can show one log to one person and a different log to everybody else, so every tilo machine acts as a witness. Machines gossip signed log heads over the same peer-to-peer layer that already carries workspace presence. A source that singles somebody out has put two contradictory heads into circulation under its own signature, and anyone who sees both catches it. The audit population is the user base, and it costs nothing, because the network was there anyway.
Once a name resolves to a hash, the bytes are only bytes and anybody can supply them: the laptop across the table, the mirror on the office network, a CDN, the publisher, a USB stick. Each is verified locally and completely, so the question of whether a source is trustworthy enough to download from stops existing.
The practical version: somebody in a shared workspace is using a tool that is missing here, and adding it is a transfer over the connection that is already open. No round trip to anyone’s infrastructure, and it works on a train. Distribution becomes a side effect of collaboration.
Because a source only vouches, curation stops being a chokepoint. Anyone can publish a package, run a source, or keep a list. A catalogue is a list somebody maintains. The tilo catalogue is a list we publish, with no powers that a personal list of favourites lacks. A default, not a gate.
Reproducibility is not safety
Nix gets something profound right. A machine that can be described and rebuilt exactly is a machine you understand, and the Profile section below is my attempt to keep that property while paying less for it.
But reproducibility answers one of three questions, and not the one that decides whether anybody is safe.
Provenance: did these bytes come from who they claim? Signatures answer this cheaply and well.
Integrity: do these bytes correspond to the source they claim? This is reproducibility’s question, and I treat it as a check instead of a property. A rebuilder is anyone who takes a package’s declared inputs, rebuilds it, and publishes a signed attestation that the same hash came out. The listing shows the count: reproduced independently by four rebuilders. Reproducibility nobody verifies is a press release, and putting the number in front of users is what makes rebuilding worth somebody’s time.
Authority: what can it do once it runs? Every other system answers this last, if at all, which is why sandboxing arrives late and stays optional. It is also the only one of the three still standing when the other two fail.
They do fail, and not by breaking. Supply chain attacks succeed through signatures, not around them: a maintainer account taken over, a malicious transitive dependency, an insider with commit rights. Every byte signed, every build reproducible, every hash checked, and the attack lands anyway. A signature makes an attack attributable, which is not the same as making it survivable. A bit-identical, independently verified copy of malware is still malware.
So the effort goes into the third question and the first two get correct, boring answers.
One thing the first two do buy, once dependency graphs live inside the signed root instead of being vendored away: a machine can answer which of my applications contain this library locally and instantly, and a revocation can name the library rather than the publisher. One bad dependency is invalidated everywhere, at launch, without waiting for a hundred vendors to notice.
The app asks, the OS answers
Confinement itself is in the previous post: three enforcement layers, a manifest that names no filesystem paths at all, and one writable directory per workspace. What is left to describe is how an application ever gets anything beyond that, and the whole model comes out of one rule: a manifest never grants anything.
An application asks for a capability and gets back a handle or not available. Whether that answer came from a preset, a standing grant, a prompt the user just saw, or a policy that will never allow it, is invisible to the application. There is one code path and the application sits on the far side of it.
That invisibility does more work than it looks. An application that cannot detect refusal cannot nag about it, cannot break spitefully in retaliation, and cannot hold its own features hostage. The dark pattern requires knowing somebody said no.
Neither of the two standard models is right. Declaring everything up front, as Android did before it grew up and as Flatpak does today, asks at the moment the user has the least context, about capabilities described in the abstract, when the only alternative to yes is not having the app. Prompting at every use produces fatigue, and fatigue produces assent. Picking between them is picking which way to lose.
The way out is noticing how few permissions need to be requests at all, because most are implied by something that already happened:
- A file picked in the system file dialog. That file is granted.
- A record dragged onto the window. That record is granted.
- A record opened with the application. That record is granted.
- A paste. That clipboard item is granted.
No dialog appears, because the user already said it. Handing over a capability by acting on something is the primary mechanism here, not a fallback, and it covers the large majority of what applications ask for.
It works because of a confinement that is not a grant at all. An application opened in a workspace is scoped to that workspace, sees nothing outside it, and starts empty. Handoff can afford to be this granular because the floor underneath it is already small.
What remains is a short enough list to take seriously: camera, microphone, location, screen capture, background execution, open-ended network, and all my records of type X. Those get a real moment, with real shapes: just this time, while it is open, until I revoke it. While any of them is live the system shows it, with an indicator drawn by the compositor that no application can suppress or imitate, and that doubles as the fastest way to switch it off.

Everything granted in a workspace is in one list, each row saying who granted it and what it covers, each with a revoke button next to it. The Until choice is the grant shape: Once is the “just this time” case, Session lasts until the application closes, Always persists until revoked here.
Network belongs on that list. No application reaches the network by default, which sounds severe and mostly isn’t, because most applications need exactly one origin and that origin is implied by something the user did: configuring the mail account, pasting the feed address, signing in. What warrants a prompt is open-ended egress, an application asking to reach anywhere at all. That is rare, worth interrupting for, and the thing people most often actually want to refuse. “This application has no network access” becomes a checkbox on every listing that does what it says.
A curator can attach a preset to a package: this map viewer expects network and location. That curator might be us, an employer, or any attestor somebody has chosen to trust. A preset is a proposal. It sits on the listing before the add, and on the screen at the moment of the add, and it takes effect because somebody added the app while looking at it. Strip it afterwards and the application keeps running. The publisher writes down what their app needs, a curator vouches for it, and the user’s presence is what converts it into authority. The distinction that matters: a preset is a sentence on a screen somebody was looking at, and a manifest is a file nobody has ever seen. Nobody, us included, can turn the second into a grant.
Which is why silent updates are safe. No update ever widens authority. A new version that wants more gets what its predecessor had, plus a request at the moment it first needs the new thing, with somebody there to answer. There is no path by which an application acquires power while nobody is looking.
Two smaller consequences. A package nobody trusted has vouched for does not get a frightening dialog, it gets a tighter default (no network, no standing capabilities, no background execution) and a plain sentence on the listing saying so, because warnings train people to click through and defaults do not. And a denied capability has to read to the application as not available, identical to the machine simply not having a camera, with the application staying useful without it. That last part is not politeness. A permission system is only real if saying no is cheap; if refusing costs the user the app, they will not refuse, and the prompt was theatre.
Last, the application that is not malicious, only expensive. Phones already report this, with a battery screen ranking applications by what they cost, and then do nothing with it, because reporting is not a bound. A tilo manifest declares memory, CPU and disk ceilings and the launcher enforces them as cgroup limits. Consumption then belongs in the same surface as permissions, because this application used forty minutes of CPU in the background yesterday is the same kind of fact as this application used your microphone, except here it is a fact with a lever next to it.
Settings are a document
Here is what Nix costs. To get a machine that can be described and rebuilt, somebody has to write the description, and the boundary between what is described and what is not gets drawn per package by whoever wrote that package’s module. It is always incomplete. What falls outside does not become imperative, it becomes invisible. The interface goes too, which is a real loss, because sometimes flipping a switch and watching what happens beats composing a sentence about it.
tilo keeps the property and drops the price by refusing the premise. Reproducible configuration does not require anybody to write configuration, it requires the system to record what they did.
The Profile is a document, the same kind of CRDT document as everything else in the system. It holds subscriptions, added packages, granted capabilities, appearance, input, keybindings, and every application’s settings. Every control in the system edits it. Flip a switch, a value changes, and the document has a hash. There is no declarative layer to keep in sync with an imperative one, because the imperative act is the declaration.
And it opens as text. As an editor, not an export. Four things have to be true for that to be pleasant instead of a novelty.
The document stores the syntax tree, not the values. Keys, values, comments, groupings and blank lines are addressable nodes with stable identities, so the text is a rendering of the tree instead of the tree being a parse of the text. Comments survive. Ordering survives.
Saving applies a structural diff. The system compares the buffer against the tree it rendered and applies the minimal change, so one edited line is one operation. It merges cleanly with a change somebody made elsewhere in the same file and cannot clobber a value nobody looked at.
The buffer is live. Change a setting in the interface while the file is open and the line changes under the cursor. Text and interface are two editors on one document, the way two people are.
Only your own layer is editable. Values inherited from somewhere else render as dimmed inlays carrying their origin, and typing over one promotes it into your layer, so “what did I change” and “where did this come from” stop being two separate questions.
Layers are packages, so taking a terminal setup to another machine is subscribing to a layer, and an employer’s policy is a layer with higher precedence that can be read. Which values it overrode is visible, instead of something discovered by feel.
The seam that ruins declarative configuration gets drawn once, by the API, for everybody. An application declares its settings schema and gets a typed accessor. Everything declared is configuration: portable, typed, validated, diffable, synced, revertible. Everything else is state, meaning caches, window positions, scroll offsets and downloaded models, which stay local, opaque and disposable. No application author decides this, because the toolkit makes the other choice impossible.
No application draws its own settings screen either. The system renders it from the declaration, the way the compositor draws window chrome. Declaring a setting is writing a field instead of building a UI, and every application’s settings come out consistent, searchable, translatable and accessible without the application doing any work.
The payoff: a machine has a hash, and a fresh install plus that hash plus its sources reproduces it. The machine, not the documents, which are records and live in workspaces. History is continuous instead of generational, so a terminal can be reverted without reverting the machine, and what is different about this computer is a diff that can be looked at whenever.
Content, finally
An application is a lens, not an owner, and that is already true inside a tilo machine. A workspace holds records by reference instead of owning them. An ebook reader is a way of looking at whichever book records are in this workspace, and two people sharing a book work on one document instead of two copies drifting apart.
Record types are declared by packages, which is what lets one library view show a book, a playlist and an article side by side. The system knows nothing about what an article is and can still display one.
Sharing across machines is that same mechanic reaching past the edge of one person’s workspaces, and it needs no new vocabulary. Somebody shares a record by offering a reference. It lands in Shared with you, a queue of pending references, which is not a workspace and not a library, and it is not usable until it is filed into a workspace, because a record is only reachable through a workspace that references it. Accepting is the same gesture as pulling one of your own records in from another workspace. What this protects is that nothing enters your workspaces because somebody else decided it should.
Putting something in front of somebody at all requires a relationship: a shared workspace, a contact, a subscription. Spam is not filtered here, it has no address to arrive at. The cost is real and worth stating, which is that first contact needs an introduction, the way it does in physical life.
A record can also be made public, which makes it reachable, not findable. Anybody holding its identity can fetch it, so a reference can be handed out beyond a contact list: posted, mailed, printed on a poster. Withdrawing it stops future changes from flowing and does not recall what anyone already has, which is a fact about giving people data rather than a flaw in the design, and the interface says so instead of implying otherwise.
Finding public things nobody has a connection to (indexes, cross-machine search, ranking) is deliberately not part of this. It is a large problem with only weak guarantees available to it, and nothing above depends on solving it. What can be searched is what can already be reached: your workspaces, your subscriptions, your catalogues. I have ideas about the rest and no design I would defend yet.
Subscription is the primitive underneath all of it, one concept doing five jobs: following a person, taking a configuration layer, trusting a source, following a catalogue, and letting somebody reach you. One signed edge with a scope, one place to see them all, one place to revoke, and portable, because they live in the Profile.
What kind of computer this is
Adding software stops being a decision, because adding cannot run code and cannot acquire power. Trying something stops being a commitment. Where the bytes came from stops being a security property. Configuration becomes something readable, diffable and portable that nobody had to write. Content moves the way collaboration already moves, and nothing arrives uninvited.
None of that comes from asking people to be careful. It comes from arranging things so that carelessness is survivable, which is the only security model that has ever worked on a computer somebody actually uses.
Status
Built and running: the package format with signed roots and content addressing, publisher identities, scoped attestors, revocation checked at install and at every launch, the three-layer sandbox, per-workspace writable directories, and image-level reproducible builds with signature verification on update.
Designed and in progress: source logs and witnessing, rebuilder attestations, the Profile, the offer mechanic, budgets, and per-origin network implication.
The weakest claim in this post is the rebuilder count. It stays at one until somebody unaffiliated rebuilds a package, and until then the mechanism is described more confidently than the ecosystem justifies.
Sources
- Flatpak permission model and the
filesystem=hostproblem: Flatpak sandbox permissions documentation - Landlock, the kernel LSM doing the third sandbox layer: landlock.io