Agents are first-class citizens
Most software treats an AI agent as a guest. There's the real system, with its users and its permissions and its audit trail — and then there's a bot, holding a shared API key, acting on everyone's behalf and no one's in particular. It works right up until you want to know who did a thing, or revoke one automation without breaking the rest, or let an agent draft content but never publish it. Then the guest model falls apart, because a guest has no identity.
remill was built the other way around. An agent isn't a guest here. It's a principal — the same kind of first-class actor a human is — and it moves through exactly the same access machinery. This piece is about what that actually means, because "agents are first-class" is easy to say and load-bearing to implement.
Everyone is a principal
Before any decision is made in remill, the actor is resolved to a principal. A human who signed in with a password becomes one. An agent presenting a bearer token becomes one. Even an unauthenticated request becomes the built-in anonymous principal. There is no code path where "the system" acts without an identity attached.
Crucially, every autonomous agent gets its own principal — its own tokens, its own role assignments, its own trail. I never issue a "team" or "shared" token, because a shared credential is just the old single-secret hole wearing a nicer name. If two agents are doing two jobs, they're two principals, and the audit log can tell them apart forever.
There's a distinction I want to flag because it's a common mistake: the only security axis is whether a principal is a human or a machine. Whether a machine is labelled a "service" (a system pulling data) or an "agent" (an autonomous AI) is a display and grouping concept — useful for humans reading the access screen, invisible to the authorization logic. The security decision never reads the cosmetic label. Keeping those two things separate is what stops "it's just a service account" from quietly becoming a privilege.
One decision point, three doors
Here is the sentence I'm most willing to defend: there is exactly one place in remill where an allow-or-deny is computed. It's a single function — authorize(principal, action, resource) — and the admin, the REST API, and the MCP server all pass through it. One pipeline, three doors.
That's a nice architecture diagram, but a diagram doesn't stop the fourth engineer (or the tired version of me at 1am) from writing a query that skips the check. Two mechanisms make skipping it a compile error rather than a matter of discipline.
The first is a witness type. The functions that read or mutate documents demand a value — a Grant — as a parameter, and only the access module is allowed to construct one. A service that tries to reach the database without going through authorize() doesn't have a Grant to hand over, so it doesn't compile. Safety isn't a convention you follow; it's a type you can't fake.
The second is how permissions get applied to reads. Instead of fetching rows and filtering them in memory, remill compiles a principal's permissions into a SQL predicate that runs inside the query — roughly "published, or created by you, or explicitly granted to you." This matters more than it looks. Post-filtering in memory is how systems leak: an unreadable row still counts toward a page size, still shifts pagination, still inflates a total. If the filter lives in the query, the row a principal can't see never existed as far as that principal is concerned.
(Under the hood there's no row-level security engine doing this for me — the database is plain SQLite. All of it is TypeScript at the service layer, which I actually prefer: the rules are readable, testable, and in one place.)
Default-deny, and no negative rules
remill starts from deny. A principal with no roles can do nothing. From there, permission is additive only — you grant capabilities, you never subtract them. There are no negative rules anywhere in the system, and that's a deliberate constraint: if you can't express a policy by adding permissions, the policy is wrong for this system. Deny rules are where access models go to become unauditable.
Permissions come in two additive layers. Roles are the broad strokes — the seeded admin, editor, author, reader — and they're scopable to a collection, so "editor, but only of articles" is a first-class thing you can say. Item grants are the fine ones, letting you share a single document with a specific principal. The vocabulary of actions is closed on purpose — read, create, update, delete, publish, manage schema, manage access, share link — and extending it requires a written decision, not a stray commit.
My favourite consequence of this design is that a rule everyone wants but few systems express cleanly just falls out: agent drafts, human publishes. Give an agent create but not publish, and it can write all day and never make anything live. I didn't add a feature for that. It's what the additive model does when you simply don't grant one verb.
Tokens that can only ever shrink
Agents authenticate with bearer tokens, and the tokens have three properties I care about.
They're hashed at rest. The plaintext is shown to you exactly once, at creation, and never stored. A stolen copy of the database yields no usable tokens.
They're scope-masked, and the mask only narrows. A token's effective power is the intersection of its principal's permissions and its own scope — so a token can shrink an agent's blast radius but never widen it. You can hand out a key that does far less than the agent behind it could, and be certain it can't be inflated.
And their capabilities are discoverable as permissions. When an agent lists the tools available over MCP, it sees only the ones it's actually allowed to use. An agent without publish doesn't see a publish tool it will fail to call — the tool simply isn't in its world. Capability discovery is permission discovery. The agent learns what it can do by looking at what it can see.
Two more pieces round this out. Teams let you share with a named group of people — but a team is an audience, never a container of capabilities. It widens who a grant reaches; it holds no power of its own. And share links let you mint a read-only, expiring link to a document — a capability that's grantable to an agent on its own, precisely because minting a link is not the same as managing access. An agent can be trusted to share one document without ever being trusted to rewrite the permission system.
Everything is on the record
Every allow and every deny writes an audit row, attributed to the principal, the token, the door it came through, the action, and the resource. The log is append-only — there is no update-or-delete path for it in the code — and reading it is itself a permission. Even denials are structured, so an agent that's refused gets told what it lacked and can reason about it, instead of retrying blindly against a wall.
I extended this to cover the one actor that isn't a person or an agent: the system itself. When a scheduled job publishes a post at its appointed time, it acts as a distinct system principal — and it, too, writes an audit row. The invariant I refused to break is that authorize() is the only thing that writes to the audit log. Nothing acts off the books, not even the cron.
The receipt
I'll close with the honest version of the demo.
This article — the collection that holds it, the draft that became it, the moment it went live — was created by an AI agent, working over MCP, holding a token I minted for exactly this. That token is scoped to everything except one capability: it cannot manage access. So the agent could invent the articles content type, write the words, and publish them. It could not mint itself another token, could not widen its own permissions, could not touch a single grant. It didn't need to be trusted not to. The system made those moves unavailable.
And every step it took — define the collection, create the draft, publish — is sitting in the audit log right now, attributed to a principal named claude-code, timestamped, on the record. That's what "first-class" means. Not that the agent is powerful. That it's accountable, on the same terms as the rest of us.
Referenced by
- One definition, six surfacesarticles
- Why I built remillarticles