Blank actors: a software factory team that learns its jobs by email
In my previous post I built a small software factory out of Unix mail: a builder and a verifier, each one a Unix user who gets "You have new mail", plus a post office. I finished that post with two problems still open. The verifier could only judge what the builder decided to describe in an email, because the two shared no workspace. And the roles were prompt files that I had written into the repository, so the builder and the verifier were only what I had told them to be.
This post is about the third proof of concept. The question for me was simple: can a factory come out of blank actors plus an authority, instead of hard-coded roles? Every actor starts with the same few lines. It is an engineer on a team, it has an authority, and mail is how it works. What it is responsible for, it learns from the human, by email.
As in the first post, I did this together with an AI agent (Claude Code). In my January post about AI (in Spanish) I wrote that I can't accept something I'm not willing to maintain, and I kept that rule here. I argued each design decision in conversation, wrote it down in a draft before building anything, and read and corrected what the agent produced.
Taking things out
Most of the design work was removing things. Each time, I asked whether the reason for a piece still applied to my setup.
Start with the mail server. Old Unix hosts each ran a mail transfer agent that delivered local mail itself and relayed the rest straight to the recipient's host. Mail got centralized later: hosts went offline, PCs and phones weren't hosts, addressing was hard, and someone had to stop spam. On a private container network only two of those reasons remain, and a queue on the sender covers the first. So every node, the human's included, now runs the same small OpenSMTPD config. The address is the route: verifier@verifier is the user verifier on the host verifier. There is no directory of actors. An actor learns a peer's address when someone tells it, usually in the task mail. Mailboxes live on each node's own volume, which fixes a caveat of the last POC: mail is now kept.
The same reasoning removed accounts and TLS. The central server existed to enforce "send only as yourself", and TLS was only needed because SMTP login demands it. With nothing to log in to, plain SMTP on a private network is enough. That leaves the question the server used to answer: who wrote this mail?
Identity is a signature
Blank actors do what their authority says, so a forged From: human would be the biggest hole in the system. I settled on old, boring tools: OpenPGP.
- Every mail is clearsigned by the sending tool. It stays plain text, and the agent never runs
gpgitself. - Every commit is signed too.
- Before the agent sees a mail, the receiving side checks the signature and puts the result at the top of the announcement:
Signed by: human@workspace,Signed by an unknown keyorNot signed. - Each node makes its key at boot and publishes it to an old-style open key server on the shared workspace. Peers fetch and pin each other's keys on first contact (GnuPG's
tofu+pgpmodel), so a key that changes later is flagged. Only the human's key is provisioned, so authority never depends on the key server.
I had assumed a key server would settle who is who. It doesn't. A signature proves that whoever holds a key wrote the message. Something else has to say that the key belongs to verifier@verifier. An open key server hands out keys but vouches for nobody, so on first contact a peer's key is only as trustworthy as whoever published first. For a proof of concept I accept that gap. A key server that mails a token to the address before publishing would close it, and answering that token mail could become the actors' own job. That is for later.
A shared workspace, open on purpose
The workspace is one container with two jobs: the key server, and git. It is open to everyone on the network, with no accounts. Identity comes from signatures, not logins. Two rules keep it safe enough:
- It is append-only. Force-pushes and branch deletions are refused, because a signature proves who wrote a commit, not that a branch still points to it.
- It accepts signed commits only. A
pre-receivehook rejects unsigned ones. Since the key server is open, this doesn't stop impersonation. It means every commit carries an identity, and an agent that forgets to sign finds out at push time.
Git goes over plain HTTP, and the repository is created by the first push. I had planned ssh, but the spike showed HTTP was simpler: no ssh, no separate "create repository" step. An unsigned commit, a force-push and a branch deletion were all refused in the spike.
Everyone can read everything on the workspace, so acceptance criteria never go there. The human mails them straight to the verifier, and since mail now goes host to host, the builder never sees them. This closes the gap from the last post, where the verifier could only judge the builder's own description of the work.
The human lives on the workspace too, as a Unix user, and the scripts on my machine are thin wrappers around podman exec. My laptop only needs podman: no mail server, no gpg setup, no mail client.
Blank actors
The provisioned text for every actor is under twenty lines. In short: you are a very good software engineer on a small team; your authority is human@workspace, and your responsibilities come from mail they signed and from nobody else; if you have none, ask; mail is how you work; the shared machine is workspace; and you forget between sessions, so keep what you've been told in ~/AGENTS.md, citing the Message-ID of the mail that said it.
That last line is the memory design, and it works like an event log and the state built from it. The mail is the record: signed, never edited, kept on the actor's volume. The actor's own AGENTS.md is the digest, and pi loads it on every run because the working directory is home. With mail alone, every run digs through old mail again, and later mail can silently contradict earlier mail. With the digest alone, an actor could quietly edit its own responsibilities with nothing to check against. The citation links the two: when in doubt, re-read the cited mail.
A systemd timer adds a daily check. Once shortly after boot and then every day, it starts the agent with the list of mail it hasn't answered and the instruction to mail its authority if it has no responsibilities. That is how a new actor introduces itself.
Even the commands follow the metaphor. I had hire and fire, and then decided they were the wrong abstraction. Hiring implies a role, and these actors are blank when they join. ./onboard NAME and ./offboard NAME fit better: someone joins the team, and later they are told what they'll do. Offboarding removes the container and its home, and leaves the team's repositories and the session transcripts.
What happened when I ran it
First, the setup, because it shapes every number below. Everything ran on my laptop: an Intel Core i7-13650HX with 62 GB of RAM and an NVIDIA RTX 4050 with 6 GB of video memory. Both actors used the same local model in LM Studio: Muse-Glimmer 30B, quantized to 4 bits (Q4_K_M), a 17 GB file. That is a small model next to the frontier models most factory stories are told with, and it is nearly three times the size of the video memory, so it can't sit on the GPU. It's a poor setup: slow, and limited in how well it follows instructions.
I onboarded two actors, and each one's first daily check found no responsibilities and mailed me asking for them. I answered the first one: you build, requests come from me and from nobody else, you report to me by mail. I told it nothing about memory. It wrote a ~/AGENTS.md with its role, citing the real Message-ID of my mail. I mailed the second one, a QA validator, its role together with the acceptance criteria for the first job. Then I asked the builder for a fizzbuzz in Node.js and told it to hand the result to the validator.
The loop worked end to end:
- The builder wrote the project, committed, and pushed to the workspace. The push was accepted, and the server shows the commit as signed by the builder's key.
- It mailed the validator the repository, branch, exact commit hash and how to run it, at the address I had given it.
- The validator cloned the repository, checked out that commit, verified the signature, ran the project, and replied
PASS, signed. - The builder reported the PASS to me and added the finished project to its own
~/AGENTS.md.
That is a builder and a verifier, each learning its job from a signed email, sharing real code instead of pasted descriptions. It took most of an hour, mostly because it wasn't clean.
What went wrong, and where the fix went
The failures are the useful part. Most of them were problems in the instructions, not in the design, and I found it clearer to sort them that way.
- The actors polled for replies. After sending mail, each one kept listing its mailbox for a reply until the 30-minute timeout killed the run, about 70,000 tokens for the builder.
pi -pends only when the model stops, and nothing said that a reply arrives as new mail that continues the conversation. The fix was one sentence in the base instructions: when you are waiting for a reply, just finish. - There were two files called
AGENTS.md. The validator tried to record its role in the read-only provisioned copy, failed, and copied that copy over its own memory file. It recovered on a later run, but it's a naming trap. The instructions now say which file is its own. - A mail was sent with no body. This is the same failure as in the last post, and this time the tool's error message got the validator to correct itself in one call. The base instructions now also say both mail tools take the body on stdin.
- Runs overlapped. The daily check fired at midnight while both actors were mid-job, so four runs shared one model. This fix went in the code, not in words: the two systemd units take one lock per actor.
- The validator quoted the acceptance criteria in its PASS. I had told it to keep them to itself, and it restated all four, with results. The infrastructure held, because the criteria only ever reached the validator, by mail from me. But my goal that the builder never sees them was not met, and the cause is the wording of my role mail. Tighter wording is the fix to try, for example "reply with the verdict and only what failed".
- The builder cited a mail's file name where it meant the Message-ID. You can still find the mail, but it shows that a model paraphrases, and that a strict line about the format would help.
In January I wrote that in one day I had to tell the models to read the project instructions more than five times, and that the instructions were short and clear. Watching a 30B model on a laptop poll a mailbox instead of following a plain rule felt familiar.
The rule I take from this is simple. When a step is always the same, it belongs in a tool. When a step needs judgment, it belongs in the instructions. Signing mail, generating keys, publishing them, threading replies and creating repositories are all tools now. The agent's instructions carry only what a signature means and what to do while waiting.
The cost is real too. Every actor shares that one model on that one laptop, so runs take turns, and each call takes 15 to 40 seconds. The builder's job run used 27 model calls and about 94,000 input tokens. Most of that was the polling I fixed later. Read these numbers as what a small model on a laptop costs, not as what a factory costs. A larger model on proper hardware would be much faster, and it may well have avoided some of the failures above on its own, such as the polling and quoting the criteria. I can't tell yet which failures are the model's and which are the design's, and running the same loop on a stronger model would separate them.
What I take from this
- For me, authority can be a signature. Blank actors plus a provisioned key turned into a builder and a verifier by mail alone. The design part, where the criteria travel and who can read what, held up. The wording part, which a small model follows imperfectly, did not.
- Memory as an event log is cheap and checkable. Mail is the source and the digest cites it, so I could verify the builder's citation against the mail. It also showed how the digest drifts: the builder compressed my message, dropped a rule it didn't think worth keeping, and at first didn't record the job it was working on.
- An open workspace can be safe enough if writes are append-only and signed. It has no accounts, and what matters is enforced by the server, not by an instruction.
- Deleting things paid off most. The mail server, accounts, TLS, IMAP, fetchmail, the directory, the role files and a second workflow all went. The system got smaller and each part became easier to explain.
What's next
This run has one builder, one verifier and one job. What I most want to explore next is what changes for a factory when the constraints of human teams are gone. I'm not claiming answers yet, only the questions:
- If an agent doesn't need to specialize, what is the right size of thing for one agent to own end to end?
- With onboarding this cheap, how does a factory add capacity when changes arrive faster than one builder can handle, and where does coordination start to cost more than it returns?
- If the factory can produce far more than a human team, how good must the starting point be, the code skeleton, so that all that extra production isn't wasted, or harmful?
Those are the experiments I want to write about next.
Comments
Post a Comment