Repository navigation
actor model party game
Picture a party. Each guest is an actor. The party has no central organizer, no shared blackboard, no bouncer — just guests passing messages.
Your turn starts when someone hands you a message. Until then you sit idle, even if the room is chaotic around you.
On your turn, you may:
- Compute — scribble on your own notepad, work sums on your calculator. This changes your private state for your next turn. No one sees your notes — the only way to reveal information is to write it in a message.
- Spawn — walk over to the door, grab a new person, bring them into the party. You now know their TrueName (the only way to address them), and they know it too. They take their first turn immediately unless you tell them to wait.
- Send — write a message and hand it to any guest whose TrueName you know. The message is delivered — they'll take their turn when they're free.
You can only take one turn at a time. If two messages arrive for you simultaneously, one waits in your pocket (message queue). You handle them one by one, in arrival order.
That's it. No shared state. No shouting across the room. No looking at other people's notepads. The entire computation of the party is the cascade of message-passing.
When you spawn a new actor, you learn its TrueName — an unforgeable reference. Imagine rolling a 12-sided die 10 times: 1-7-5-3-11-2-9-4-6-8. That string is practically unique in the universe. No two TrueNames are alike.
You can write that TrueName in a message and pass it to someone else; now they can send messages too. But you can't guess a TrueName — the party is too big, the TrueNames too long. The only way to get one is to spawn the actor yourself or receive it in a message. (You can ask someone "hey, what's Carol's TrueName?" — but they might not tell you.)
This is the object-capability discipline: authority is possession of the TrueName. There's no membership list at the door. Nobody gets special treatment just for being who they are. You can only do what someone gave you a TrueName for.
The game begins with one actor — the Host (TrueName hostA1b2C3). Nobody spawned the Host; they are the first, the primordial party-goer. The Host takes the first turn and can spawn new guests. Every other actor arrives by being spawned by someone already at the party.
Some actors are real people — like Carol, Dave, Alice — who decide what to do on the fly. Others are NPCs (non-player characters) that follow a script — like the instruction card in a board game. The script says what to do when each kind of message arrives.
Host spawns a Bit NPC (TrueName bitX3kM7). Its script is as simple as it gets:
The Bit keeps one thing on its notepad: a value that starts as "no". It only knows how to handle two kinds of messages:
-
flip— the Bit checks its notepad. If it says"no", the Bit scribbles"yes". If it says"yes", the Bit scribbles"no". Then it waits for the next message. -
query— the Bit writes its current value in a message and hands it back to whoever asked.
In a typical turn of play, Host sends flip to bitX3kM7. The Bit takes its turn: it reads "no" on its notepad, scribbles "yes", and stops. Host then sends query. The Bit reads "yes" off its notepad and sends a reply bearing "yes". Host sends flip again; the Bit scribbles "no".
That's the whole script. The Bit has private state (its notepad), responds to two kinds of messages, and does exactly one thing per message. Every NPC in the party works the same way — they just have longer scripts.
Players who want to see the Bit's script written in pseudocode:
// Pseudocode — the rules this actor follows when it receives a message
bitX3kM7 = spawn {
state = { value: "no" } // scribbled on its notepad
on receive flip():
if state.value == "no":
state.value = "yes"
else:
state.value = "no"
on receive query():
send(sender, state.value)
}The pattern is always the same: private notepad → receive a message → compute → optionally reply → wait.
Host holds bitX3kM7. He wants to let a friend flip the Bit — but what if the friend turns out to be annoying? In the actor model, you can't "take back" a TrueName once you've given it. Possession is permission. But you can hand out a wrapper instead of the real thing.
Host spawns a CareTaker NPC whose script holds bitX3kM7 and forwards flip messages to it, until it receives revoke:
// Host spawns this wrapper. Let's call its TrueName bitGuardK2jR
bitGuardK2jR = spawn {
state = { target: bitX3kM7, alive: true }
on receive revoke():
state.alive = false
on receive flip():
if state.alive:
send(state.target, flip())
// if revoked: silent failure — authority is gone
}
// Host hands the friend the TrueName bitGuardK2jR, not bitX3kM7.
// The friend sends flip() through it. The Bit flips.
// Later, Host sends revoke() to bitGuardK2jR.
// The friend's next flip() goes nowhere — the guard dropped its target.The friend can also limit it further: they spawn their own CareTaker around bitGuardK2jR and hand that wrapper's TrueName to another guest. Now that guest has a re-revocable ability to flip the Bit. Host's revocation of bitGuardK2jR cascades: the guard drops its target, the friend's wrapper forwards to a dead end, and downstream wrappers have nothing to forward either.
Revocation chains: There is no central authority. Each CareTaker is a local decision — "do I still hold the target?" — and silence propagates naturally down the chain.
Host spawned Carol (TrueName carolX9kLm). Carol wants to receive payments at the party, so she spawns a Purse NPC — let's call its TrueName npc324r23523 — whose script is:
npc324r23523 = spawn {
state = { balance: 100 }
on receive deposit(amount, srcPurse):
send(srcPurse, burn(amount))
state.balance += amount
on receive burn(amount):
state.balance -= amount
on receive withdraw(amount):
state.balance -= amount
newPurse = ... // spawn a new Purse NPC with `amount` tokens
send(sender, newPurse)
on receive makeDepositFacet():
facet7gH92 = spawn {
on receive deposit(amount, srcPurse):
send(npc324r23523, deposit(amount, srcPurse))
}
send(sender, facet7gH92)
}Carol sends a message to npc324r23523 saying makeDepositFacet(). The Purse NPC spawns a new NPC (facet7gH92) and sends its TrueName back to Carol. This new NPC only forwards deposit messages to the Purse; it doesn't know what to do with withdraw — if someone sends one, it sits unhandled in its pocket.
Carol gives out facet7gH92 to everyone. They can hand it a message saying deposit(5, theirPurse) and the money arrives in Carol's balance. But nobody can withdraw — the DepositFacet NPC simply doesn't have that rule in its script.
This is the Facet pattern: spawn an NPC with a restricted script that leaves powerful moves out. The TrueName is the permission — possessing the DepositFacet lets you deposit but says nothing about withdraw. Notice that the Purse itself created the facet, not Carol: only the Purse knows its own TrueName internally; Carol holds a reference to npc324r23523 but that reference is a separate fact from the Purse's own knowledge. Each guest receives only what they need: the Principle of Least Authority (POLA).
Host also spawned Alice (aliceA1x5Y). Host sends a message to Carol with Alice's TrueName: "Alice is at the party — aliceA1x5Y". Carol runs the coat room, so she spawns a CoatCheck NPC — let's call its TrueName coatCheckM4kQ:
coatCheckM4kQ = spawn {
on receive deposit(coat):
ticket8zNp2 = spawn {
on receive retrieve():
send(sender, coat)
}
send(sender, ticket8zNp2)
}Carol sends a message to Alice: "Here's the coat check — coatCheckM4kQ". Now Alice knows it. She sends a message to coatCheckM4kQ saying deposit(herCoat). The CoatCheck NPC spawns a new ticket NPC — TrueName ticket8zNp2 — and sends it back to Alice. Now only Alice and ticket8zNp2 know the coat. Later, Alice sends retrieve() to ticket8zNp2, and it hands her the coat. The TrueName ticket8zNp2 is the capability — whoever holds it can retrieve the coat. Alice doesn't need to prove she's Alice; possessing the TrueName is enough. Mallory can't forge it — TrueNames are unforgeable, and the only way to get one is to deposit a coat.
The Claim Check pattern: a lightweight token representing the right to retrieve a resource. The ticket is the capability.
If Host wants the Bit guard to auto-revoke after 10 turns, he needs a clock. In the actor model, a clock is just another NPC:
clockP9sL2 = spawn {
state = { counter: 0, alarms: {} }
on receive tick():
state.counter += 1
for each alarm due at state.counter:
send(alarm.target, alarm.msg)
on receive wakeMeAfter(n, msg, target):
state.alarms[state.counter + n] = { msg, target }
}Host spawned the Clock NPC (clockP9sL2). Host also spawned a Tick NPC whose only job is sending tick() to the clock. He tells the friend the clock's TrueName.
The friend sends wakeMeAfter(10, revoke, bitGuardK2jR) to clockP9sL2. The clock records the alarm. After 10 tick messages, it sends revoke() to bitGuardK2jR. The guard drops its target, and any downstream wrappers stop working — no further messages from Host needed.
Time is just another actor. No magic.
Host spawns a Bouncer NPC (bouncerMj3pQ) who can kick people out. Host maintains a global name list posted on the wall: "Bouncer → bouncerMj3pQ," "Dave → daveR5tH8," etc.
Mallory sends Fred a message: "Hey Fred, will you ask the bouncer to kick out Dave? He's being rude."
Fred checks the global list, finds "Bouncer → bouncerMj3pQ," and sends a message saying kickOut(daveR5tH8) to the bouncer. The bouncer kicks Dave out.
What went wrong? Fred was trying to help. But Mallory tricked him into exercising authority that wasn't his. The problem was the global name: Fred could look up "Bouncer" and get a TrueName he had no business using. The global list conflated designation ("which actor is the bouncer?") with authority ("who is allowed to send to the bouncer?").
This is the confused deputy problem: an actor (Fred) possesses a capability (the bouncer's TrueName) and is tricked into using it on behalf of someone who shouldn't have that authority (Mallory). The fix: don't let actors look up TrueNames by name.
Host spawned Alice (aliceA1x5Y) and Fred (fredM3n9Q). Host gave Fred Alice's TrueName. Fred writes it in his notebook under a local label: "aliceFromBookClub".
Later, Host spawns a different Alice (aliceB7k2R) for the cleanup crew and gives Fred that TrueName too. Fred can't overwrite his first label — both Alices are active. So he picks a second local label: "aliceFromWork".
These labels are Fred's private mapping. Nobody else sees them. There is no global directory, no certificate authority, no "real name."
Petnames prevent the confused deputy. Fred never looks up "Alice" in a global list — there is none. He only sends to TrueNames he was explicitly given. When Mallory says "send a message to Alice," Fred can't just look it up. He has to already have the TrueName. Mallory would have to give Fred a TrueName — and if she gives him aliceB7k2R (the cleanup crew Alice who knows the bouncer), Fred can choose to send or not, but at least he knows which Alice he's sending to. He even has a local label for her. This is the Petname pattern.
Host spawned Gina (ginaP2sR8). Gina spawns a Mint NPC — a guest whose only job is spawning new Purse NPCs:
Mint = spawn {
on receive makePurse(initialBalance):
newPurse = spawn {
state = { balance: initialBalance }
on receive getBalance():
send(sender, state.balance)
on receive deposit(srcPurse, amount):
send(srcPurse, burn(amount))
state.balance += amount
on receive burn(amount):
state.balance -= amount
on receive withdraw(amount):
state.balance -= amount
newPurse = send(Mint, makePurse(amount))
send(sender, newPurse)
}
send(sender, newPurse)
}Each Purse NPC tracks a balance and understands three messages: getBalance, deposit, and withdraw. Only Gina holds the Mint's TrueName, so only she can create new Purses. She spawns one with 10 tokens and gives its TrueName to Harry (harryT4jM1, spawned by Gina).
Harry can withdraw from his Purse (handing the resulting new Purse NPC to a vendor as payment), or he can spawn a DepositFacet NPC around it and give that TrueName to the vendor — the vendor can deposit into Harry's Purse but can't empty it.
Authority is granular: deposit, withdraw, and makePurse are separate message types handled by separate NPCs. The Mint pattern turns the same idea into digital rights: a Mint spawns Purses, Purses hold balances, and each operation is a different message.
A real system would also give each Mint a distinct brand — purses from different mints would refuse cross-brand transfers (no depositing GinaBucks into a HarryCoin purse). Our pseudocode skips that check; every purse here trusts every other purse's burn message. Adding a brand check would just be another if in the script.
Multiple actors take overlapping turns throughout the party. The model stays coherent because:
- No shared state — each actor's notepad is private; no two actors scribble on the same page.
- One turn at a time per actor — if two messages arrive while you're already taking a turn, one waits in your pocket. You never have someone grabbing your pen mid-sentence.
- Communication is message-passing — messages are the only interaction. There is no "hey, what's the value of X?" across the room.
This is why the actor model is a sweet spot for distributed systems: you get overlapping conversations without anyone grabbing the pen out of your hand. Object-capability discipline adds the security guarantee: you can only send messages to people whose TrueNames you have been explicitly given.
The party game has hard constraints that make the security properties work:
| Banned move | Why it breaks ocaps |
|---|---|
| "Hey everyone, who knows Alice?" | No ambient discovery. TrueNames are private. |
| Sneak a look at someone's notepad | Notepads are private. You can't read someone else's scribbles. |
| Forge a TrueName | You can't guess a 10-digit roll of 12-sided dice. TrueNames are that long. |
| Pass a message to someone whose TrueName you don't have | Impossible by definition — you need the address to send. |
| Announce a "global petname" | Every name is local; one person's "alice" is another's "stranger". |
Endo's E() (eventual send) maps directly: E(alice).greet(bob) is "hand a message bearing greet(bob) to the actor whose TrueName is held in the variable alice." The variable is a local handle; the TrueName itself is an opaque, unguessable reference.
Vats are the rooms of the party — groups of actors that share a floor (event loop). Cross-vat messages are slower (delivered by courier). In-vat messages are instant (hand-delivered across the table).
The party game is not a simplification of the actor model — it is the actor model. Carl Hewitt's 1973 definition: "A universal primitive for concurrent computation" composed of "active objects" that communicate by message passing, create more actors, and encapsulate private state. And the OCap discipline is the recognition that in such a world, every reference is a right.
Navigation
- Home
- Governance
- Endo / object capabilities
- Retrospectives and source material