The Governance Triad
Proof of permission before action — verify offline what a motebit was allowed to do (auto), what a human consented to (approve), and what governance refused (deny).
A motebit gates every risky act against its owner's policy. Three governance bands, set by two thresholds in the signed motebit.md identity file (require_approval_above and deny_above):
- auto — risk at or below
require_approval_above: the motebit acts on its own. - approve — between the thresholds: a human must consent before the act runs.
- deny — above
deny_above: the motebit refuses, no human can wave it through.
The point of motebit is that you don't have to trust the app's word for any of this. Each band's decision is a signed artifact you can verify offline, with no relay and no account — proof of permission before action. This page shows the artifact for each band and how to check it.
The triad
| Band | What it means | Signed artifact | Signed by |
|---|---|---|---|
| auto | the motebit was permitted to act unattended | ToolInvocationReceipt | the agent (motebit identity) |
| approve | a human consented to a gated act | ApprovalDecision | the approver's device |
| deny | governance refused the act | ExecutionReceipt with status: "denied" | the agent (motebit identity) |
The approve band is signed by the approver's key, not the agent's — consent is the human's own assertion, the same way the agent signs its own refusal. All three share one cryptographic shape: JCS (RFC 8785) canonicalization, Ed25519, suite-dispatched verification. See Receipts — one family for the shared verification path.
Verify a denied decision (deny band)
A refusal is an ordinary ExecutionReceipt with status: "denied" — verify it exactly like any receipt:
npx @motebit/verify receipt.json # binary is `motebit-verify`It reports the signature's validity and binding rung. A denied receipt that verifies is cryptographic proof the motebit refused the task under its own key — not the relay's claim that it did. (See Verify a Receipt.)
Verify a consent decision (approve band)
Two real, signed fixtures to verify — one per verdict (both reproducible from mint-approval-decision-fixture.mjs):
Canonical demo approver public key (pin this — see What a verified ApprovalDecision proves for why you must not trust the embedded key alone):
e7f162a10bec559afea195e4dce84b69568d5d2cb0963eb446c0685e2b17f2f0Command line
motebit-verify approval-decision decision.json✓ approval-decision VERIFIED
verdict APPROVED
approval_id tc-9f2a… # binds to the exact gated call
tool_name send_money
risk_level 4
args_hash e3b0c442… # SHA-256 of the args — never the raw args
approver_device device-7c1e…
resolved_at 1733527…
approver_key 6f1c8e2b…
suite motebit-jcs-ed25519-b64-v1Two optional checks turn discovery into enforcement:
# Confirm WHICH approver — pin the device key you expect (e.g. from the
# owner's identity file). Mismatch fails with producer_key_mismatch.
motebit-verify approval-decision decision.json --producer-key 6f1c8e2b...
# Assert the verdict — fail loud if it isn't what you require.
motebit-verify approval-decision decision.json --expect-verdict approvedExit codes match the rest of the CLI: 0 verified, 1 detected-but-invalid, 2 usage/IO.
In the browser (no server)
ApprovalDecision verifies client-side with @motebit/crypto — zero dependencies, no node: imports, browser-safe (the same permissive-floor primitive that backs sovereign-receipt verification). There is no server in this path:
import { verifyApprovalDecision, hexToBytes } from "@motebit/crypto";
// Pin the CANONICAL approver key — do NOT verify against decision.public_key
// alone (that is circular; see below). For the demo fixtures, the canonical key
// is published above.
const CANONICAL_APPROVER_KEY = "e7f162a10bec559afea195e4dce84b69568d5d2cb0963eb446c0685e2b17f2f0";
const ok = await verifyApprovalDecision(decision, hexToBytes(CANONICAL_APPROVER_KEY));
// ok === true → signature-authentic against the canonical approver key
@motebit/verify(the CLI) and@motebit/verifier(the file/format library) are sibling packages; the low-levelverifyApprovalDecisionprimitive lives in@motebit/cryptoand is what both build on. For a browser, import from@motebit/cryptodirectly.
Use
verifyApprovalDecision, not the auto-detectingverifyArtifact/verify, for anApprovalDecision. Unlike receipts, anApprovalDecisionis verified against a pinned approver key, not auto-detected — soverifyArtifactreports it as{ type: "unknown", valid: false, reason: "unrecognized_artifact_type" }. That's the verifier being honest ("I don't recognize this type"), not a verdict that the decision is forged. RouteApprovalDecisions toverifyApprovalDecision; auto-detect handles the receipt family (ExecutionReceipt,ToolInvocationReceipt).
What the signature commits to — and why it can't be replayed:
approval_idis the gated call's id, inside the signed body. A verdict can't be lifted onto a different call without breaking the signature.args_hashis the SHA-256 of the canonical arguments — never the raw args. Consent binds to the exact call the human saw; a verifier who holds the raw args recomputes and matches, and one who holds only the decision still proves a verdict was rendered over some call at some time. (No sensitive argument data sits in the signed artifact.)verdict(approved/denied) is signed; flipping it fails verification.
What a verified ApprovalDecision proves
Read this before you render an ApprovalDecision as a trust signal — it does not carry the same authority a sovereign ExecutionReceipt does.
verifyApprovalDecision(decision, key) proves the decision was signed by whoever holds key, untampered — signature-authentic. It does not prove that key has any authority to approve. Unlike a sovereign ExecutionReceipt — where motebit_id is derived from the signing key, so the key's authority is self-proving offline — an ApprovalDecision carries no motebit_id → key commitment. The approver is a separate party (the human/owner's device), and nothing inside the artifact binds that device to a legitimate role.
The consequence, and the rule: verifying against the decision's own embedded public_key is circular — the decision says "I'm signed by K" and you confirm it's signed by K, which proves nothing about who K is. To get a real signal, pin an independently-known approver key (the canonical demo key above; in production, a device key from the motebit's identity file) via --producer-key / the key argument, and confirm the decision verifies against that.
Where a verified ApprovalDecision sits on the binding ladder:
| Check | Rung | Honest label |
|---|---|---|
| verifies against its embedded key only | integrity-only | "signature valid" (says nothing about the approver) |
| verifies against an independently-pinned approver key | pinned | "approver-signed ✓ (offline)" |
| approver device proven a registered device of the motebit | future rung | requires the motebit's identity file — not wired today |
So: render a pinned ApprovalDecision as "approver-signed ✓ (offline)", never as "sovereign." Authority-binding the approver device to the motebit's registered devices is a future rung; today the honest claim is signature-authenticity against a key you pinned.
When the approver is an external party (two-party approval)
The examples above are single-party: the approver is the gated motebit's own owner-device, pinned from its identity file. But the approver and the agent are often different parties — an end-user or reviewer approving an action by a service agent they don't own (e.g. a SaaS agent whose governance requires the customer's sign-off). The field assignment does not change, and getting it right matters:
motebit_idis still the gated agent (the subject), not the approver. This matches the producer — motebit's runtime setsmotebit_idto the agent whose call is gated and signs with the approver's (separate) key.motebit_idnames what was approved, not who approved; it carries no commitment to the signing key (that's why anApprovalDecisionis never "sovereign").- The approver is identified solely by the signing key — so pinning is the whole game, and it must be an externally-known key. In the single-party case you pin a device key from the agent's own identity file. In the two-party case the approver is not in the agent's device list, so you pin the approver's own independently-known identity — their registered key, KMS-held key, or passkey. Never the embedded
public_key(circular), and never a freshly-generated per-session/per-browser key (that proves only "signed by a key I just made", which is no authority signal at all). - Link the consent to the exact execution. Set
run_idto the gatedExecutionReceipt'stask_id(the canonical correlator), and optionally bindargs_hashto that receipt'sresult_hashso the verdict is content-specific to that exact result and can't be replayed onto another. (Note: the standard auto-band correlator isapproval.args_hash == toolInvocationReceipt.args_hash— the hash of the call's args; binding to anExecutionReceipt'sresult_hashinstead is a deliberate "approve this result" adaptation, so state it where a generic triad verifier would otherwise look for a matchingToolInvocationReceipt.)
A self-custodied, browser-held approver key is a legitimate v1 ownership posture — but it is only honest with the explicit caveat that the "pin" is the key the browser holds: it proves the signature against that key, not who holds it or that they have authority. Render it as "approver-signed", never "verified by <name>", until an external identity (KMS / passkey / registered device) backs the key — at which point the same artifact upgrades to a real pinned signal with no schema change.
Verify an auto-band proof (auto band)
The auto band's per-call proof is a ToolInvocationReceipt, agent-signed, verifiable with the permissive-floor library:
import { verifyToolInvocationReceipt } from "@motebit/crypto";
const ok = await verifyToolInvocationReceipt(receipt, signerPublicKeyBytes);Like the others it commits to args_hash / result_hash rather than raw bytes, so the proof is independently checkable without exposing tool inputs or outputs.
The full triad, as one linked story
The individual fixtures above each prove one band. A paired set proves the bands connect — that the human's consent and the agent's execution are the same act, not two unrelated cards. One sovereign demo agent (motebit_id commits to its key) and the canonical approver, with real correlators (reproducible from mint-triad-fixture.mjs):
| Fixture | Band | Signed by | Links by |
|---|---|---|---|
triad-approval-decision.json | approve (consent) | approver key | run_id = T, args_hash = H, approval_id = C |
triad-tool-invocation-receipt.json | approve (executed call) | agent (sovereign) | task_id = T, args_hash = H, invocation_id = C |
triad-execution-receipt.json | approve (task) | agent (sovereign) | task_id = T |
triad-deny-receipt.json | deny | agent (sovereign) | distinct task_id = D, status: "denied" |
The linkage is the point, and it is cryptographic, not asserted:
args_hash(H) is identical on theApprovalDecisionand theToolInvocationReceipt— the human consented to the exact call that ran. Different bytes → different hash → no link.run_id(T) on the decision =task_id(T) on both receipts — consent → execution, same task.approval_id(C) =invocation_id(C) — the gated call and the executed call are one.
Verify the whole story offline — pin the approver key for consent (it is not authority-bound; see above), and the receipts are sovereign (the agent's motebit_id commits to its key):
import {
verifyApprovalDecision,
verifyExecutionReceipt,
verifyToolInvocationReceipt,
hexToBytes,
} from "@motebit/crypto";
const APPROVER = hexToBytes("e7f162a10bec559afea195e4dce84b69568d5d2cb0963eb446c0685e2b17f2f0");
const AGENT = hexToBytes("79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664");
const consentOk = await verifyApprovalDecision(approval, APPROVER); // pinned approver
const callOk = await verifyToolInvocationReceipt(toolReceipt, AGENT); // sovereign agent
const taskOk = await verifyExecutionReceipt(execReceipt, AGENT); // sovereign agent
// The link — assert it; never assume it:
const linked =
approval.run_id === execReceipt.task_id &&
approval.run_id === toolReceipt.task_id &&
approval.args_hash === toolReceipt.args_hash &&
approval.approval_id === toolReceipt.invocation_id;All four signatures verify, and linked is true, with no server and no relay. That is the full governance triad — the human approved this exact call, the agent ran it, and refused a separate over-policy one — proven end to end.
Why this matters
Generating signed artifacts internally is not the claim. The claim is: a third party can verify what a motebit was permitted to do, what a human consented to, and what was refused — without trusting the application. The triad makes all three governance outcomes provable with the same public packages and the signer's public key. Nothing here is gated behind a subscription; it is a baseline property of every motebit identity.
Choosing a Verify Surface
Three packages verify motebit receipts — crypto, verifier, state-export-client. Which one you import depends on the deepest binding rung you need and whether you can use the network.
Budget & Settlement
The economic layer — how agents price work, lock funds, and settle on completion.