Closures, Objects, Actors, and Picos

Closure to Pico

I've been intrigued by closures ever since I taught an introductory computer science course from Structure and Interpretation of Computer Programs (SICP) at UC Davis in 1988. Watching students realize that a function plus the environment it was born in was enough to build objects, with no classes and no special syntax, was one of the great pleasures of teaching that course. I've followed the idea around ever since and used it wherever I could. When I built picos, I described each one as a closure over services and data. I've also described picos as implementing the actor model of distributed computing. But as this week's post on moving picos between engines shows, they're something more than that.

Closures in SICP

A closure is a procedure bundled with the environment in which it was created. When you call it, the body runs against bindings that were captured at creation time and that nothing outside the closure can see. Chapter 3 of SICP uses this to build a bank account:

(define (make-account balance)
  (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (define (dispatch m)
    (cond ((eq? m 'withdraw) withdraw)
          ((eq? m 'deposit) deposit)
          (else (error "Unknown request" m))))
  dispatch)

(define acc (make-account 100))
((acc 'withdraw) 50)   ; => 50

The variable balance lives in the environment that make-account created, and the only way to reach it is through dispatch. Each call to make-account produces a new environment, so every account has its own private balance. SICP calls this style message passing: you don't operate on the account's data; you send the account a message and it decides what to do.

SICP is also careful about what this costs. Once a closure holds mutable state, two accounts that start with the same balance are no longer interchangeable, and the book spends a section on sameness and change to work through what that means. With state comes identity. That observation turns out to be the thread that runs all the way to picos.

Closures and Objects

The account above is an object in every way that matters: encapsulated state, behavior bound to that state, and an interface defined by the messages it accepts. make-account plays the role of a class, and dispatch is a hand-built method table. There's an old programming koan that captures the equivalence: closures are a poor man's objects, and objects are a poor man's closures. Each can be built from the other.

What object-oriented languages add is vocabulary and machinery. Classes give the constructor a name and a place to live; inheritance lets one dispatch table extend another; method syntax spares you from writing the cond yourself. Those are real conveniences, but they don't change the underlying model. An object, like a closure, lives in one address space, is named by a pointer that means nothing outside that heap, and is invoked by a synchronous call that runs on the caller's thread. When the process ends, so does the object.

Actors

The actor model, introduced by Carl Hewitt in 1973 and formalized by Gul Agha in the 1980s, takes message passing literally. An actor has private state and a mailbox. It processes messages one at a time, and in response to a message it can do three things: send messages to actors it knows about, create new actors, and decide how it will handle the next message. Messages are asynchronous; the sender doesn't wait, and there is no shared memory between actors.

Agha's framing of an actor as a closure with a mailbox is the halfway point between the SICP account and a pico. The dispatch is still there, but the call has become a message in a queue, and the one-at-a-time rule replaces locks as the way an actor protects its state. Actors also introduce something closures and objects lack: an address that is useful only to those who have been given it. Knowing an actor's address is the capability to talk to it. That's a short step from a reference to a relationship.

Picos

A pico is an actor, and you can easily translate the SICP account to KRL. Here's the withdraw written as a KRL rule:

rule withdraw {
  select when account withdrawal
  pre {
    amount = event:attrs{"amount"}
  }
  if ent:balance >= amount then noop()
  fired {
    ent:balance := ent:balance - amount
    raise account event "balance_updated" attributes {
      "balance" : ent:balance
    }
  } else {
    raise account event "insufficient_funds" attributes {
      "balance" : ent:balance,
      "amount" : amount
    }
  }
}

The entity variable ent:balance is the captured environment, the select when clause is the dispatch, and an account:withdrawal event is the message. Each pico has its own event queue and retrieves events from it one at a time, just as an actor drains its mailbox. A pico usually has several rulesets installed, each with its own entity variables, so it's more precisely a set of closures over separate environments that share one identity and one event queue.

Where picos depart from closures, objects, and most actor systems is that they insist on surviving. A pico's state is persistent by default, so it outlives the process that happens to be running it. Picos are internet-first; its identity is a DID that anyone on the network can resolve, not a pointer or a runtime address. Its references to other picos are channels and peer DIDs, each named, governed by policy, and revocable, rather than raw pointers with unlimited authority. And its code can change over its lifetime as rulesets are installed and removed. Each of those properties follows from the first: once state has to outlive the process, it has to be persistent; once a persistent entity can be reached from anywhere, its identity has to be a name; once that name can be handed to strangers, the reference has to carry policy.

Those same choices are what make a pico movable. Nobody moves closures between machines, because the captured environment is opaque and full of pointers into the local heap; the mobile-code systems that tried, like Emerald, Obliq, and Telescript, had to invent restrictions to make it work. A pico never captures anything that can't follow it. Entity variables are plain data, rulesets are fetched by URL, and every reference to another pico is a name. Exporting a pico is closure serialization, importing it is rehydration, and the ECI remapping on import is pointer swizzling under a different name.

Side by Side

The table lines up the four models on the properties that change as you move from one to the next. Every cell could be argued at length, and particular languages and actor systems blur some of these lines, but the rows are the properties that matter when you want software to last longer than the process running it.

Closure Object Actor Pico
State Captured lexical environment Instance variables Private local state Entity variables, one set per ruleset
Identity Pointer in one heap Reference in one heap Mailbox address DID resolvable on the internet
Invocation Synchronous call Synchronous method call Asynchronous message Asynchronous event; synchronous query
Concurrency Caller's thread Caller's thread; shared memory possible One message at a time One event at a time; no shared memory
Lifetime Until garbage collected Until garbage collected Usually the life of the runtime Persistent; survives restarts
Behavior change Fixed at creation Fixed by class Chooses behavior for next message Rulesets installed and removed over its life
References to others Captured variables Object references Actor addresses Channels and peer DIDs with policy
Mobility Bound to its heap Bound to its heap Location-transparent in some systems Moves between engines with relationships intact

Read left to right, each column keeps what the one before it had and adds something. Objects give closures a vocabulary. Actors give objects asynchrony and isolation. Picos give actors persistence, a durable identity, and relationships they can carry with them. SICP's lesson about sameness and change was that state brings identity with it; picos take that seriously enough to make the identity cryptographic and the state portable.

A closure in 1988 was a beautiful way to think about computation. A pico is a closure that has been made to answer for itself in the world, one that can belong to a person rather than being tied to the machine it runs on.


Photo Credit: Closure to Pico from ChatGPT (public domain)


Moving Picos from Mesh to Mesh

A pico packs up and moves to a new engine

In one of my favorite books, Vernor Vinge's Rainbows End, Juan Orozco and a pair of troublesome twins play in a public park where the physical world is overlaid with a constantly shifting digital one. Creatures, games, and other virtual constructs inhabit the park, interacting with people and with the networked infrastructure around them. What caught my attention in that scene is that these software entities have an identity independent of any particular piece of infrastructure while still maintaining a relationship with the physical and network environment they inhabit. They exist in the world without belonging to a particular computer.

I think the scene sticks with me because that idea has long been my dream for picos. A pico has an identity, persistent state, rules, and relationships, but none of those things inherently require it to remain on the machine where it was created. If we separate the identity of a pico from its execution location, a pico could move from one host to another while remaining the same computational entity. Other picos and applications could keep interacting with it without needing to know where it's running.

Why would we want a pico to move? There are plenty of reasons:

  • Resilience and censorship: Move away from a host that becomes unavailable, hostile, or filtered.
  • Economics: Migrate to cheaper compute or storage when there's no reason to stay put.
  • Latency: Move computation closer to the people, devices, or other picos it interacts with.
  • Bandwidth: Move toward a data source rather than continually hauling data across the network.
  • Privacy and jurisdiction: Keep a pico on infrastructure with appropriate governance or data-handling rules.

Underneath all of those is a more powerful reason. If your picos can only live on one engine, you don't own them in any meaningful sense; you're renting a seat from whoever runs that engine. Portability is how we avoid lock-in, and avoiding lock-in is a precondition for personal agency in a system built from software that acts on your behalf. Version 1.7 of the pico engine, released a few days ago, takes the first real step: a pico and its subtree can move from one mesh to another, on a different engine, while the relationships it has with other picos keep working.

A Bike Changes Hands

The easiest way to see what this means is to watch it happen. The scenario in the demo video shown below imagines a bike club that owns gravel bikes for members to borrow and runs a community pico to coordinate rides. Each bike has a pico that serves as its digital twin: the pico holds the bike's state and history, has a child pico for the bike's computer, and has relationships with the club's Manifold app and with the Trail Ride Community. A ride coordinator with his own mesh has formed a relationship with Gravel Bike #7 so it can take part in the rides he organizes. Then the club sells bike #7, and the bike's pico needs to move to the new owner's mesh on a different engine.

The bike club and Alice, the new owner, use Manifold, the framework for managing things and communities that I've written about before and recently moved my own sensor network onto. Manifold matters here for a reason I'll come back to: a thing pico assumes certain services exist above it in the tree, and the destination has to provide them. What you see in the video is the engine's developer interface rather than anything a bike owner would use; it lets us observe and manipulate the picos directly, which is what a demo of the plumbing needs. Three meshes run on two engines, the club's and the coordinator's on one and the new owner's on the other.

The bike's pico and its bike-computer child are exported from the club's mesh as an encrypted blob and imported under the new owner's Manifold. After the import, the bike has the same did:webvh identity it had before, the ride coordinator's relationship with it still works, and the Trail Ride Community back on the club's engine still has a link to it. The new owner's Manifold forms a fresh relationship with the bike, since that one belongs to the new context rather than the old. Nothing had to be reintroduced.

A pico that stands in for a physical thing grows more valuable the longer it lives, because it accumulates the thing's history, relationships, and state over years. I made this argument in August using an example from Fuse, where selling a car meant handing its pico to the new owner with the maintenance history intact. The bike is the same idea. Its identity is bound to the bike, not to the club, and it has to be able to outlive any particular owner or engine because the bike will.

What Makes a Pico Portable?

Making a pico portable requires separating what the pico is from where the pico runs. A pico isn't a process, container, or virtual machine; those are execution mechanisms tied to particular infrastructure. A pico is an enduring computational entity with an identity, state, rules, and relationships. The host pico engine is simply the place where that entity happens to execute.

That distinction defines what has to move. A portable pico needs all the information necessary to recreate itself on a new engine: its rulesets, persistent state, configuration, keys, and information about its relationships with other picos and services. Some of that information needs special handling. Private keys, for example, shouldn't be copied around indiscriminately. But conceptually the goal is clear: capture the pico's computational state, transfer it to another compatible host, resume execution there, and retire the old instance.

Identity continuity poses the hard problem. If a pico moves, everyone it interacts with shouldn't have to get reacquainted with it; its identity must remain stable even when its location changes. That means addressing and routing have to be indirect: send an event to the pico, not to the server currently running it. Some discovery mechanism maps the pico's enduring identity to its current network location, and moving becomes a routing update rather than an identity change.

Relationships create a similar requirement. A pico might have channels to dozens of other picos in different meshes and even on different engines, and those relationships should survive migration without being recreated. Ideally, the endpoints of a relationship identify the participating picos rather than their hosts, so that after a move messages simply begin flowing to the pico's new location. Webhooks don't enjoy this property; each one holds a URL, and every one of them has to be updated by hand when the thing it points at moves. This is the difference between having a protocol and merely having a convention.

One thing I learned building this is that the unit of portability isn't a single pico and it isn't a whole mesh either. Exporting an entire mesh is the wrong granularity; the bike club isn't moving, only the bike is. But a pico rarely travels alone. Bike #7 has a bike-computer child, and parent-child relationships are part of how a pico behaves: children rely on their parent for rulesets, shared relationships, and policy. So what moves is a subtree, the pico and its descendants, and the importer attaches it under a new parent in the destination mesh.

That subtree boundary creates a dependency the design has to take into account. A thing pico inside the Manifold framework assumes Manifold's services exist above it; importing it under a bare root on another engine would leave it running but functionally broken. The export carries the subtree only, not its ancestors, so the importer is responsible for supplying the platform the subtree expects. In the demo that means adopting the bike under the new owner's Manifold app pico, which is exactly where a thing pico belongs.

Those pieces are what 1.7 puts in place, and they're the foundation for the capability imagined in the scene from Rainbow's End I mentioned earlier. With a stable identity, relationships that follow it, and an export that captures what it is, migration could become part of a pico's own behavior in a future release. A pico could monitor the environment in which it runs, discover alternative hosts, evaluate them against its own policies, and decide whether moving is advantageous, weighing cost, latency, bandwidth, reliability, jurisdiction, trust, or the availability of particular services. None of that exists today, but nothing in the architecture stands in its way now. At that point portability stops being a deployment feature; just as a pico can react to an event by updating state or sending a message, it could react by moving itself. The infrastructure provides places to run; the pico decides where it wants to live.

Why DIDs Make the Difference

When I added pico-to-pico identity in version 1.6, I said it was the foundation portability was waiting on. That wasn't a side benefit; I built the layer for this express purpose. Before 1.6, a pico's only identifiers were its channel identifiers (ECIs), and an ECI is both a local routing address and, in practice, a capability. Change the ECI and you break every peer, UI link, and webhook that holds the old value. Moving a pico while keeping its ECIs is fraught from both a security and an operability standpoint, and moving it without them meant starting over.

Giving every pico two kinds of DIDs splits those concerns apart. The did:webvh is the pico's stable, long-term address, the passport it hands to strangers. Its self-certifying identifier (SCID) is fixed for the life of the DID, while the host and path in the DID's log can change; a move appends an entry to the log saying where the pico now lives, and anyone who resolves the DID finds the new location. The did:peer pairs that each relationship uses are private, point-to-point identifiers with no engine address baked into them. Peers follow the DID, and the engine mints fresh ECIs on the new host as nothing more than routing handles.

Relationships survive because DIDComm already knows how to handle a party whose endpoint or keys change. After an import, the moved pico sends a rotation message to each of its peers, and each peer updates where it sends traffic for that relationship. The ride coordinator's mesh never learned the bike's new ECI and never needed to; it learned that the DID it already trusted now resolves somewhere else. That's an example of what it means for identity to be a protocol rather than a convention: the update is a message the relationship was designed to carry, not a chore for the operator.

What Actually Happens in a Move

The export walks the subtree and packages everything needed to reconstitute it: installed rulesets, entity state for each ruleset, channels with their tags and policies, scheduled events, and an identity snapshot for each pico that includes its did:webvh log and keys and its peer relationships. Rulesets with a canonical remote URL are carried by reference and fetched again on import; only rulesets that can't be retrieved on the destination get embedded inline. The whole package is encrypted with a secret the exporter chooses, because a blob that contains keys and relationship state is a capability-bearing secret and has to be handled like one. Transferring the blob is up to you; nothing in Wrangler moves it for you yet.

The import creates the picos under the recipent parent, mints new ECIs for every channel, and hydrates entity state, remapping any old ECIs that rulesets had stored in their own state. Then it raises wrangler:pico_imported so rulesets can run their own migration logic. With the picos in place, the identity service relocates each did:webvh by appending the new location to its log and notifies every peer of the move over DIDComm. Picos on the destination engine that had relationships with the subtree before it arrived get reconciled locally, so a relationship formed before the move keeps working after it.

What 1.7 implements is more accurately a copy than a move. The import does nothing to the source; the donor subtree is left behind in a vestigial state, still holding the same DID, and has to be deleted once peers have cut over. Leaving it there is worse than untidy, since an engine that still has a local pico with that DID will route to the stale copy rather than the real one. Atomicity, rollback when a move half-completes, and forwarding from the old location are all open problems. The design notes in the repository lay out what's done and what isn't.

Release 1.7

Version 1.7.0 of the pico engine ships two things. The first is the portable move described above. Underneath are engine primitives: ctx:exportRulesetState and ctx:importRulesetState for a single ruleset's state, and ctx:exportSubtree and ctx:importSubtree for a whole subtree. The developer's interface to that functionality is io.picolabs.portable_subtree, part of Wrangler, which wraps the full pipeline, including DID relocation and peer notification, in a single query to export and a single event to import. On the Manifold side, io.picolabs.manifold_portable_thing in the manifold-api repository builds on that interface to export a thing from one Manifold and import it, with registration, into another. The repository includes a docker-compose file that stands up two engines so you can run the cross-engine demo yourself.

The second is an upgrade to the peer DIDs themselves. Relationships formed on 1.7 use did:peer:4, the variant on DIF's standardization path, in place of the hand-rolled did:peer:2 identifiers 1.6 used. No npm package existed, so the engine carries a TypeScript port of DIF's reference implementation that I intend to contribute upstream. Existing relationships keep their did:peer:2 identifiers and keep working; nothing converts automatically, and I'd rather let old relationships age out than rewrite identifiers that other picos already hold.

Just a Start

What I ultimately want is for a pico to move itself on command, and eventually on its own judgment, with the old instance retired as part of the same operation. This release gives us the export, the import, and the identity machinery that lets relationships follow the pico, but the transfer and cleanup have to be handled ad hoc, either manually using the developer console or programmatically in KRL. Those are the pieces that turn a copy into a move.

I also want to be careful about what one working demo proves. We implemented and tested this against a scenario we designed. A scenario you design will contain the cases you thought of. There are almost certainly dependencies between picos, relationships that cross subtree boundaries, and external systems holding old addresses that the bike club demo didn't exercise. The only way to find them is to move real picos between real meshes, which is why I expect the next few releases to be shaped by what breaks.

The important lesson is that supporting personal agency in a technical system is a choice, and it's almost always the harder one. Pinning a pico to the engine where it was born is easy; making its identity survive a move took an identity layer, a messaging protocol, and a careful accounting of what a pico actually is. We can build systems that people own or systems they merely rent. The choice is ours, and this release is my vote.


Photo Credit: A pico packs up and moves to a new engine from ChatGPT (public domain)


Human in the Loop Control for OpenClaw

Human in the Loop

This post is part of a series on using dynamic authorization to control and coordinate AI agents. See the series recap to find other posts in this series.

I've been writing about how we can use Cedar to control agent actions and releasing demos showing Cedar as the authorization step in OpenClaw. One of the constant worries with agents is that a sufficiently motivated agent will find creative ways around controls. For example, I might think to limit access to a specific directory, but not think to keep the agent from creating a link to it so that the policy allows what it would otherwise deny. Policies are written by people who can't anticipate everything; agents are very good at finding the thing we didn't anticipate.

One way to protect high-impact actions is by putting a human in the loop (HItL). Some actions, like sending email on my behalf, moving money, or deleting data, carry consequences that are hard to undo. For those, I don't want to rely solely on having written the perfect policy. I want the agent to stop and wait until I've looked at exactly what it's about to do and said yes. This post describes a new OpenClaw demo that does that using Cedar and a Yubikey.

Controls, Not Prompts

The easy way to get a human in the loop is to tell the agent to ask. You put "always check with me before sending email" in the system prompt and hope for the best. That instruction is a suggestion to a language model. It competes with everything else in the context window, including the content of a web page or document that might have been written specifically to talk the agent out of asking. A prompt is guidance; it isn't a control.

The theme running through this series is that authorization belongs in the harness, not the prompt. In the policy-aware agent loop, every tool invocation passes through a policy enforcement point (PEP) that asks Cedar whether the action is allowed. The model doesn't get a vote. It can reason about the denial and replan, but it can't skip the check, because the check isn't something the model does. It's something the harness does to the model's proposed actions.

Human approval works the same way in this demo. The PEP, not the agent, decides when a human must approve, and the PEP, not the agent, supplies the evidence that a human did. The agent can't forge that evidence because it never touches it. That's the difference between asking an agent to behave and building a system where misbehaving doesn't work.

What the Demo Does

Earlier demos answered the question "may the agent do this?" This one answers a different question: "may the agent send this email now that a human has cryptographically attested to it?" The demo adds a simulated send_email tool to OpenClaw. Gathering information and drafting the message are ungated; the agent can read files and write a scratch draft just as before. Sending is different. The following diagram shows the agent loop from the earlier posts with the HItL modifications added.

Agent Loop with Human in the Loop Authorization
Agent Loop with Human in the Loop Authorization (click to enlarge)

When the agent calls send_email, the PEP asks Cedar for a decision and gets a deny, because no human has approved the message. Cedar doesn't know anything about approval workflows; it just says no. The PEP intercepts that deny and interprets it. Because the tool is send_email, the PEP treats the deny as "not yet" rather than "no," and instead of returning it to the agent, it parks the request and waits for a human.

The human opens an approver page, reviews the recipient, subject, and body, and taps a Yubikey. The PEP then retries the Cedar request with the human's approval injected into the context. This time Cedar returns permit, and the email is "sent," which in the demo means a JSON file lands in a mailbox directory.

If nobody taps the key before the timeout (three minutes by default), the PEP returns a hard deny to the agent and doesn't wait again. You can see this in the diagram as the timeout path that runs from the /authorize box back to the agent's evaluation step. The agent learns that the send didn't happen and can replan or report back, but it can't try again and hope the human is more agreeable the second time.

The Cedar policies that enforce this are short. One permits SendEmail when a verified WebAuthn approval is present and matches the request; the other forbids it otherwise:

@id("hitl-1-allow-send-with-approval")
permit(
  principal,
  action == OpenClaw::Action::"ToolExec::SendEmail",
  resource
)
when {
  context has humanApproval &&
  context.humanApproval.verified == true &&
  context.humanApproval.method == "webauthn" &&
  context has requestHash &&
  context.humanApproval.requestHash == context.requestHash
};

The forbid policy is the same test negated. Since Cedar's forbid always overrides permit, the explicit forbid means that no other permit policy, now or added later, can accidentally open up email sending without a human. The timeout lives in the PEP rather than the policy because Cedar has no notion of the current time; it evaluates the context it's given, which is exactly what makes its decisions predictable.

Notice what the policies don't say. Nothing in them tells anyone to go find a human; Cedar just returns deny, the same answer it gives for any other forbidden action. The PEP is what turns that deny into a request for approval. It intercepts the decision before the agent sees it and interprets a deny on send_email as "waiting for a human" rather than "no." In other words, Cedar decides whether the action is allowed, and the PEP decides what a denial means. That division keeps the policy engine simple and deterministic, but it also means the rule about which denials a human can override lives in code, a point I'll come back to below.

A Passkey for Each Approval

The approval isn't a generic "the human said OK." The demo uses ordinary WebAuthn, the protocol behind passkeys, and binds the challenge to a hash of the specific request. When I tap the Yubikey, I'm signing a statement about this recipient, this subject, and this body. The requestHash check in the policy makes sure the approval Cedar sees matches the action the agent is actually trying to take. If the agent changes a word in the body after I approve, the hashes don't match and the send is denied.

Think about the difference between signing a check and signing a blank check. A blank check says "I trust whoever holds this"; a signed check for a specific amount to a specific payee says "I approve this." Most approval flows for agents are closer to the blank check: the human clicks "allow" once and the agent carries that authority forward. Binding each approval to a single request keeps the human's authority attached to the human's decision. The approval can't be replayed for a different message or stretched to cover the next ten sends.

The "human" part of human in the loop is only as strong as where the passkey lives. On a Yubikey or other roaming hardware key, the credential stays on the device and approval requires someone to physically present the key and touch it. In a software authenticator, like a browser's passkey store or a password manager, the WebAuthn ceremony succeeds just the same, but anything that can unlock that store can approve. The demo asks the browser for a cross-platform authenticator so you'll be prompted for the security key rather than Touch ID. For a while, at least, we don't expect software agents to physically insert and tap Yubikeys. That's the point.

Try It Yourself

The code is in the openclaw-cedar-policy-demo repository on GitHub, and the HItL README walks through running it. Clone the repo, but don't start with the HItL demo. It builds on the earlier ones, so work through the basic Cedar demo first to get the PDP, the PEP, and your OpenClaw configuration in place. The README also includes commands to verify that the earlier demos still pass once the HItL changes are added.

The README covers starting the PDP (which also serves the approver page), registering your Yubikey, and running a set of policy tests that don't need a key at all. The live walkthrough asks the agent to send a simulated email, and then you approve it on the approver page. If you let it time out instead, you'll see the agent receive the denial and no mailbox file appear. I'd recommend trying both; watching the agent handle the timeout says as much about the design as watching the approval succeed.

Shortcuts and the Road to Production

Like the other demos in this series, this one is built to show an architecture, not to run in production. It takes several shortcuts:

  • Simulated delivery—No email leaves the machine. An approved send writes a JSON file to demo/mailbox/sent/, named with a UTC timestamp.
  • No notification—Nothing tells you a request is waiting. You have to open the approver page yourself and watch for it.
  • One gated action, chosen in code—The policies name SendEmail directly, and the PEP is hard-coded to park any deny on send_email. Only that one tool requires a human, and the PEP, not policy, decides that a deny there should wait for approval. It will even park a send that some other policy forbids, asking for a tap that can't change the outcome.
  • One approver on localhost—The PDP serves the approver page, there's a single registered credential, and it has to run on localhost so the WebAuthn origin matches.
  • Trust in the authenticator—The demo asks for a roaming authenticator, but nothing verifies that the credential actually lives on hardware.

None of these change the architecture, and each points toward how you'd generalize it into a production system:

  • Policy-selected actions—Policy rather than code would decide which actions need a human, perhaps based on resource attributes like sensitivity or on the scope of a delegation. The PEP still interprets the deny, but it would take its cue from policy. For example, it could park only when every policy behind the deny carries an annotation like @hitl("required"), or use partial evaluation to confirm that human approval is the only thing standing between the request and a permit.
  • Approvers as principals—The approver would be a principal in the policy model, so Cedar could say who is allowed to approve what; approving a wire transfer and approving a calendar invite shouldn't require the same person.
  • An approval service—A dedicated service would replace the page served by the PDP and push requests to the approver's phone or other device.
  • Durable state and audit—The pending store would be durable and every approval would be logged, giving you an audit trail that ties each consequential action to a specific human decision.
  • Authenticator attestation—Checking the authenticator's attestation would let policy require hardware keys for the actions that matter most.
  • Multiple approvers—Requiring more than one approval is a natural extension of the multi-signature patterns I discussed in It's Not Just What Agents Can Do...It's When They Can Do It!

One more thing matters that no amount of cryptography fixes: the human has to see what they're approving. The Yubikey tap attests to the request hash, but the human decides based on what the approver page shows. If the display summarizes or hides part of the action, the attestation covers something the human never saw. A production approval UI needs to present the action faithfully, which is an old lesson from digital signatures that applies to agents just as well.

Humans Where They Matter

Putting a human in the loop for everything would defeat the purpose of having an agent. People who are asked to approve every action stop reading and start tapping, and a tap without attention is just a slower way of saying yes. The value of this design is that policy decides where the human belongs. Most actions flow through Cedar and never interrupt anyone. The few that carry real consequences stop and wait for a person who can see exactly what's about to happen.

Agents act on our behalf, and the authority they carry is ours. When the stakes are high, the agent shouldn't be able to decide on its own that we would have approved, and it shouldn't be able to argue its way past a prompt that says to ask. Placing the control in the harness, bound to a specific action and a physical key, keeps that decision where it belongs. We can build agents that treat our approval as a formality, or we can build ones where our approval is a real control. The second is harder, but it's the one that keeps people in charge of what's done in their name.


Photo Credit: Human in the Loop from ChatGPT (public domain)


Moving My Sensor Network onto Manifold

Cabin temperatures in Home Assistant

I have been working to make Manifold a good framework for building pico meshes, the kind I use for my network of LoRaWAN sensors. The new Manifold API doesn't have a UI like the old Manifold did. I want to use Home Assistant (HA) for that and recently created a custom HA integration. With the groundwork laid, it was time to move my actual sensor network onto the new framework running on the updated pico engine. This post describes that move and the problems I ran into, and fixed, along the way.

Manifold as a Framework for Pico Meshes

Manifold began as an application for managing your things and creating communities to group them together. The primary application was Safe and Mine, a QR-code-based tagging system to identify your keys, bags, and mugs. Manifold was expandable with custom applications (written in KRL) that could be added to each thing.

When I rebuilt it as the Manifold API, I stopped treating it as a finished app and started treating it as a framework, a set of domain rulesets other people can extend to build their own meshes. A pico mesh is a small network of independent, addressable computational things, each with its own state, its own rules, and its own relationships. It still supports Safe and Mine, but I wanted it to be more than that—a personal space to manage both inactive (like your mug) and active (like a temperature sensor) things. The reason to build on Manifold is ownership; the mesh belongs to the person who runs it, not to me and not to a platform, and they decide what connects to it and what it exposes.

That distinction is the whole point. I've been working for years to build alternatives to what I call the CompuServe of Things. A sensor network you rent from a vendor is legible to the vendor first and to you second. A sensor network that runs as your own pico mesh answers to you, and the framework underneath it exists to keep that relationship intact as the network grows.

The Network I Was Moving

The network I wanted to move is not a demo. It is a working LoRaWAN deployment I have written about before, starting with easier IoT deployments on Helium and then the temperature sensors in a remote pumphouse. I have temperature probes in the crawl space of my cabin, on the north side deck, and in my beehive in addition to the pumphouse. Those sensors report over Helium, and until now they ran on the old sensor-network code, essentially recreating the features of the Manifold framework in addition to their specific duties interpreting and managing the payloads received over LoRaWAN.

Rebuilding the network on top of Manifold simplifies the code and gives the sensors additional capabilities. Each sensor becomes a thing in a community, with an owner pico above the Manifold pico that ties the whole mesh together. The structure is easier to see than to describe, so here is the mesh as it stands after the move.

The sensor network as a pico mesh
The sensor network as a pico mesh, with the owner and Manifold picos above the individual sensor things (click to enlarge)

Everything below the Manifold pico is a sensor or a community. The pumphouse, cabin crawl space, cabin north side deck, triple-temperature probe, and beehive are sensors. The Sensors pico is a Manifold community pico for the sensors. It handles things like notifications, sensor initiation, and reading aggregation. Moving to Manifold didn't change what the sensors measure, but it makes them more capable and manageable. And having them in a standard mesh means I can write HA integrations that work for lots of different meshes.

Home Assistant as the Interface

Because the new Manifold API has no interface of its own, I spent some time making Home Assistant fill that role. In Using Home Assistant with Manifold I described the integration: Home Assistant runs the pico engine's OAuth flow, comes away with a scoped token, and reads the mesh through it. That gives me dashboards, history, and alerts on software many people already run at home, without my having to write a UI of my own. The sensor network ships a companion Home Assistant component so its sensor things show up as real temperature and humidity entities, not just generic mesh objects. The code serves as an example of how to write HA companion components that work with the Manifold integration.

Where It Broke: OAuth Meets Webhooks

The initial port looked good. Every sensor came across, the communities lined up, and the readings flowed into the mesh the way they had before. The trouble started when I wired the mesh to Home Assistant, because that connection depends on the OAuth protection I added to the engine in Identity for the Pico Engine. Turning on OAuth secured the mesh against outside access, which is exactly what I wanted for Home Assistant; it also secured the mesh against webhook calls from Helium, which is not what I wanted at all.

Helium delivers sensor readings by calling a webhook defined on the pico for a specific sensor. The Helium console is software sitting outside my mesh that needs to POST data in, on its own schedule, with no human present to complete a login. The same protection that made Home Assistant's access deliberate and revocable made the inbound webhook fail with an authorization error. That is the tension at the center of this move: authentication that assumes a person in the loop breaks the moment a machine needs to knock on the door.

Two Ways Through

There are two reasonable ways to let a machine through without tearing down the protection around everything else. Picos define webhooks by creating channels. Each channel has a policy saying what events and queries are allowed on that channel.

  • Client-credentials channels. The engine already supports OAuth client credentials for a channel. I described this feature in Identity for the Pico Engine. A channel configured this way lets the caller authenticate as itself, against that one channel, rather than as a person against the whole mesh. You configure the bearer token in Helium as part of defining the webhook and Helium includes it on each POST.
  • Owner-declared exempt channels. Alternately, the mesh owner can mark a channel as exempt from the OAuth protection. This is only safe when the channel's policy is tight enough to keep the exemption from becoming an open door, and in this case it is, because the channel accepts one narrow shape of sensor data and nothing else.

Both of these approaches rely on the owner being careful about scoping the authority of the webhook so that it doesn't represent a security threat. Neither one weakens the mesh as a whole, and neither one hands a machine the standing of a person. I updated the pico engine to support the owner-declared exemption, so the choice now belongs where it should, with the person who runs the mesh, decided channel by channel rather than for the network all at once.

The Window onto the Mesh

With data flowing again, Home Assistant does exactly what I hoped. The dashboard at the top of this post is my cabin: the pumphouse, the crawl space, the north side deck, each updating every few minutes from the sensor network now running on Manifold. I get history, thresholds, and a picture of the current temperatures at a glance, all from software I run.

Home Assistant also makes alerting easier. I already had notifications on the old setup, but each one was its own bit of wiring off to the side; Home Assistant keeps the readings, the history, and the rules that act on them together in one place. I can build an automation directly on a reading, so a low temperature in the pumphouse sends me a notification while there is still time to act, rather than after a freeze has cracked a pipe and left the cabin without water. Having that alongside everything else I watch, instead of bolted on separately, is exactly what I want an interface for the mesh to do.

None of this is a new capability so much as a boundary drawn in the right place. The reading Helium sends and the reading Home Assistant shows are the same as they were a year ago. What changed is the software and systems are less creaky and easier to manage and update. The mesh belongs to the person who runs it, and Home Assistant is just a window onto it.


Software-Defined Three-Way Switch

Lights over the fireplace benches

As part of our remodel, Lynne wanted lights over the built-in benches on either side of our fireplace. The fireplace itself made it hard to run both fixtures back to a single switch, so the easy path left us with one switch at each end of the hearth. That works, but it means anyone who wants the benches lit has to cross the room and flip two switches. It's a small annoyance, the first-world that the physical world imposes from time to time. Fortunately, software can fix it.

Both switches are Inovelli Blue Series 2-1 dimmers, Zigbee devices that report their state and take commands over the network as readily as a person flips them by hand. That makes them programmable in a way an ordinary switch is not; the paddle still does the obvious thing, but the switch is also something software can watch and control. I've leaned on that elsewhere in the house. One of them runs the bathroom fan, turning it on by itself when the humidity spikes and off again once the room dries out, so nobody has to remember.

The fix for the benches uses the same idea. Each light keeps its own switch, and a pair of Home Assistant automations wires the two together in software: when one switch turns on, it turns on the other, and the same for off. Flip either paddle and both fixtures respond, which is exactly how a three-way switch behaves when it controls a single light. Here there are two lights and two circuits that never meet in the wall, made to act as one from the automation layer instead.

And nothing about this arrangement is fixed, either. The same two switches could join a scene that dims the whole room for a movie, answer a voice command, or turn themselves off when the house goes to bed, none of which requires a visit from an electrician. Because the coordination lives in software rather than in the wiring, teaching the switches a new behavior simply requires writing another automation, not running another wire. The pairing I set up today is just the first thing I have asked them to do.

The wires in the wall didn't change; the behavior did. That's the beauty of adding a little software to a physical control: the constraints of the wiring are no longer the constraints of the room. When the layout fights you, you don't always have to open the wall. Sometimes you can just change what the switches do.


Photo Credit: Lights over the fireplace benches from Phil Windley (CC BY 4.0)


There's a Personal App for That

The QX3 microscope beside my laptop running Windley Microscope

Over twenty-five years ago, I bought an Intel Play QX3 microscope for my kids to play with. It came with a CD-ROM holding the control software, which, as I recall, ran only on Windows. The microscope then spent the better part of two decades in the back of a cabinet in my office, gathering dust.

I pulled it out recently for my grandkids, hoping the Mac might simply recognize it as a camera. It didn't. I couldn't find any software that would run on a modern machine, and I was about ready to send the whole thing to the thrift store. Then I had an idea: what if I used Claude to write an app for it?

A morning later I had one, and my grandkids were watching the ridges of a seashell fill the screen. The interesting part of this story isn't the microscope; it's that building a custom app for a single obsolete device is now a morning's work instead of a project. That change is going to reshape what we expect software to be.

A Microscope With No Software

The QX3 is a small USB microscope that Intel and Mattel sold around the turn of the century. It magnifies at 10x, 60x, and 200x (using physcial lenses, not software); it has a camera in the head, a light on top and another in the stage, and a focus wheel. For a toy, it is genuinely good, and it still works exactly as well as it did in 2000. The problem is not the hardware; it is everything the hardware expects to talk to.

The QX3 predates USB Video Class, the standard that lets a modern operating system treat any conforming camera as a webcam with no driver at all. To macOS, the QX3 is not a camera; it is an unknown vendor-specific USB device, and the system has no idea what to do with it. Over the years, hobbyists filled that gap with their own drivers and apps for Linux and the Mac. Those projects are mostly dead now.

They died for an ordinary reason. Each one depended on the operating-system internals of its moment, and macOS kept moving: it went 64-bit, it tightened what kernel extensions could do, and it reworked its USB and camera frameworks more than once. Keeping a driver alive for a discontinued children's toy is a lot of unpaid work for an audience of a few, so the maintainers moved on and the code stopped building. The hardware still worked; the software around it rotted away.

Building It With Claude

I opened Claude in Cursor and worked in stages. I didn't begin with a plan for an app; I began by trying to talk to the device at all. Each stage did one small thing and proved it worked before I asked for the next.

  1. Identify the device. I gave Claude the handful of facts the macOS "About This Mac" USB panel showed for the microscope: a vendor ID, a product ID, and not much else.
  2. Blink the lights. I had Claude write a small command-line program that turned the microscope's LEDs on and off. That was the entire test: could code I control make something physical happen across the USB cable? It could.
  3. See through it. Next came a command-line tool that pulled the image the microscope was seeing and saved a snapshot to a file.
  4. Give it a face. Then a real Mac app with a window: switches for the two lights, a brightness slider, a live preview, and a button to take a picture.
  5. Record. After that, movie capture and time-lapse, so my grandkids could watch an ant walk or a drop of water evaporate.
  6. Clean it up. Finally the work that separates a demo from a tool: an app icon, a proper Quit item in the menu, and the usual round of small bug fixes.
The Windley Microscope app showing a live view of a seashell
The Windley Microscope app showing a live view of a seashell (click to enlarge)

The result is a few hundred lines of Swift that talk to the microscope directly over USB. I called it Windley Microscope, which is the kind of name you give something built for exactly one household. It does what the original Windows software did, and only what my grandkids and I actually want it to do.

Why This Works

The QX3 is easy to bring back precisely because it is old. A modern webcam sits behind several layers: USB Video Class, the camera framework, the operating system's device and privacy management. Those layers are what make a new camera "just work," and they are also what you cannot reach around when it doesn't. The QX3 has none of them.

A seashell captured through the QX3
A seashell captured through the QX3 (click to enlarge)

It sits right on the USB wire, speaking a simple protocol, with nothing between my code and the device. The same plainness that made it invisible to macOS made it straightforward to control myself; there was no abstraction to defeat, only a device to address. Simple architecture wins. When the platform declines to capture a device, you are not locked out of it; you are left free to talk to it on your own terms.

An App for an Audience of One

Here is the part that reaches past one microscope. A few years ago I would not have spent two weeks building a custom app to run a discontinued toy for my grandkids; the effort was far larger than the result was worth. I was happy to spend a morning. That shift changes the cost dynamics of software in a profound way.

An app no longer has to be a product. It does not need a market, a store listing, or a user base large enough to justify the build. It can be personal1: written for exactly what one person needs, and for no one else. There is a massive backlog of small software applications that never get written because the audience is one (or small) and the price was two weeks (or two years). That backlog has quietly become approachable. Imagine all the things that will be built because of this profound change in cost.

An important point: this is easy on a laptop because computers still let their owners run whatever code they write and talk to whatever hardware they plug in. It is not easy on iOS, where the platform, not the owner, decides what is allowed to run. The model that let me revive the QX3 in a morning barely exists on the devices most people actually use; that is a decision about how those platforms are built, not a fact of nature. As personal software becomes ordinary, it will press on that decision.

The microscope works again, and that is a small, good thing. The larger thing I got back is the ability to make the computer do the specific thing I want, without waiting for a company to decide enough other people want it too. That capability used to belong to firms with a market to serve. It is starting to belong to the rest of us.


Notes

  1. Personal is not personalized. In 2026, a personalized system is one a company tunes to you from the data it collects about you; the tuning serves the company, and the surveillance that feeds it is the point. A personal app is the reverse: you build or commission it, it runs on your own machine, and it answers only to you. I mean personal in its older, plainer sense: software that belongs to you, not software aimed at you.

Photo Credit: The QX3 microscope beside my laptop running Windley Microscope from Phil Windley (CC BY 4.0)


Authentication Is Largely Solved. Authorization Isn't.

Authorization in Action book cover

My new book, Authorization in Action, is now available from Manning. It's a book about the question that comes after we know who you are: what are you allowed to do? Knowing who someone is doesn't tell you what they should be able to touch, change, or spend. For most of the last two decades I've worked on the first question, proving who someone is; this book is about why the second one is now the one that matters, and how to build systems that answer it well.

The short version of the argument is one I didn't expect to be making a few years ago. Authentication, the act of proving who someone is, has become about as good as we can reasonably ask; passkeys and FIDO have quietly closed most of the gap that kept me up at night in 2005. Authorization, the act of deciding what someone can actually do, is still mostly improvised, buried in application code, and reinvented badly on every team. That imbalance is the reason the book exists.

How I Got Here

When I was CIO for the State of Utah in 2001, I noticed that nearly every problem that reached my desk had an identity component hiding inside it. Consolidating directories so every employee could have a utah.gov address, moving the state's website to a new domain, sorting out who could touch which system: none of these looked like identity problems on the surface, yet all of them turned on knowing who someone was and what they were permitted to do. I didn't have language for it then, but that observation set the direction for everything I've worked on since.

A few years later Doc Searls, Kaliya Young, and I started the Internet Identity Workshop (IIW), and it has now met 42 times. IIW has become the place where the identity community works on its hardest problems together. Along the way I wrote three books; Digital Identity helped enterprises build identity strategies, The Live Web moved the focus from organizations to individuals with their own data and APIs, and Learning Digital Identity finally named the thing I'd been circling for years: identity is fundamentally about relationships, not identifiers. Each book was really an attempt to answer a question the previous one had raised.

While I was finishing Learning Digital Identity, the next question came into focus, and conversations at IIW confirmed it. Knowing who someone is tells you almost nothing about what they should be able to do; the interesting, unsolved work all lives on the far side of authentication. That's the work my new book takes on.

The Second Question

The reason the second question is harder is that it's not one question but several, and they're the ones that actually govern what happens inside a system. What can this person do; under what conditions; on whose behalf; and in which context? Miss any one of them and the rest stop meaning much; the same request can be right for a manager at noon from the office and wrong for a contractor at midnight from an unknown device. A login gets you through the door, but it says nothing about which rooms you can enter, what you can carry out, or whether the person who sent you had the standing to send you at all.

In late 2022 I joined AWS Identity, and one of the teams we worked closely with was building Amazon Verified Permissions and the Cedar policy language. Working at that scale showed me something the theory hadn't: fine-grained authorization doesn't only make a system safer, it makes it more usable. When the rules about who can do what are explicit and external to the code, you can hand people exactly the access they need without either drowning them in prompts or handing over the keys to everything. Getting authorization right is how a system stays both safe and pleasant to use, and those goals stop fighting each other.

Then Came the Agents

What finally convinced me the world needed this book was AI. As agents begin acting on people's behalf, "what is this thing allowed to do, and on whose authority?" stops being a background concern and becomes the whole game. An agent that can read your calendar, spend your money, or send mail as you is only as trustworthy as the boundaries around it, and those boundaries are authorization. I've argued before that authorization is the hard problem in agentic AI, and nothing since has changed my mind.

This is where the technical question turns into a human one. If we can't say precisely what an agent may do on our behalf, we're left choosing between agents that can't do anything useful and agents we have to trust blindly; neither of those is a world I want to live in. Authorization is the infrastructure that lets us delegate real authority to software while keeping it bounded, accountable, and revocable. That's not a convenience feature. It's what makes it possible to act through machines without surrendering to them.

What's in the Book

Authorization in Action follows a fictional company, ACME, as it works its way from the tangled access-control code most teams start with toward policy-based authorization it can actually reason about. The book uses that story to get concrete about the models and mechanics—relationships, roles, attributes, policy languages, and the architecture that separates deciding from enforcing—rather than leaving them as abstractions. This is the book I needed on my own shelf while I was solving these problems as an enterprise architect at BYU, so it's aimed at the developers and architects who have to make these decisions everyday

Identity tells us who is involved. Authorization determines what happens next. I've come to believe that second question is quietly becoming the heart of security, governance, and trust online, and I hope the book makes the case as clearly to you as the last few years have made it to me. You can find Authorization in Action at Manning and coming soon to Amazon.


Why Not SPIFFE for Pico Identity?

A printed visitor badge and a passport

When I wrote about pico-to-pico identity arriving in version 1.6, I explained why I reached for DIDs rather than KERI. The other obvious question is "Why not SPIFFE, the workload-identity stack that Kubernetes shops already run?" It's a fair question, because a pico is, from one angle, just a workload; it's a running process that needs to prove which process it is before another one will talk to it. So why give it a DID instead of a SPIFFE ID?

The answer comes down to a single property: whether the identity travels with the thing it names. SPIFFE and picos give opposite answers to that, and the opposition isn't an accident. It falls out of what each one is built to identify.

What SPIFFE Gets Right

SPIFFE is a genuinely good design for the problem it takes on. A workload asks the local agent for its identity, and instead of holding a secret it proves who it is by what it is and where it runs; the platform attests to the process, and the workload gets back a short-lived credential called an SVID. There's no long-lived key to leak, because the credential expires in minutes and renews on its own. For a datacenter full of ephemeral, interchangeable containers, that's exactly right; you don't want a Kubernetes pod hoarding a private key that outlives it.

The trust anchor for all of this is a server that runs the trust domain. That server decides which workloads exist, attests to them, and signs their SVIDs, and every workload in the domain trusts it by construction. Inside one administrative boundary that centralization is a feature, not a flaw; someone runs the cluster, and that someone is exactly who should be minting identities in it. SPIFFE fits the shape of the place it lives.

Identity that's Derived versus Identity that Lasts

That fit is also the reason SPIFFE isn't what a pico needs. A SPIFFE identity is derived: it comes from what the workload is and where it runs, and the domain's server mints it fresh in whatever environment the workload happens to land in. Move the workload to a different cluster and it gets a different identity from a different authority, because the identity was never the workload's to carry; it was a fact about the environment. That is a badge the building prints for you at the door. It works beautifully inside the building and means nothing the moment you step outside.

A pico's identity is long-lived by design. Every pico holds its own keys and carries a did:webvh that stays the same identifier no matter which engine it runs on or how long it runs; the identity is a property of the pico, not of the host underneath it. When a pico moves between engines, its identity moves with it, because the pico is the one holding the keys that prove it. That is a passport rather than a printed badge; you carry it across borders, and it still says who you are on the other side. For a system whose whole point is picos and meshes that are portable between engines, an identity that gets re-minted per environment just isn't the right choice.

Durable identity is a property that makes picos useful for tasks that ephemeral workloads can't do easily.

  • Persistent relationships. A subscription links two identifiers; if they reset on every restart, the relationship can't outlive the move. Durable identity is what lets a mesh of relationships accumulate and survive.
  • Reputation and history. Because a pico keeps the same identifier over time, the parties it deals with can remember their history with it and decide how far to trust it. An identifier that resets each session gives them nothing to base that judgment on.
  • Credentials. A verifiable credential is a claim about a subject, and the subject has to persist for the claim to keep meaning anything. Without a durable identity there is nothing to hold or present a credential.
  • Delegation. An actor operating on your behalf needs a stable identity so the authority you grant persists and stays revocable. Ephemeral identity means re-authorizing constantly and having nothing durable to take back.
  • Continuity across infrastructure. Because the keys belong to the pico, the actor is decoupled from the host; you can move engines, change providers, or recover from a crash without losing who the pico is.
  • Longevity beyond any vendor. The identity outlives the company that built the device, so a pico doesn't die when someone's cloud is switched off. This is the Internet of My Things rather than the CompuServe of Things.
  • A lifecycle that follows the thing. For something with a long life that changes hands, identity and history span custodians instead of resetting with each one.

A digital twin is a good example of where durable identity matters. A pico that stands in for an IoT device grows more valuable the longer it lives, because it accumulates the device's history, relationships, and state over years rather than minutes. When I built Fuse, each car had its own pico, and selling the car meant handing that pico to the new owner with the maintenance history and important relationships intact; the identity was bound to the vehicle, not to me. That is the case ownership language gets wrong. The pico's identity outlives any particular owner, and it has to, because the car it represents might too.

Trust Domains and Strangers

The same split shows up when two identities from different places need to trust each other. SPIFFE can federate across trust domains, but it does it the administrative way: the operators of two domains exchange trust bundles ahead of time, and only then can workloads on one side verify workloads on the other. That's reasonable when both domains belong to the same company, or to two companies that signed a contract; someone with authority on each side sets the relationship up in advance. It assumes the parties already have a reason, and a person, to arrange the introduction.

Picos owned by two different people, on two different engines, don't have that person in the middle, and I don't want to require one. A pico resolves another pico's DID and the two form a relationship directly, with no administrator on either side pre-arranging a federation. This is the same first-person idea I keep pushing for human identity, now applied to software actors: identity you present yourself, not identity an authority vouches for on your behalf. Trust between strangers shouldn't require a treaty negotiated by their landlords.

An Actor, Not a Process

None of this makes SPIFFE wrong; it's just built for a different kind of thing. SPIFFE names a process: a unit of execution that runs, does its work, and can be killed and restarted at will, where you care that some process is handling the request rather than which one. A pico is an actor: something that persists, holds state and relationships, and acts for someone or something over time. A process has an identity so the system can manage it; an actor has an identity that it uses over time.

Systems engineers have a mantra for this divide: treat your servers as cattle, not pets. Cattle are numbered and interchangeable, and when one gets sick you replace it instead of nursing it back to health; that is exactly the right way to run a fleet of workloads, and it's the world SPIFFE is built for. A pico is a pet. It has a name and a history, and relationships that make it worth keeping this one rather than swapping in another that would do the same job.

That distinction is the thread running through everything above. Persistent relationships, reputation, credentials, delegation, history, continuity, and longevity are not affordances a process needs; they are what it takes to be an actor rather than a process, and every one of them presupposes an identity that lasts. So a durable identity isn't a feature bolted onto a workload. It's foundational.

The choice between SPIFFE and DIDs, then, isn't really a choice between two identity technologies; it's a question about what you're naming. If you are naming a replaceable process inside one administrator's domain then SPIFFE is the right tool, and I'd use it. But a pico is an actor that persists over time, not a process that happens to carry an identity, and the identity model has to reflect that. Give the thing that's meant to persist and travel an identity that persists and travels with it.


Photo Credit: A printed visitor badge and a passport from ChatGPT (public domain)


Pico-to-Pico Identity Arrives

Two kinds of identity for a pico

Last month I released version 1.5 of the Pico Engine and argued that identity inside the engine is really three problems, not one. A human needs to prove who they are to a pico mesh; an outside app or webhook needs a scoped way into the mesh; and one pico needs to know which pico is calling it and whether to trust it. I shipped the human and third-party layers then and left the harder one for later. Today I'm releasing version 1.6, and that third layer, pico-to-pico identity, is here.

The short version is that every pico now has its own cryptographic identity, and that identity can travel. This is the piece I've wanted for a long time, because it's the foundation for a goal I keep circling back to: picos and whole meshes that are portable between engines rather than pinned to the machine where they happened to be born. Let me describe the shape of it, because the design turns on a distinction that took me a while to get right.

The Layer I Was Waiting to Build

I shipped the first and third layers in 1.5 and held this one back on purpose. Passkeys and OAuth sit on machinery the engine already had: channels, ECIs, and channel policy. Pico-to-pico identity required something a bit more complex. It meant moving DID keys into the engine as a core primitive, pulling a lot of KRL into wrangler, and running pico-to-pico traffic over DIDComm instead of plain HTTP. That's a large change, and it touches much of the engine, so I wanted to handle it separately.

This layer is what makes a pico an actor with a real online presence. A channel identifier tells you how to reach a pico on one engine right now; it says nothing durable about the pico's identity. For introductions between strangers, for encrypted traffic, and eventually for verifiable credentials, a pico needs a stable answer to "which pico is this?" that is portable across engines. That answer is a DID, and in 1.6 every pico has one from the moment it's created.

Two DIDs, Two Jobs

The design I landed on gives each pico two types of DIDs, each with different jobs. The first is a did:webvh, the pico's portable identity. Every pico gets one when it's created, and the engine serves its DID document at a stable URL so anyone can resolve it. Think of it as the pico's passport: it's how a pico introduces itself, and it's what you hand a stranger who needs to know who you are before they'll talk to you.

The second is a did:peer, and there's one of them for every relationship, called a subscription, a pico forms. It isn't provisioned up front; the two engines mint a fresh pair during the introduction handshake. There is a separate pair of peer DIDs for each relationship. Think of it as an email you give to exactly one person. The passport says who you are to everyone; the email is the private line for a single connection, and it means nothing to anyone else.

Subscription developer UI showing webvh DID at the top and peer DIDs in the established subscription
Subscription developer UI showing webvh DID at the top and peer DIDs in the established subscription (click to enlarge)

The developer UI makes the split concrete. The Identity panel at the top of a pico's Subscriptions tab shows its one did:webvh, the passport it hands out, next to the switch that decides whether it accepts unsolicited introductions to that DID. Each established subscription below carries a different pair: the remote party's peer DID and this pico's own peer DID, minted for that one relationship. The subscription's Rx channel is where policy still lives; the DIDs name who the two parties are, and the ECI decides what this one is allowed to do here.

That split matters more than you might think. A single, universal identifier is a mistake I've watched newcomers to identity make for years. Correlatability is a factor, but the bigger payoff is practical: because every relationship has its own identifier, each one can be managed on its own. You can rotate the keys on a single connection, or tear it down and rebuild it, without disturbing any of the others; if a peer DID is ever compromised, the damage stops there and you recover that one relationship in isolation. It's the email analogy again, where changing the address you gave one contact doesn't impact the rest.

How Picos Actually Talk

Two picos that aren't parent and child talk to each other through a subscription, which is just a pairwise relationship with a record on each side. In 1.6 you form one by handing the initiator the recipient's did:webvh as the target; the two picos run the introduction, agree, and store each other's peer DIDs. After that, queries and events flow over the peer relationship. On the same mesh the engine keeps that traffic local; across meshes or across engines it runs encrypted over DIDComm to the peer's address.

DIDs didn't need a new authorization mechanism; they slot into the channel model the engine already had. A subscription's peer DID is its channel, and channel policy governs it exactly as it governs any other channel, so authorization is enforced right where it always was. The DID names who is on the other end of the relationship, and the policy on that channel decides what they are allowed to do. Legacy ECIs haven't gone anywhere either; parent and child picos still talk over family channels named by ECIs, where a full DID would be overkill.

The consequence I care about is what this does for trust between strangers. A pico can choose to accept unsolicited introductions to its public identity, which lets a community pico or a registry take subscriptions from picos it has never met. Two meshes owned by two different people, on two different engines, can form a relationship because one resolved the other's DID and both agreed; no one had to stand up a federation agreement or register with a common broker first. The relationship is the unit of trust, and these changes make it cryptographic and portable.

Why These Methods, Not KERI

Anyone who has spent time at IIW will ask why I reached for did:webvh and did:peer rather than KERI, which answers the same "who is this actor" question with self-certifying identifiers and a key event log that needs no web host at all. KERI has a great design, and on the narrow point of surviving a move it's arguably stronger than did:webvh; a KERI identifier carries no URL that has to stay reachable, which is exactly the loose end I admitted above. So this wasn't a judgment that one approach is right and the other wrong. It came down to what picos already need to do the moment two of them are connected.

That need is messaging, and picos exchange events and queries over DIDComm. Peer dids were built for precisely that job: a pairwise, private relationship identity that DIDComm tooling already knows how to carry encrypted traffic over. Pairing it with did:webvh for the public introduction let me use a messaging philosophy the pico world has used for years since it parallels the DIDComm model. KERI's strengths live mostly in the identifier and its key history; picos needed the identifier and the encrypted conversation that follows it, and the DIDComm path gave me both with the least new machinery. If the day comes when the portability tradeoff bites hard enough, I'd happily revisit that choice.

A Step Toward Portable Meshes

I won't pretend a pico can pick up and move to a new engine today with no loose ends; the engine base URL is still baked into a pico's did:webvh, and making that survive a move is work still ahead. But the hard part, giving each pico a real cryptographic identity and a way to carry its relationships, is now in the engine rather than wrapped around it. That's the foundation portability was waiting on, and everything above it, from moving a mesh between engines to carrying credentials between them, builds on this.

Full portability is the largest gap, but it isn't the only one. The identifiers are in place; the verifiable credentials that ride on them are not, so a pico can prove who it is but can't yet hand another pico a signed claim about what it is or may do. Key management is thin across the board: the private keys behind a pico's DIDs sit in the engine's own store today, when they belong in the operating system's key vault or hardware-backed storage, and rotation and recovery need more work now that a pico's identity is the thing others rely on. Losing the keys shouldn't mean losing the pico. Authorization also still keys off the channel a caller holds rather than the identity behind it, so the natural next step is to let a receiving pico decide based on who is actually calling using a proper authorization engine. Each of these builds on the identity that landed in 1.6 rather than replacing it.

I've spent a long time arguing that people deserve software that acts for them and that they own outright, not a rented seat on someone else's platform. A mesh you can pick up and move provides infrastructure to realize that idea. If your picos can only live on one engine, you don't really own them unless everyone is running their own engines (not likely). Version 1.6 doesn't finish that story, but it lays down the identity it depends on. The next thing I want to do is exercise it in Manifold and find the rough edges by using it. If you want the details, the DID and Subscriptions pages walk through each piece.


Photo Credit: Two kinds of identity for a pico from ChatGPT (public domain)


The Pressure Behind Identity's Diseconomies of Scale

An engineer trying to hold back a high-pressure system springing leaks from every joint

Eve Maler recently unpacked a statistic that is easy to misread. In her post she reports on an IANS Research finding that identity and access management was the only security category with negative economies of scale; IAM takes 8% of the security budget at organizations under $400M in revenue but 14% at organizations over $10B. The obvious reading is that identity gets more expensive, per dollar, the bigger you get. Eve's better reading is that "gnarliness," her word for the tangle that makes identity hard, is multi-factorial, and that company size is a poor proxy for it.

She lists the real drivers: how many jurisdictions you operate in, how large your partner ecosystem is, how many apps you are wiring to an identity provider, how loosely coupled your lines of business are, and whether anyone owns identity strategically. What predicts gnarliness, she argues, is not any single factor but the unique combination your organization lives with. Reading her post, I recognized a shape I had drawn for chapter 19 of my forthcoming book, Authorization in Action. I give the tangle she describes has a name and a diagram.

Gnarliness Has a Shape

In the book I distinguish two things that grow at different rates as a system expands. The first is the decision surface: the total set of situations in which access has to be evaluated. The second is the decision infrastructure: the shared policy, consistent enforcement, contextual signals, governance, and delegation models that let those decisions be made well. The surface expands with more actors, more actions, more contexts, more delegation, and more automation. The infrastructure only expands when someone deliberately builds it.

Decision surface, decision infrastructure, and authorization pressure

The gap between the decision surface and the decision infrastructure is authorization pressure.

When the surface outruns the infrastructure, the gap between them shows up as authorization pressure: the growing difficulty of making decisions consistently, explaining why they came out the way they did, and keeping control as the system scales. Pressure is not a failure of effort; ACME, the company I follow through the book, was improving access control the whole time. Pressure is what you feel when access decisions are being made everywhere and governed nowhere. Eve's drivers of gnarliness map almost one to one onto the things that expand the decision surface; more jurisdictions maps to more context, a bigger partner ecosystem is more delegation, and more apps across more brands is more actors taking more actions.

Why Size Was Never the Predictor

Once you see identity's difficulty as pressure rather than size, Eve's objection to size as a predictor is easy to explain. Revenue and headcount tell you almost nothing about the decision surface. A small, acquisitive fintech operating in a dozen regulatory regimes, wiring together the identity systems of the companies it just bought, can carry far more surface than a giant consumer brand running one simple app for five hundred million users. The first organization is under enormous authorization pressure; the second is barely under any. Size isn't what matters in either case.

That is also why IAM shows up as the category with negative economies of scale while other security categories get cheaper per dollar. The IANS number is not measuring the cost of being big. It is measuring the pressure that accumulates when the decision surface expands faster than the infrastructure meant to govern it, and large organizations have simply had more time and more room to let that gap grow. In other words, the diseconomies of scale are a symptom of authorization pressure, and the pressure itself is what you get when the decision infrastructure falls behind the decision surface. The underlying cause is an infrastructure that never grew to match the surface, not size.

Relieving the Pressure

You cannot relieve the pressure by shrinking the decision surface, because the business is the thing expanding it. Every new partner, every new market, every agent you deploy is simply the company doing what it exists to do. The only durable move is to strengthen the infrastructure so decisions stay consistent, explainable, and bounded even as the surface grows. That means externalizing policy out of application code, evaluating decisions at runtime against relationships and attributes and context, governing the signals those decisions depend on, and making delegation explicit rather than implicit.

This is where Eve's diagnosis and mine reinforce each other most usefully. She notes that the organizations doing well with identity treat it strategically, often under someone she calls an Identity Product Owner, and that unified identity has started pushing downmarket wherever identity turns out to be a revenue multiplier rather than a cost. In the language of my book, those organizations have built decision infrastructure ahead of their decision surface, so the pressure never gets too high. Identity stops being the category that gets gnarlier with size and becomes a capability that lets a company expand the surface on purpose. A gap closed early is the difference between complexity that compounds and complexity you can manage.

The Surface Won't Stop Expanding

Here's the bad news: the decision surface is about to grow faster than it ever has. More interactions are crossing organizational boundaries; more actions are taken by services and agents instead of people; more decisions depend on real-time context that no static role can capture. I have argued in my series on agentic AI and authorization that agents expand the surface precisely because they act over time, under changing conditions, and often on someone else's behalf. The pressure Eve measured in enterprise IAM budgets is the same pressure that will decide whether agentic systems are governable at all.

That is the human stake underneath the budget line. Authorization is how we govern the ways authority gets exercised, by our people, by our partners, and increasingly by the software acting for them. When the pressure is high, people confront a digital world of inconsistent permissions and unexplainable denials, a world where no one can say why the door opened or stayed shut. When the infrastructure keeps pace, authority is legible and bounded, and people can act with confidence inside clear limits. Eve is right: gnarliness is multi-factorial and size is the wrong thing to measure. What we should measure instead is the pressure between the decisions we now have to make and the infrastructure we have built to make them well. That pressure is something we can manage.


Photo Credits: Under Pressure from ChatGPT (public domain) and Decision Surface and Decision Infrastructure, from Chapter 19 of Authorization in Action (Manning)