Identity System

Identity System

The identity system in the Pico Engine is available in versions 1.6 and higher.

The pico-engine identity model separates who you are (human access to your mesh), what your picos are (pico-to-pico identity), and who third parties are (external apps and webhooks). Each layer uses a different mechanism, but they work together: you sign in with a passkey, your root pico acts as your agent in the mesh, and external callers use OAuth to reach the public API.

This page is the overview. Detailed guides live on the child pages linked below.


Three planes of identity

Plane

Question it answers

Mechanism

Status

Plane

Question it answers

Mechanism

Status

Human → root pico

Is this the right person for this account?

Passkeys (WebAuthn)

Available

Pico → pico

Which pico is calling, and is it allowed?

DID/DIDComm + channel policy

Available

External app → mesh

May this third party call the public API?

OAuth 2.0

Available

Human → root pico (passkeys)

Every account is tied to a root pico. When you register or sign in, you use a passkey — a device-backed WebAuthn credential (Touch ID, Windows Hello, a security key, etc.). There is no shared password and no central user database: each root pico is its own WebAuthn relying party, and the passkey proves you control that account.

The root pico is your agent in the system. You authenticate to it; it holds your mesh, rulesets, and cryptographic identity for pico-to-pico communication.

→ See: Passkey authentication

Pico → pico (DID/DIDComm)

Since pico-engine 1.6, every pico has a portable did:webvh identity that provides the answer to “which pico is this?” for introductions, discovery, and (eventually) verifiable credentials. When two unrelated picos form a relationship, they run a SKY intro handshake; after approval, the relationship stores pairwise did:peer DIDs for ongoing encrypted traffic.

Three identifiers show up in practice:

Identifier

Role

Identifier

Role

did:webvh

Portable pico identity — share this when someone wants to form a relationship with you (wrangler:myDid(), relationships → Identity)

did:peer

Private relationship identity — minted during intro; used for queries and events after the relationship is established

ECI

Local channel on this engine — relationship Rx policy is enforced here (classic ECI-based relationships also use a remote Tx ECI)

DID-based relationships are the default for new work: raise wrangler:relationship with layer2: true and target_did set to the recipient’s did:webvh (not a peer DID, not a wellKnown ECI). ECI-based relationships — the classic wellKnown_Tx handshake — remain fully supported for existing rulesets and tutorials.

This plane is separate from how humans sign in: you use a passkey to reach your mesh; picos use DIDs and channel policy to talk to each other. It is also separate from OAuth: external apps present Bearer tokens on /sky/*; pico-to-pico traffic uses SKY over DIDComm when picos are on different meshes or engines, and stays on verified local dispatch when both picos share a mesh on the same engine.

Root picos default to public intro (accept unsolicited SKY intro on did:webvh); child picos default to off. Toggle in the developer UI Relationships tab or with wrangler:set_public_intro.

Details: Relationships (formation, lifecycle, routing, cross-engine)

Deprecated: io.picolabs.did-o — use wrangler:picoQuery and event:send with a DID or an established relationship map.


Short enough for an overview; the relationships page carries the how-to. Say if you want it tighter (drop the table or the deprecated line).

External app → mesh (OAuth)

Third parties — Home Assistant, inbound webhook senders, future integrators — cannot use your passkey session. They authenticate with OAuth 2.0 and present a Bearer token on the public /sky/* API.

Two grant types cover the two main external patterns:

Grant

Typical use

Scope

Grant

Typical use

Scope

Client Credentials

Inbound webhooks (Helium, Stripe, …)

Single channel (one ECI)

Authorization Code + PKCE

User-connected apps (Home Assistant, dashboards)

Whole mesh under a root

Both can coexist in the same mesh. They share the same token store and Bearer middleware; validation differs by grant type.

→ See: OAuth — Client Credentials (webhooks)
→ See: OAuth — Authorization Code Grant (apps)


Two HTTP surfaces

The engine exposes two channel HTTP prefixes. Treat them as different audiences, not interchangeable paths.

Prefix

Audience

Authentication

Prefix

Audience

Authentication

/c/*

Engine UI, in-engine KRL HTTP loops

Passkey session cookie

/sky/*

External callers (webhooks, integrators, Manifold-style apps)

OAuth Bearer token when required; otherwise channel policy only

Rule of thumb: If you are building something outside the engine — a webhook receiver, Home Assistant, a mobile app — use /sky/* only. Never depend on /c/* for third-party access.

When OAuth is required on /sky/*

Bearer tokens are enforced on /sky/* when either:

  1. The channel was created with the oauth-webhook tag (webhook Client Credentials), or

  2. The optional ruleset (part of Wranglerio.picolabs.oauth is installed on the root pico (mesh lock).

Without mesh lock, open channels continue to work under channel policy alone. Installing io.picolabs.oauth on the root requires a valid Bearer token on all /sky/* requests for that mesh — the standard setup for Authorization Code apps and for locking down external access.


How the pieces fit together

Human Engine External caller │ │ │ │ passkey (WebAuthn) │ │ ├────────────────────────►│ session cookie on /c/* │ │ │ (UI, settings, channel mgmt) │ │ │ │ │ │◄─────────────────────────────────┤ │ │ Bearer oat_… on /sky/* │ │ │ (OAuth — CC or ACG) │ │ │ │ │ consent (ACG only) │ │ ├────────────────────────►│ /oauth/authorize → approve │ │ │ │
  1. You register or sign in with a passkey → the engine issues a session used by the developer UI and session-gated management APIs.

  2. You (or your automation) create webhook credentials or register OAuth apps from the UI while signed in.

  3. External systems exchange credentials at POST /oauth/token and call /sky/* with Authorization: Bearer <token>.

  4. For Authorization Code apps, the user must sign in and approve access on a consent page before the app receives tokens.

Channel ECI stays in the URL for routing (/sky/query/<eci>/…). Access tokens are opaque bearer strings (oat_…); they are not the ECI itself.


Accounts, roots, and access control

One account, one root

Each registration creates an account and a root pico (1:1). The first registration on a fresh engine is bootstrap — no separate setup token. Additional accounts require either open self-signup (environment variable) or an invite link from an existing user.

Legacy engines with a root but no auth accounts can claim the existing root with a passkey instead of creating a new one.

Admin vs conferred rights

  • Admin rights follow the pico parent–child tree: control over your root and its descendants.

  • Other rights (read access, specific events, etc.) are conferred through policy — channel rules, relationship tags, and (eventually) verifiable credentials — without requiring admin status in the hierarchy.

Passkey login establishes who you are. The hierarchy and channel policy determine what you (or a token) may do.


Choosing the right mechanism

I want to…

Use

I want to…

Use

Use the developer UI

Passkey sign-in (session on /c/*)

Let a webhook POST events to one channel

Client Credentials on that channel (oauth-webhook tag)

Let an app query many picos under my root

Install io.picolabs.oauth, register an OAuth app, Authorization Code + PKCE

Call the engine from KRL on the same host

/c/* (localhost session bypass by default)

Lock all external API access on my mesh

Install io.picolabs.oauth on the root


Configuration

Identity-related environment variables:

  • PICO_ENGINE_BASE_URL — public URL for passkey relying-party origin and OAuth redirects (must match what users see in the browser).

  • PICO_ENGINE_ALLOW_SELF_SIGNUP — allow open registration after bootstrap (true / 1).

  • PICO_ENGINE_ALLOW_LOCALHOST_C — set to 0 to require a passkey session on /c/* even from localhost (default allows localhost for in-engine HTTP loops).

See the Environment variables page for the full list.