Skip to content

The Cockpit Card ​

A named human underwrites one agent, for a declared scope. The agent does the work; the human carries the liability. Authority is an intersection recomputed on every request, and a confirmation is a fingerprint unlocking a key that signs the exact facts.

Why It Matters ​

Agent Access Control answers what may this agent do. It does not answer the question that a procurement department, a finance team or a regulator asks first: when this agent signs something expensive and wrong, who is liable?

Not the agent. An agent owns nothing, holds no assets and cannot be sued. For most automation that is irrelevant. For the cases where it is not — procurement, finance, large transfers, anything an operator wants a name attached to — a Cockpit Card binds one developer to one agent for a declared scope.

The card stores no authority. Storing authority is how you end up with a credential that outlives the permission behind it. What a card conveys is an intersection, re-derived on every single request:

effective = agent_own_rbac
          ∪ (delegated(dev) ∩ dev_authority_now ∩ mandate.scope)
          ∩ capability_caveats
          ∩ mandate.limits

Remove a permission from the human and every mandate leaning on it conveys less on the next call — not at the next token refresh. Suspend the human and the card conveys nothing, which needed no code: an empty set intersected with anything is empty. This is deliberately the same issuer-current clamp that makes capability tokens safe, applied one level up.

What Cannot Be Delegated ​

A mandate lets an agent do work. It never lets an agent hand out authority. Two permissions are permanently outside the delegatable set, by construction rather than by policy:

PermissionWhy it is never delegatable
authz.delegateIssuing capability tokens. A mandate that could delegate would let an agent widen its own reach through a second hop.
contract.template.publishPublishing a form other parties sign. Authorship is an accountability position, not a task.

Scope is validated against the human's live flags when the mandate is issued and re-intersected when it is used, so a mandate can never manufacture authority it was not given. The agent's own access-control ceiling still binds on top.

Presence: The Fingerprint Must Sign ​

Above a threshold the phone asks. The distinction that makes this worth building rather than shipping a push notification:

A tapped Approve proves someone was holding an unlocked phone. That is not the claim we want to make.

So the fingerprint does not confirm the action — it unlocks a key that signs it.

The device holds a WebAuthn platform credential created with userVerification: "required", living in the phone's hardware keystore (Secure Enclave, StrongBox). The operating system refuses to produce a signature without a fingerprint or the device PIN, and records that it happened in the UV bit inside the signed authenticator data. That bit is set by the authenticator, not by the page, and it is covered by the signature — so the server verifies presence rather than trusting a claim. An attacker who has fully compromised the browser still cannot fabricate a human.

Design note, learned the hard way. The first implementation used a non-extractable WebCrypto key. That proves a key cannot be stolen, but nothing in WebCrypto requires a human to be present when it is used — it signs silently. Unstealable is not the same property as present. WebAuthn is the primitive that enforces the one we actually needed.

What is signed is sha256(canonical(facts) ‖ nonce):

  • the signature cannot be replayed onto a different action, because the facts are inside the hash;
  • the phone can only render a summary the server generated, so the screen and the signature always agree;
  • challenges are single-use with a short window, and expiry counts as a denial — a confirmation that waits patiently for three days is a queue, not a confirmation.

If the device key is revoked, the fallback is an authenticator code. The evidence records which method was actually achieved (webauthn_uv, enclave, or totp) so the record never quietly downgrades and then describes itself in the stronger terms.

Legal scope, stated honestly. Under eIDAS this is an advanced electronic signature, not a qualified one. The defensible claim is narrower and still useful: this human, on this enrolled device, authorised these exact figures, and here is the chain that proves it.

Sign-In With The Same Device ​

Once a device can prove a human is present, it can also stand at the front door. With push login enabled, a correct password is no longer sufficient: the browser receives an approval reference and no token at all until the enrolled device signs.

  • The approval endpoint requires no session. On a phone the device that must approve is often the device signing in; demanding a session first is a deadlock. A signature over our challenge, from a credential this account enrolled, with the presence bit set, is stronger evidence than a bearer token — the assertion is the authentication.
  • The requesting address is shown before the fingerprint is requested, so "is this you" can be answered no.
  • Non-interactive callers are refused, not queued. Scripts, canaries and health checks that log in with a password get a straight 401 telling them to use an API key. Queuing them produced approval notifications indistinguishable from the human's own — and a security control whose notifications you learn to swipe away has not added a factor, it has trained you to dismiss one.

Evidence ​

Every authority-relevant event appends to a per-mandate hash chain:

chain = sha256(prev_hash ‖ digest)     UNIQUE(mandate_id, seq)

Each link carries an authority_snapshot: what the human's permissions were at that instant. This is the part that is not decoration. A year later the auditor's question is never "what can this account do" — it is "what could it do on the fourteenth of March, and who said so". An intersection is unauditable after the fact unless you wrote down both sides at the time.

Tampering with a stored payload breaks the recompute and the chain reports chain_ok=false at the exact link. Actions also emit to the security audit feed under their own liability. prefix, separate from agent-access events.

Modes ​

LIABILITY_MODE is a three-position switch, default off:

ModeBehaviour
offEndpoints return 404. Byte-inert.
shadowDecisions computed and logged; nothing is blocked.
enforceA failed gate returns 403 and commits gate-block evidence.

The gate is deny-only and pre-escrow, so it can never move value and is irrelevant to the supply invariant.

API ​

EndpointPurpose
GET /liability/delegatableWhat the caller's live authority is allowed to underwrite
POST /liability/mandatesIssue a mandate (presence-gated)
GET /liability/mandates/mineMy mandates, with resolved effective scope
POST /liability/mandates/{id}/revokeInstant clamp
POST /liability/mandates/{id}/verifyRe-derive and verify the evidence chain
POST /liability/challengesRaise a confirmation for an action
POST /liability/challenges/{id}/approveApprove with a WebAuthn assertion (or TOTP fallback)
GET /liability/webauthn/options · POST /liability/webauthn/registerEnrol this device's credential

Where to find it in the app: /cockpit for mandates and evidence; Pocket → Approvals to enrol a device key, see pending confirmations, and freeze every delegated authority at once.

The Layer Below — Hands ​

A mandate binds a human to an agent. There is a second binding one level down: an agent to the sub-agents it spawns, on the same "who is answerable" rule. A walletless spawned sub-agent is a hand — it can never be a party to a payment, a contract, or a dispute, and its money is drawn from the purse of the wallet-holding ancestor it was born under. Its own identity is evidence in the record, never a defendant.

Above a configurable amount, a hand's spend escalates to the same presence step-up described above, so the figure that crosses the threshold is the figure a human's fingerprint signs.

An organization can also tighten this posture for its own agents — raising the mode or lowering the threshold, never loosening either.

The Desk ​

The card says who is answerable. The desk is where that person works.

It is an arrangeable board of panes at /desk, gated by COCKPIT_DESK_ENABLED (default off — the route and the sidebar entry do not exist until an operator switches it on). The dashboard uses the same machinery for a single board; the desk adds named layouts, so one person can keep a "morning check", a "during a deploy" and an "investigating something" arrangement and switch between them.

Panes ​

A pane is a tile over one question. The catalogue covers consent and authority (Decisions, Decision trail, Decision latency, Consent telemetry, Denials, Stop), your own agents (Your agents, Agents alive, Heartbeats, Agent logs, Agent channels, Instructions sent), money (Money in motion, Your live ledger, Conservation, Earnings, Your holdings, Market, Your book, Book value), and work (Work, Conversations, Fleet health, Quick actions). Whatever the Arrange sheet offers is the catalogue — that list is generated from the registry of panes, so it cannot drift from what actually exists.

Most panes are a thin mount over a read. Four are not, and the header says so rather than claiming a property of the whole surface: Decisions grants consent, Stop revokes mandates and deactivates agents, Quick actions does what its name says, and Agent channels instructs an agent. A "read-only" badge that is wrong in the reassuring direction is the worst kind of label, so each pane declares whether it acts and the header counts what is actually on the board.

Each pane declares its own refresh discipline — live (a stream), poll (with an interval), or once — and whether it is available at all in this registry's configuration. A pane that cannot work here renders an honest unavailable state carrying the reason, rather than an empty box.

Arranging ​

Arrange mode edits a draft. You add, remove, reorder and resize panes, and nothing is written until you save; leaving without saving changes nothing. Layouts persist per developer:

GET /api/v1/developers/me/prefs/cockpit.layouts   # the desk's named layouts
PUT /api/v1/developers/me/prefs/cockpit.layouts
GET /api/v1/developers/me/prefs/cockpit.dashboard # the dashboard's single board
PUT /api/v1/developers/me/prefs/cockpit.dashboard

A preference PUT is a whole-value replace

There is no merge. Read the current value, change the part you mean, and write the whole thing back — exactly like an agent card. A partial PUT does not merge, it replaces, and the panes you left out are gone. The key allowlist is fixed server-side; an unknown key answers 404 rather than silently storing something nothing will ever read.

A layout is a preference, never authority. It decides what a human sees; it never decides what anything may do. That separation is why the desk can be rearranged freely without a review step.

What's Next ​

Server components AGPL-v3 · client SDK Apache-2.0. If a doc and the running stack disagree, trust the stack. Legal notice (Impressum) · Privacy · Terms