gabriel / musehub public
docs_muse_identity.html html
2,012 lines 105.8 KB
Raw
sha256:8a1389ae62d0a688d5e027763249bd8faedfc39ab00857d65da6620d1ab8a689 Merge 'feat/9a-4-f7-overseer-provenance' into 'dev' — propo… Human 4 days ago
1 {% extends "musehub/base.html" %}
2
3 {% block container_extra_class %} page-container{% endblock %}
4 {% block body_class %}app-shell{% endblock %}
5 {% block title %}Cryptographic Identity — Muse Developer Docs{% endblock %}
6 {% block page_json %}{"page":"docs-identity"}{% endblock %}
7
8 {% block content %}
9 <div class="devdocs">
10 <div class="devdocs-layout">
11
12 {# ── Sidebar ─────────────────────────────────────────────────────────────── #}
13 <aside class="devdocs-sidebar">
14 <nav class="devdocs-nav" aria-label="Docs navigation">
15 <div class="devdocs-nav-group">
16 <div class="devdocs-nav-group-label">Sections</div>
17 {% for slug, num, title, desc in phases %}
18 <a class="devdocs-nav-link devdocs-nav-link--phase{% if slug == current %} devdocs-nav-link--active{% endif %}"
19 href="/muse/{{ slug }}">{{ num }} {{ title }}</a>
20 {% endfor %}
21 </div>
22 <div class="devdocs-nav-group">
23 <div class="devdocs-nav-group-label">On this page</div>
24 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#key-model">Key model</a>
25 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#path-segments">Path segments</a>
26 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#entity-id">entity_id</a>
27 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#role-keys">Role keys</a>
28 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#msign">MSign auth</a>
29 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#identity-toml">identity.toml</a>
30 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#codec">Codec</a>
31 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#trust-chain">Trust chains</a>
32 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#attestations">Attestations</a>
33 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#invariants">Invariants</a>
34 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#quorum">Quorum</a>
35 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#cli">Auth CLI</a>
36 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#key-rotation">Key rotation</a>
37 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#agent-keys">Agent sub-keys</a>
38 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#identity-repos">Identity repos</a>
39 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#recover">Recovery</a>
40 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#migrate">Migration</a>
41 <a class="devdocs-nav-link devdocs-nav-link--sub" href="#mpay">MPay</a>
42 </div>
43 </nav>
44 </aside>
45
46 {# ── Content ─────────────────────────────────────────────────────────────── #}
47 <article class="devdocs-content">
48
49 <div class="devdocs-breadcrumb">
50 <a href="/muse">Developer Docs</a>
51 <span>›</span>
52 <span>Cryptographic Identity</span>
53 </div>
54
55 <div class="devdocs-phase-header">
56 <span class="devdocs-phase-num">PHASE 02</span>
57 <h1 class="devdocs-phase-title">Cryptographic Identity</h1>
58 <p class="devdocs-phase-desc">
59 Every actor in the Muse ecosystem — human, agent, or organisation — has a
60 cryptographic identity derived from a single HD wallet seed using SLIP-0010.
61 There are no passwords, no JWTs, no OAuth flows. Every HTTP request is
62 signed with Ed25519 and verified at the hub before any data is touched.
63 In addition to humans, agents and organizations are first-class citizens
64 with their own derived keys, and every commit carries a tamper-evident
65 provenance chain traceable back to a root mnemonic.
66 </p>
67 </div>
68
69 {# ── Key model ───────────────────────────────────────────────────────────── #}
70 <section class="devdocs-section" id="key-model">
71 <h2 class="devdocs-section-title"><a href="#key-model">Key model</a></h2>
72
73 <p>
74 Muse uses <strong>Ed25519</strong> (RFC 8032) throughout. Every human identity
75 is backed by a BIP-39 mnemonic — the root of an HD wallet. Child keys are
76 derived deterministically using SLIP-0010 (the standard for non-secp256k1
77 curves), so an agent spawned from a human inherits a key derived from that
78 human's seed at a specific HD path. You can rotate, recover, and audit the
79 entire chain from one secret.
80 </p>
81
82 <h3 class="devdocs-subsection-title"><a href="#derivation-path">Derivation path</a></h3>
83
84 <p>
85 Muse's HD path has six levels. Every level answers exactly one question:
86 </p>
87
88 <div class="devdocs-code-block">
89 <div class="devdocs-code-header">
90 <span class="devdocs-code-lang">text</span>
91 <span class="devdocs-code-label">HD path structure</span>
92 </div>
93 <pre><code>m / purpose' / domain' / entity_type' / entity_id' / role' / index'</code></pre>
94 </div>
95
96 <p>
97 The <code>purpose'</code> level is <code>1075233755'</code> —
98 <code>int.from_bytes(sha256(b"muse")[:4], "big") &amp; 0x7FFFFFFF</code> —
99 a hardened namespace that separates Muse keys from every other HD wallet
100 application at the root. All six levels use hardened derivation
101 (<code>'</code>), so no child key ever exposes its siblings or parent.
102 </p>
103
104 <h3 class="devdocs-subsection-title" id="path-segments"><a href="#path-segments">Path segment reference</a></h3>
105
106 <table class="devdocs-table">
107 <thead>
108 <tr><th>Level</th><th>Name</th><th>Answers</th><th>Example</th></tr>
109 </thead>
110 <tbody>
111 <tr>
112 <td><code>purpose'</code></td>
113 <td>Purpose</td>
114 <td>Which application?</td>
115 <td><code>1075233755'</code> — sha256(b"muse")[:4] &amp; 0x7FFFFFFF, hardened at the root to isolate Muse keys from every other HD wallet app</td>
116 </tr>
117 <tr>
118 <td><code>domain'</code></td>
119 <td>Domain</td>
120 <td>Which capability universe?</td>
121 <td><code>1660078172'</code> for Identity (MSign auth); <code>678195575'</code> for Code (commit signing). Hash-derived from the domain name string — open namespace, no committee needed</td>
122 </tr>
123 <tr>
124 <td><code>entity_type'</code></td>
125 <td>Entity type</td>
126 <td>What class of principal?</td>
127 <td><code>0'</code> = human, <code>1'</code> = agent, <code>2'</code> = org</td>
128 </tr>
129 <tr>
130 <td><code>entity_id'</code></td>
131 <td>Entity ID</td>
132 <td>Which specific principal of that type?</td>
133 <td>For humans, always <code>0'</code> — there is only one you per mnemonic. For agents, each slot gets a different index: agent <code>0'</code>, agent <code>1'</code>, agent <code>2'</code>, … A music composition agent and a code review agent can each have their own sub-seed derived at different entity_id values, even within the same domain</td>
134 </tr>
135 <tr>
136 <td><code>role'</code></td>
137 <td>Role</td>
138 <td>What does this key do?</td>
139 <td><code>0'</code> = sign (MSign auth, commit signing); <code>1'</code> = receive (MPay payment address). Same identity, same domain, but different keys for different cryptographic purposes — see role table below</td>
140 </tr>
141 <tr>
142 <td><code>index'</code></td>
143 <td>Index</td>
144 <td>Which rotation?</td>
145 <td><code>0'</code> is the original key. Each call to <code>muse auth rotate</code> increments to <code>1'</code>, <code>2'</code>, … The previous key stays valid until you explicitly revoke it, so rotation is always overlap-safe</td>
146 </tr>
147 </tbody>
148 </table>
149
150 <p>
151 <code>muse auth show --json</code> prints your full HD identity — path decoded,
152 public key, fingerprint, and hub registration timestamp:
153 </p>
154
155 <div class="devdocs-code-block">
156 <div class="devdocs-code-header">
157 <span class="devdocs-code-lang">bash</span>
158 <span class="devdocs-code-label">show full HD identity</span>
159 </div>
160 <pre><code>muse auth show --json</code></pre>
161 </div>
162 <div class="devdocs-code-block devdocs-code-block--output">
163 <div class="devdocs-code-header">
164 <span class="devdocs-code-lang">json</span>
165 <span class="devdocs-code-label">output</span>
166 </div>
167 <pre><code>{
168 <span class="tok-key">"hub"</span>: <span class="tok-str">"staging.musehub.ai"</span>,
169 <span class="tok-key">"handle"</span>: <span class="tok-str">"gabriel"</span>,
170 <span class="tok-key">"type"</span>: <span class="tok-str">"human"</span>,
171 <span class="tok-key">"algorithm"</span>: <span class="tok-str">"ed25519"</span>,
172 <span class="tok-key">"fingerprint"</span>: <span class="tok-str">"sha256:a3f2c9d8e1b47f560c8a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"</span>,
173 <span class="tok-key">"hd_path"</span>: <span class="tok-str">"m/1075233755'/1660078172'/0'/0'/0'/0'"</span>,
174 <span class="tok-key">"mnemonic_word_count"</span>: <span class="tok-num">24</span>,
175 <span class="tok-key">"derived_paths"</span>: {
176 <span class="tok-key">"identity_msign"</span>: <span class="tok-str">"m/1075233755'/1660078172'/0'/0'/0'/0'"</span>,
177 <span class="tok-key">"payments_mpay"</span>: <span class="tok-str">"m/1075233755'/284229149'/0'/0'/0'/0'"</span>,
178 <span class="tok-key">"avax_c_chain"</span>: <span class="tok-str">"m/44'/60'/0'/0/0"</span>,
179 <span class="tok-key">"agent_slot_0"</span>: <span class="tok-str">"m/1075233755'/1660078172'/1'/0'/0'/0'"</span>
180 },
181 <span class="tok-key">"avax_c_chain_address"</span>: <span class="tok-str">"0x3f8a1c2d…"</span>
182 }</code></pre>
183 </div>
184
185 <h3 class="devdocs-subsection-title"><a href="#domain-namespace">Domain namespace</a></h3>
186
187 <p>
188 The <code>domain'</code> level is the atom. Everything else — entities, keys,
189 roles — exists within a domain. The namespace is open by design: new domains
190 plug in without touching the schema.
191 </p>
192
193 <table class="devdocs-table">
194 <thead>
195 <tr><th>Index</th><th>Domain</th><th>Use</th></tr>
196 </thead>
197 <tbody>
198 <tr><td><code>1660078172'</code></td><td>Identity</td><td>MSign auth, MuseHub registration, cross-domain self</td></tr>
199 <tr><td><code>284229149'</code></td><td>Payments</td><td>MPay claims, financial settlement</td></tr>
200 <tr><td><code>678195575'</code></td><td>Code</td><td>Commit provenance, software VCS</td></tr>
201 <tr><td><code>915186137'</code></td><td>Mist</td><td>Content-addressed artifact hosting</td></tr>
202 <tr><td><code>1755707987'</code></td><td>Music</td><td>Stori, audio production signing</td></tr>
203 <tr><td><code>1444628350'</code></td><td>MIDI</td><td>Maestro, symbolic music</td></tr>
204 <tr><td><code>1556829714'</code></td><td>Blockchain</td><td>On-chain operations — ERC8004, EVM, AVAX</td></tr>
205 <tr><td><code>2023564266'</code></td><td>Generic</td><td>Untyped / catch-all (reserved)</td></tr>
206 <tr><td><code>…</code></td><td>(open)</td><td>Future domains extend the table, not the schema</td></tr>
207 </tbody>
208 </table>
209
210 <p>
211 Domain <code>1660078172'</code> (Identity) is the special case — it is the key
212 that crosses all other domains. When Gabriel logs into MuseHub, that is
213 <code>domain=1660078172'</code>. When Gabriel signs a commit, that is
214 <code>domain=678195575'</code>. Same entity, different domain keys, provably isolated.
215 </p>
216 <p>
217 Domain integers are computed as
218 <code>int.from_bytes(sha256(name.encode())[:4], "big") &amp; 0x7FFFFFFF</code> —
219 a stable, collision-resistant value derived from the canonical domain name
220 string. Use <code>muse domain index &lt;name&gt;</code> to compute or look up
221 any domain integer offline.
222 </p>
223
224 <h3 class="devdocs-subsection-title"><a href="#least-privilege">Principle of least privilege — encoded in the key tree</a></h3>
225
226 <p>
227 Because domain is a structural level in the derivation path, agent delegation
228 is cryptographically scoped, not policy-scoped. An agent sub-seed derived at
229 <code>domain=284229149'</code> can only produce keys within the Payments domain. It
230 physically cannot derive an Identity key or a Code key — not because a rule
231 forbids it, but because the math does not connect those branches.
232 </p>
233
234 <p>
235 The three entity types map directly onto the derivation path:
236 </p>
237
238 <table class="devdocs-table">
239 <thead>
240 <tr><th>Index</th><th>Entity type</th><th>Description</th></tr>
241 </thead>
242 <tbody>
243 <tr><td><code>0'</code></td><td>Human</td><td>A person with a root mnemonic. Account 0 is always the primary identity.</td></tr>
244 <tr><td><code>1'</code></td><td>Agent</td><td>Any non-human principal delegated from a human or org — LLM agents, CI runners, daemons. All the same at the key level.</td></tr>
245 <tr><td><code>2'</code></td><td>Org</td><td>A collective identity. Membership and governance live above the key layer; the tree records only that this principal is a collective.</td></tr>
246 </tbody>
247 </table>
248
249 <p>
250 There is no separate service or daemon type — those are agents. Quorum is not
251 an entity type either; it is a threshold policy that references multiple paths
252 at verification time. The key tree stays flat and complete.
253 </p>
254
255 <h3 class="devdocs-subsection-title" id="entity-id"><a href="#entity-id">entity_id — principal slot</a></h3>
256
257 <p>
258 <code>entity_id'</code> is the slot number within an entity class. For humans it is
259 always <code>0'</code> — there is exactly one of you per mnemonic. For agents,
260 each spawned agent gets its own slot: the first agent you spawn derives its sub-seed
261 at <code>entity_id=0'</code>, the second at <code>entity_id=1'</code>, and so on.
262 Two agents at different <code>entity_id</code> values cannot derive each other's
263 keys even if they share the same domain — the math isolates the branches.
264 </p>
265
266 <p>
267 In practice, Agentception computes a stable slot from the agent's handle string
268 (<code>sha256(handle.encode())[:4] &amp; 0x7FFFFFFF</code>) so the same handle
269 always maps to the same HD slot deterministically, without a central registry.
270 </p>
271
272 <h3 class="devdocs-subsection-title" id="role-keys"><a href="#role-keys">Role — key purpose</a></h3>
273
274 <p>
275 <code>role'</code> separates the <em>cryptographic purpose</em> of a key within a
276 domain. The same identity, at the same domain, can hold multiple keys — one for
277 each role:
278 </p>
279
280 <table class="devdocs-table">
281 <thead>
282 <tr><th>Index</th><th>Constant</th><th>Purpose</th></tr>
283 </thead>
284 <tbody>
285 <tr><td><code>0'</code></td><td><code>ROLE_SIGN</code></td><td>Primary signing key — MSign HTTP authentication, commit provenance. The key used in every day-to-day request.</td></tr>
286 <tr><td><code>1'</code></td><td><code>ROLE_RECEIVE</code></td><td>MPay receiving address — the key that payment senders encrypt to. Kept separate so a payment-key leak cannot impersonate you.</td></tr>
287 <tr><td><code>2'</code></td><td><code>ROLE_PROVISION</code></td><td>Provisioning key — used during entity bootstrapping, e.g. initial agent spawn. Rotated out of service once the entity is live.</td></tr>
288 <tr><td><code>3'</code></td><td><code>ROLE_ATTEST</code></td><td>Third-party attestation — signing claims <em>about</em> another identity (code review, master approval, skill verification). Distinct from self-signing so attestation keys can be delegated independently.</td></tr>
289 <tr><td><code>4'</code></td><td><code>ROLE_DELEGATE</code></td><td>Scoped authority delegation — reserved for future use.</td></tr>
290 </tbody>
291 </table>
292
293 <p>
294 The practical benefit of role separation is <strong>key compromise isolation</strong>.
295 If your payment receiving key leaks, your signing key is unaffected — they derive
296 from different paths even though they share the same mnemonic. An attacker who steals
297 your <code>ROLE_RECEIVE</code> key cannot impersonate you in MSign requests, and
298 someone who compromises your <code>ROLE_SIGN</code> key cannot redirect your
299 incoming payments. Rotate one role without touching the others.
300 </p>
301
302 <div class="devdocs-code-block">
303 <div class="devdocs-code-header">
304 <span class="devdocs-code-lang">python</span>
305 <span class="devdocs-code-label">Domain-scoped agent delegation</span>
306 </div>
307 <pre><code><span class="tok-cmt"># Music composition agent — can only derive Music domain keys</span>
308 music_agent_seed = derive_sub_seed(master_seed, domain=<span class="tok-num">1755707987</span>, entity_type=<span class="tok-num">1</span>, agent_id=<span class="tok-num">0</span>)
309
310 <span class="tok-cmt"># Code review agent — can only derive Code domain keys</span>
311 code_agent_seed = derive_sub_seed(master_seed, domain=<span class="tok-num">678195575</span>, entity_type=<span class="tok-num">1</span>, agent_id=<span class="tok-num">0</span>)
312
313 <span class="tok-cmt"># Agentception orchestrator — Identity domain only, nothing else</span>
314 auth_agent_seed = derive_sub_seed(master_seed, domain=<span class="tok-num">1660078172</span>, entity_type=<span class="tok-num">1</span>, agent_id=<span class="tok-num">0</span>)</code></pre>
315 </div>
316
317 <p>
318 The mnemonic lives in <code>~/.muse/identity.toml</code> (in memory only —
319 never written to disk after initial setup). Each derived key is persisted as a
320 PEM file alongside its fingerprint. Agentception injects agent sub-seeds into
321 spawned agents via the <code>MUSE_AGENT_KEY_FD</code> file descriptor — a 64-byte
322 raw sub-seed passed over a pipe — so the agent's process never touches the
323 root mnemonic.
324 </p>
325
326 <div class="devdocs-callout">
327 {{ icon("info", 16, "devdocs-callout-icon") }}
328 <div>
329 <code>ml-dsa-65</code> (CRYSTALS-Dilithium) is reserved for post-quantum
330 migration. The algorithm prefix in every ID and signature string
331 (<code>ed25519:…</code>, <code>mldsa65:…</code>) means the format is
332 self-describing — old and new keys coexist without a flag day.
333 </div>
334 </div>
335 </section>
336
337 {# ── MSign ──────────────────────────────────────────────────────────────── #}
338 <section class="devdocs-section" id="msign">
339 <h2 class="devdocs-section-title"><a href="#msign">MSign authentication</a></h2>
340
341 <p>
342 Every authenticated request to MuseHub carries an
343 <strong>MSign Authorization header</strong>. No session cookies, no bearer
344 tokens. The hub verifies the signature on every request — stateless,
345 replay-protected, and auditable.
346 </p>
347
348 <div class="devdocs-code-block">
349 <div class="devdocs-code-header">
350 <span class="devdocs-code-lang">http</span>
351 <span class="devdocs-code-label">MSign header format</span>
352 </div>
353 <pre><code>Authorization: MSign handle=<span class="tok-str">"gabriel"</span> alg=<span class="tok-str">"ed25519"</span> ts=<span class="tok-num">1744000000</span> sig=<span class="tok-str">"&lt;base64url&gt;"</span></code></pre>
354 </div>
355
356 <h3 class="devdocs-subsection-title"><a href="#canonical-message">What gets signed</a></h3>
357
358 <p>
359 The signature covers a canonical message assembled from the request's
360 algorithm, method, host, path, timestamp, and body hash — bound together
361 with newlines:
362 </p>
363
364 <div class="devdocs-code-block">
365 <div class="devdocs-code-header">
366 <span class="devdocs-code-lang">text</span>
367 <span class="devdocs-code-label">canonical message (newline-separated, UTF-8)</span>
368 </div>
369 <pre><code>{algorithm}
370 {METHOD}
371 {host}
372 {path_with_query}
373 {unix_timestamp}
374 {hex(sha256(request_body))}</code></pre>
375 </div>
376
377 <div class="devdocs-code-block">
378 <div class="devdocs-code-header">
379 <span class="devdocs-code-lang">text</span>
380 <span class="devdocs-code-label">example — pushing to gabriel/muse</span>
381 </div>
382 <pre><code>ed25519
383 POST
384 staging.musehub.ai
385 /gabriel/muse/push
386 1744000000
387 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855</code></pre>
388 </div>
389
390 <table class="devdocs-table">
391 <thead><tr><th>Field</th><th>Value</th><th>Notes</th></tr></thead>
392 <tbody>
393 <tr><td>algorithm</td><td><code>ed25519</code></td><td>Matches <code>alg=</code> in header</td></tr>
394 <tr><td>METHOD</td><td><code>POST</code>, <code>GET</code>, …</td><td>Uppercase</td></tr>
395 <tr><td>host</td><td><code>staging.musehub.ai</code></td><td>Lowercase; strip standard ports (443/80)</td></tr>
396 <tr><td>path_with_query</td><td><code>/gabriel/muse/push</code></td><td>Include <code>?query</code> if present</td></tr>
397 <tr><td>unix_timestamp</td><td><code>1744000000</code></td><td>Seconds since epoch; replay window ±30 s</td></tr>
398 <tr><td>body_hash</td><td>64-char lowercase hex</td><td>SHA-256 of raw request body; empty body → SHA-256 of <code>b""</code></td></tr>
399 </tbody>
400 </table>
401
402 <h3 class="devdocs-subsection-title"><a href="#signing-requests">Signing a request</a></h3>
403
404 <div class="devdocs-code-block">
405 <div class="devdocs-code-header">
406 <span class="devdocs-code-lang">bash</span>
407 <span class="devdocs-code-label">muse sign — produce or execute a signed request</span>
408 </div>
409 <pre><code><span class="tok-cmt"># Produce an Authorization header for a POST</span>
410 muse sign header \
411 --method POST \
412 --path /gabriel/muse/push \
413 --hub {{ site_base_url() }} \
414 --json
415
416 <span class="tok-cmt"># Sign and execute in one command</span>
417 muse sign request \
418 --method POST \
419 --url {{ site_base_url() }}/gabriel/muse/push \
420 --body-file payload.msgpack \
421 --json
422
423 <span class="tok-cmt"># Verify a header (useful in tests)</span>
424 muse sign verify \
425 --header <span class="tok-str">'MSign handle="gabriel" alg="ed25519" ts=1744000000 sig="..."'</span> \
426 --method POST \
427 --url {{ site_base_url() }}/gabriel/muse/push \
428 --public-key-b64 &lt;base64url-pubkey&gt;</code></pre>
429 </div>
430
431 <div class="devdocs-code-block">
432 <div class="devdocs-code-header">
433 <span class="devdocs-code-lang">python</span>
434 <span class="devdocs-code-label">signing programmatically</span>
435 </div>
436 <pre><code><span class="tok-kw">from</span> muse.core.msign <span class="tok-kw">import</span> sign_request, verify_request
437 <span class="tok-kw">import</span> httpx, time
438
439 <span class="tok-cmt"># sign_request returns the Authorization header value</span>
440 auth_header = sign_request(
441 method=<span class="tok-str">"POST"</span>,
442 url=<span class="tok-str">"{{ site_base_url() }}/gabriel/muse/push"</span>,
443 body=payload_bytes,
444 private_key=ed25519_private_key, <span class="tok-cmt"># cryptography.hazmat PrivateKey</span>
445 handle=<span class="tok-str">"gabriel"</span>,
446 )
447
448 resp = httpx.post(
449 <span class="tok-str">"{{ site_base_url() }}/gabriel/muse/push"</span>,
450 content=payload_bytes,
451 headers={<span class="tok-str">"Authorization"</span>: auth_header,
452 <span class="tok-str">"Content-Type"</span>: <span class="tok-str">"application/x-msgpack"</span>},
453 )</code></pre>
454 </div>
455 </section>
456
457 {# ── identity.toml ───────────────────────────────────────────────────────── #}
458 <section class="devdocs-section" id="identity-toml">
459 <h2 class="devdocs-section-title"><a href="#identity-toml">identity.toml</a></h2>
460
461 <p>
462 All identity state for a host lives in <code>~/.muse/identity.toml</code>.
463 Each section is keyed by <code>host[:port]</code> for human entries, or
464 <code>host[:port]#handle</code> for agent entries. One file, one source of
465 truth — no per-repo credential config.
466 </p>
467
468 <div class="devdocs-code-block">
469 <div class="devdocs-code-header">
470 <span class="devdocs-code-lang">toml</span>
471 <span class="devdocs-code-label">~/.muse/identity.toml</span>
472 </div>
473 <pre><code><span class="tok-cmt"># Human entry — one per hub</span>
474 [<span class="tok-str">"localhost:10003"</span>]
475 type = <span class="tok-str">"human"</span>
476 handle = <span class="tok-str">"gabriel"</span>
477 algorithm = <span class="tok-str">"ed25519"</span>
478 fingerprint = <span class="tok-str">"sha256:a3f2c9d8..."</span> <span class="tok-cmt"># sha256:&lt;64-hex&gt; fingerprint of public key</span>
479 hd_path = <span class="tok-str">"m/1075233755'/1660078172'/0'/0'/0'/0'"</span> <span class="tok-cmt"># SLIP-0010 derivation path</span>
480
481 <span class="tok-cmt"># Agent entry — provisioned by gabriel on this hub</span>
482 [<span class="tok-str">"localhost:10003#claude-code"</span>]
483 type = <span class="tok-str">"agent"</span>
484 handle = <span class="tok-str">"claude-code"</span>
485 algorithm = <span class="tok-str">"ed25519"</span>
486 fingerprint = <span class="tok-str">"sha256:b7e1a4c2..."</span>
487 hd_path = <span class="tok-str">"m/1075233755'/1660078172'/1'/0'/0'/0'"</span>
488 provisioned_by = <span class="tok-str">"gabriel"</span>
489 provisioned_by_fingerprint = <span class="tok-str">"sha256:a3f2c9d8..."</span>
490 capabilities = [<span class="tok-str">"push"</span>, <span class="tok-str">"pull"</span>, <span class="tok-str">"commit"</span>]</code></pre>
491 </div>
492
493 <table class="devdocs-table">
494 <thead><tr><th>Field</th><th>Type</th><th>Present on</th><th>Description</th></tr></thead>
495 <tbody>
496 <tr><td>type</td><td>string</td><td>all</td><td><code>"human"</code> or <code>"agent"</code></td></tr>
497 <tr><td>handle</td><td>string</td><td>all</td><td>Hub-assigned username</td></tr>
498
499 <tr><td>algorithm</td><td>string</td><td>all</td><td><code>"ed25519"</code> (reserved: <code>"ml-dsa-65"</code>)</td></tr>
500 <tr><td>fingerprint</td><td>string</td><td>all</td><td><code>sha256:&lt;64-hex&gt;</code> fingerprint of the public key</td></tr>
501 <tr><td>hd_path</td><td>string</td><td>all</td><td>SLIP-0010 derivation path, e.g. <code>"m/0'"</code></td></tr>
502 <tr><td>provisioned_by</td><td>string</td><td>agent</td><td>Handle of the human who spawned this agent</td></tr>
503 <tr><td>provisioned_by_fingerprint</td><td>string</td><td>agent</td><td>Fingerprint of the provisioning key</td></tr>
504 <tr><td>capabilities</td><td>list</td><td>agent</td><td>Allowed operations: <code>"push"</code>, <code>"pull"</code>, <code>"commit"</code>, …</td></tr>
505 <tr><td>mnemonic</td><td>string</td><td>human (memory only)</td><td>BIP-39 phrase; injected at startup, never persisted</td></tr>
506 </tbody>
507 </table>
508 </section>
509
510 {# ── Codec ──────────────────────────────────────────────────────────────── #}
511 <section class="devdocs-section" id="codec">
512 <h2 class="devdocs-section-title"><a href="#codec">Cryptographic value codec</a></h2>
513
514 <p>
515 Every cryptographic value is <strong>always canonically prefixed</strong>.
516 Never a bare hex string or bare base64 blob. The prefix embeds the algorithm,
517 making every value self-describing across storage, wire, and logs.
518 </p>
519
520 <div class="devdocs-code-block">
521 <div class="devdocs-code-header">
522 <span class="devdocs-code-lang">text</span>
523 <span class="devdocs-code-label">wire formats</span>
524 </div>
525 <pre><code><span class="tok-acc">sha256:</span>&lt;64-hex&gt; — content-addressed object / commit / snapshot ID
526 <span class="tok-acc">ed25519:</span>&lt;base64url&gt; — Ed25519 signature or public key (no padding)
527 <span class="tok-acc">mldsa65:</span>&lt;base64url&gt; — post-quantum signature (reserved)</code></pre>
528 </div>
529
530 <div class="devdocs-code-block">
531 <div class="devdocs-code-header">
532 <span class="devdocs-code-lang">python</span>
533 <span class="devdocs-code-label">muse.core.types — codec functions</span>
534 </div>
535 <pre><code><span class="tok-cmt"># Split without decoding — use when you need the string form</span>
536 split_id(<span class="tok-str">"sha256:abc..."</span>) → (<span class="tok-str">"sha256"</span>, <span class="tok-str">"abc..."</span>)
537 split_sig(<span class="tok-str">"ed25519:AAA..."</span>) → (<span class="tok-str">"ed25519"</span>, <span class="tok-str">"AAA..."</span>)
538 split_pubkey(<span class="tok-str">"ed25519:BBB..."</span>) → (<span class="tok-str">"ed25519"</span>, <span class="tok-str">"BBB..."</span>)
539
540 <span class="tok-cmt"># Decode to raw bytes — use for cryptographic operations only</span>
541 decode_sig(<span class="tok-str">"ed25519:AAA..."</span>) → (<span class="tok-str">"ed25519"</span>, b<span class="tok-str">"..."</span>)
542 decode_pubkey(<span class="tok-str">"ed25519:BBB..."</span>) → (<span class="tok-str">"ed25519"</span>, b<span class="tok-str">"..."</span>)
543
544 <span class="tok-cmt"># Encode raw bytes to prefixed string</span>
545 encode_sig(<span class="tok-str">"ed25519"</span>, raw_bytes) → <span class="tok-str">"ed25519:&lt;base64url&gt;"</span>
546 encode_pubkey(<span class="tok-str">"ed25519"</span>, raw_bytes) → <span class="tok-str">"ed25519:&lt;base64url&gt;"</span>
547
548 <span class="tok-cmt"># Just the algo name</span>
549 sig_algo(<span class="tok-str">"ed25519:AAA..."</span>) → <span class="tok-str">"ed25519"</span></code></pre>
550 </div>
551
552 <div class="devdocs-callout devdocs-callout--warn">
553 {{ icon("alert", 16, "devdocs-callout-icon") }}
554 <div>
555 Never strip prefixes manually — no <code>.removeprefix("ed25519:")</code>,
556 no <code>[10:]</code>, no inline <code>.partition(":")</code>. Always use
557 <code>split_sig</code> / <code>split_pubkey</code> / <code>split_id</code>.
558 Never store or transmit bare bytes — always encode with
559 <code>encode_sig</code> / <code>encode_pubkey</code> before serialisation.
560 </div>
561 </div>
562 </section>
563
564 {# ── Trust chains ──────────────────────────────────────────────────────── #}
565 <section class="devdocs-section" id="trust-chain">
566 <h2 class="devdocs-section-title"><a href="#trust-chain">Trust chains</a></h2>
567
568 <p>
569 The identity graph is a DAG with three node types and two edge types.
570 Humans are the only root nodes — they derive authority from their key pair.
571 Agents are provisioned by (spawned from) humans or other agents.
572 Organisations exist as collective entities whose authority is the quorum
573 of their members.
574 </p>
575
576 <div class="devdocs-code-block">
577 <div class="devdocs-code-header">
578 <span class="devdocs-code-lang">text</span>
579 <span class="devdocs-code-label">identity DAG — node and edge types</span>
580 </div>
581 <pre><code><span class="tok-cmt">Nodes:</span> HUMAN AGENT ORG
582
583 <span class="tok-cmt">Edges:</span> spawns(from, to) — from provisions to (human→agent, agent→agent)
584 member_of(member, org) — member joins org (human, agent, or org → org)
585
586 <span class="tok-cmt">Example trust chain:</span>
587 gabriel (HUMAN)
588 ├─[spawns]──▶ claude-code (AGENT)
589 │ └─[spawns]──▶ worker-7 (AGENT)
590 ├─[member_of]──▶ musehub-org (ORG, quorum=1)
591 └─[member_of]──▶ graph-lab (ORG, quorum=2)
592 ├─◀── gabriel
593 ├─◀── claude-code
594 └─◀── musehub-org</code></pre>
595 </div>
596
597 <p>
598 The graph is append-only and enforced at push time by
599 <code>IdentityPushValidator</code> on the hub side. Three invariants must hold
600 on every push — in order, hard errors before warnings:
601 </p>
602
603 <table class="devdocs-table">
604 <thead><tr><th>Invariant</th><th>Name</th><th>Enforcement</th><th>Effect on push</th></tr></thead>
605 <tbody>
606 <tr>
607 <td>I1</td><td>Acyclicity</td>
608 <td><span class="devdocs-badge devdocs-badge--hard">hard error</span></td>
609 <td>Push rejected; offending edge listed in errors</td>
610 </tr>
611 <tr>
612 <td>I2</td><td>Root distance</td>
613 <td><span class="devdocs-badge devdocs-badge--warn">warning</span></td>
614 <td>Push accepted; orphaned node annotated</td>
615 </tr>
616 <tr>
617 <td>I3</td><td>Authorization</td>
618 <td><span class="devdocs-badge devdocs-badge--hard">hard error</span></td>
619 <td>Push rejected; missing signature listed</td>
620 </tr>
621 </tbody>
622 </table>
623 </section>
624
625 {# ── Attestations ─────────────────────────────────────────────────────────── #}
626 <section class="devdocs-section" id="attestations">
627 <h2 class="devdocs-section-title"><a href="#attestations">Attestations</a></h2>
628
629 <p>
630 An attestation is an <strong>Ed25519-signed claim</strong> one identity makes about
631 another — the same key material, the same primitives, and the same canonical-prefix
632 discipline as the rest of this page. The claim is content-addressed by the SHA-256 of
633 its canonical message, verified entirely against the attester's registered public key,
634 and preserved forever in the audit trail. Attestations are how the identity DAG above
635 acquires <em>semantic edges</em> — beyond <code>spawns</code> and
636 <code>member_of</code>, an attestation can record that one identity reviewed another's
637 code, approved a master, verified a skill, or simply trusts them.
638 </p>
639
640 <h3 class="devdocs-subsection-title"><a href="#attest-canonical">Canonical message</a></h3>
641
642 <p>
643 The <code>ATTEST</code> domain separator is the entire reason cross-protocol replay
644 is impossible. An MSign auth header signed under <code>MUSE-SIGN-V1</code> can never
645 be replayed as an attestation, and an MPay claim signed under <code>MPAY</code> can
646 never be replayed as a code review. The canonical message is UTF-8, newline-separated,
647 with no trailing newline:
648 </p>
649
650 <div class="devdocs-code-block">
651 <div class="devdocs-code-header">
652 <span class="devdocs-code-lang">text</span>
653 <span class="devdocs-code-label">identity scope — 5 lines</span>
654 </div>
655 <pre><code>ATTEST\n{attester}\n{subject}\n{claim}\n{issued_at_iso}</code></pre>
656 </div>
657
658 <div class="devdocs-code-block">
659 <div class="devdocs-code-header">
660 <span class="devdocs-code-lang">text</span>
661 <span class="devdocs-code-label">repo / commit scope — 6 lines (scope_ref appended)</span>
662 </div>
663 <pre><code>ATTEST\n{attester}\n{subject}\n{claim}\n{issued_at_iso}\n{scope_ref}</code></pre>
664 </div>
665
666 <p>
667 <code>{claim}</code> is a compact JSON object whose top-level <code>"type"</code>
668 key must match a registered claim type. Compactness is enforced by serialising with
669 <code>json.dumps(claim, separators=(",", ":"), sort_keys=True)</code> so the same
670 logical claim always produces the same bytes.
671 </p>
672
673 <div class="devdocs-code-block">
674 <div class="devdocs-code-header">
675 <span class="devdocs-code-lang">python</span>
676 <span class="devdocs-code-label">building and signing the canonical message</span>
677 </div>
678 <pre><code><span class="tok-cmt"># Identity scope — 5 lines</span>
679 parts = ["ATTEST", attester, subject, claim_json, issued_at_iso]
680
681 <span class="tok-cmt"># Repo / commit scope — 6 lines</span>
682 if scope != "identity":
683 parts.append(scope_ref)
684
685 msg = "\n".join(parts).encode("utf-8")
686 signature = privkey.sign(msg) <span class="tok-cmt"># Ed25519, RFC 8032</span>
687 sig_str = encode_sig("ed25519", signature) <span class="tok-cmt"># → "ed25519:&lt;base64url&gt;"</span></code></pre>
688 </div>
689
690 <p>
691 The content-addressed identifier is independent of the signature — two attestations
692 with the same canonical message collapse to the same row, which is why
693 <code>create</code> is idempotent:
694 </p>
695
696 <div class="devdocs-code-block">
697 <div class="devdocs-code-header">
698 <span class="devdocs-code-lang">text</span>
699 <span class="devdocs-code-label">attestation_id derivation</span>
700 </div>
701 <pre><code>attestation_id = sha256(attester NUL subject NUL claim NUL issued_at_iso)</code></pre>
702 </div>
703
704 <h3 class="devdocs-subsection-title"><a href="#attest-claim-types">The 17 claim types</a></h3>
705
706 <p>
707 Every attestation declares exactly one claim type. The registry is seeded in
708 <code>musehub.services.musehub_attestations</code> and mirrored to the
709 <code>musehub_attestation_claim_types</code> table — the DB is authoritative at
710 runtime and accepts new types via <code>add_claim_type</code>, but every node ships
711 with the same 17 seed entries below. Each type pre-declares which scopes it makes
712 sense in; mismatches are rejected before any signature is verified.
713 </p>
714
715 <div class="devdocs-table-wrap">
716 <table class="devdocs-table">
717 <thead><tr><th>Category</th><th>Type key</th><th>Label</th><th>Valid scopes</th><th>Meaning</th></tr></thead>
718 <tbody>
719 <tr><td rowspan="3">identity</td><td><code>human</code></td> <td>Human</td> <td><code>identity</code></td> <td>Subject is a verified human.</td></tr>
720 <tr> <td><code>org</code></td> <td>Organisation</td> <td><code>identity</code></td> <td>Subject is a legitimate organisation.</td></tr>
721 <tr> <td><code>agent</code></td> <td>Agent</td> <td><code>identity</code></td> <td>Subject is a trustworthy agent.</td></tr>
722 <tr><td rowspan="3">trust</td> <td><code>spawned-by</code></td> <td>Spawned By</td> <td><code>identity</code></td> <td>Subject agent was provisioned by attester.</td></tr>
723 <tr> <td><code>delegate</code></td> <td>Delegate</td> <td><code>identity</code></td> <td>Attester delegated authority to subject.</td></tr>
724 <tr> <td><code>trusted</code></td> <td>Trusted</td> <td><code>identity</code></td> <td>Attester generally trusts subject.</td></tr>
725 <tr><td rowspan="3">collab</td> <td><code>collab</code></td> <td>Collaborator</td> <td><code>identity</code>, <code>repo</code>, <code>commit</code></td> <td>Attester and subject collaborated.</td></tr>
726 <tr> <td><code>co-author</code></td> <td>Co-author</td> <td><code>identity</code>, <code>repo</code>, <code>commit</code></td> <td>Subject co-authored something with attester.</td></tr>
727 <tr> <td><code>contractor</code></td> <td>Contractor</td> <td><code>identity</code></td> <td>Subject performed contracted work for attester.</td></tr>
728 <tr><td rowspan="3">code</td> <td><code>code:reviewed</code></td> <td>Code Reviewed</td> <td><code>commit</code>, <code>repo</code></td> <td>Attester reviewed subject's code.</td></tr>
729 <tr> <td><code>code:approved</code></td> <td>Code Approved</td> <td><code>commit</code>, <code>repo</code></td> <td>Attester approved a specific code delivery.</td></tr>
730 <tr> <td><code>deploy:approved</code></td> <td>Deploy Approved</td> <td><code>commit</code></td> <td>Attester approved a deployment at this commit.</td></tr>
731 <tr><td rowspan="4">music</td> <td><code>stems:verified</code></td> <td>Stems Verified</td> <td><code>identity</code>, <code>commit</code></td> <td>Attester verified the authenticity of subject's stems.</td></tr>
732 <tr> <td><code>mix:approved</code></td> <td>Mix Approved</td> <td><code>identity</code>, <code>commit</code></td> <td>Attester approved a mix by subject.</td></tr>
733 <tr> <td><code>midi:generated</code></td> <td>MIDI Generated</td> <td><code>identity</code>, <code>commit</code></td> <td>Attester confirms subject generated the MIDI.</td></tr>
734 <tr> <td><code>master:approved</code></td> <td>Master Approved</td> <td><code>identity</code>, <code>commit</code></td> <td>Attester approved a master by subject.</td></tr>
735 <tr><td>skill</td> <td><code>skill:verified</code></td> <td>Skill Verified</td> <td><code>identity</code></td> <td>Attester verified a declared skill of subject.</td></tr>
736 </tbody>
737 </table>
738 </div>
739
740 <div class="devdocs-callout">
741 {{ icon("info", 16, "devdocs-callout-icon") }}
742 <div>
743 Use <code>--metadata '{"key":"value"}'</code> on <code>attestation create</code>
744 to attach extra fields inside the claim JSON — e.g.
745 <code>{"type":"skill:verified","skill":"counterpoint"}</code>. Anything you put in
746 the metadata becomes part of the canonical message and is therefore covered by the
747 signature; rewrite it after the fact and the signature breaks.
748 </div>
749 </div>
750
751 <h3 class="devdocs-subsection-title"><a href="#attest-scopes">The three scopes</a></h3>
752
753 <p>
754 Scope answers <em>"what is this attestation about?"</em> The scope is part of the
755 canonical message via <code>scope_ref</code>, which means an attestation that says
756 "I reviewed gabriel/musehub at commit <code>sha256:abc…</code>" cannot be replayed as
757 "I reviewed gabriel/musehub at commit <code>sha256:def…</code>" — the bytes don't
758 match and the signature fails.
759 </p>
760
761 <div class="devdocs-table-wrap">
762 <table class="devdocs-table">
763 <thead><tr><th>Scope</th><th><code>scope_ref</code> format</th><th>Required fields</th><th>Use for</th></tr></thead>
764 <tbody>
765 <tr>
766 <td><code>identity</code></td>
767 <td>— (omitted)</td>
768 <td>—</td>
769 <td>Claims about a handle as a whole — humanity, org status, trust, skill verification.</td>
770 </tr>
771 <tr>
772 <td><code>repo</code></td>
773 <td><code>{handle}/{repo_slug}</code></td>
774 <td><code>scope_ref</code></td>
775 <td>Claims about a specific repository — collaboration, ongoing review.</td>
776 </tr>
777 <tr>
778 <td><code>commit</code></td>
779 <td><code>{handle}/{repo_slug}@sha256:{commit_id}</code></td>
780 <td><code>scope_ref</code>, <code>commit_id</code></td>
781 <td>Claims about a specific delivery — code review, deploy approval, master sign-off.</td>
782 </tr>
783 </tbody>
784 </table>
785 </div>
786
787 <p>
788 The validator runs in this order, and stops at the first failure: <strong>(1)</strong>
789 claim type exists and is not deprecated; <strong>(2)</strong> scope is in the claim
790 type's <code>valid_scopes</code>; <strong>(3)</strong> required scope fields are
791 present; <strong>(4)</strong> Ed25519 signature verifies against the attester's
792 registered public key. Cheap checks first; cryptography last.
793 </p>
794
795 <h3 class="devdocs-subsection-title"><a href="#attest-cli">CLI commands</a></h3>
796
797 <table class="devdocs-table">
798 <thead><tr><th>Task</th><th>Command</th></tr></thead>
799 <tbody>
800 <tr><td>Issue an identity-scope attestation</td><td><code>muse hub attestation create --subject &lt;handle&gt; --type &lt;claim_type&gt;</code></td></tr>
801 <tr><td>Issue a repo-scope attestation</td><td><code>muse hub attestation create --subject &lt;handle/repo&gt; --type &lt;claim_type&gt; --scope repo --scope-ref &lt;handle/repo&gt;</code></td></tr>
802 <tr><td>Issue a commit-scope attestation</td><td><code>muse hub attestation create --subject &lt;handle/repo&gt; --type &lt;claim_type&gt; --scope commit --scope-ref &lt;handle/repo@sha256:…&gt; --commit-id &lt;sha256:…&gt;</code></td></tr>
803 <tr><td>Attach extra claim fields</td><td><code>muse hub attestation create … --metadata '{"skill":"counterpoint"}'</code></td></tr>
804 <tr><td>Issue with an expiry</td><td><code>muse hub attestation create … --expires-in 90d</code></td></tr>
805 <tr><td>List attestations about a subject</td><td><code>muse hub attestation list --subject &lt;handle&gt; --json</code></td></tr>
806 <tr><td>Filter list by attester</td><td><code>muse hub attestation list --subject &lt;handle&gt; --attester &lt;handle&gt; --json</code></td></tr>
807 <tr><td>Filter list by claim type</td><td><code>muse hub attestation list --subject &lt;handle&gt; --type &lt;claim_type&gt; --json</code></td></tr>
808 <tr><td>Include revoked entries</td><td><code>muse hub attestation list --subject &lt;handle&gt; --include-revoked --json</code></td></tr>
809 <tr><td>Revoke (only the original attester)</td><td><code>muse hub attestation revoke &lt;attestation_id&gt; --subject &lt;handle&gt;</code></td></tr>
810 </tbody>
811 </table>
812
813 <div class="devdocs-code-block">
814 <div class="devdocs-code-header">
815 <span class="devdocs-code-lang">bash</span>
816 <span class="devdocs-code-label">end-to-end example</span>
817 </div>
818 <pre><code><span class="tok-cmt"># gabriel attests that claude-code is a trustworthy agent</span>
819 muse hub attestation create --subject claude-code --type agent --json
820
821 <span class="tok-cmt"># gabriel reviews a specific commit on his own repo</span>
822 muse hub attestation create \
823 --subject gabriel/musehub --type code:reviewed \
824 --scope commit \
825 --scope-ref gabriel/musehub@sha256:abc123... \
826 --commit-id sha256:abc123... \
827 --json
828
829 <span class="tok-cmt"># list everything attested about gabriel</span>
830 muse hub attestation list --subject gabriel --json
831
832 <span class="tok-cmt"># revoke an old attestation (only the original attester can do this)</span>
833 muse hub attestation revoke sha256:af54753d... --subject claude-code --json</code></pre>
834 </div>
835
836 <p>
837 The CLI loads the private key from <code>~/.muse/identity.toml</code> (or the agent
838 sub-key injected via <code>MUSE_AGENT_KEY_FD</code>), builds the canonical message,
839 signs it locally, and POSTs the signature plus public key to
840 <code>/api/profiles/{handle}/attestations</code>. The hub never sees the private key
841 — it only verifies the signature against the public key it already has on file from
842 the attester's registration.
843 </p>
844
845 <h3 class="devdocs-subsection-title"><a href="#attest-rest-api">REST API</a></h3>
846
847 <div class="devdocs-table-wrap">
848 <table class="devdocs-table">
849 <thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
850 <tbody>
851 <tr>
852 <td><code>GET</code></td>
853 <td><code>/api/profiles/{handle}/attestations</code></td>
854 <td>List attestations about <code>{handle}</code> (add <code>?include_revoked=true</code> to include retired entries).</td>
855 </tr>
856 <tr>
857 <td><code>POST</code></td>
858 <td><code>/api/profiles/{handle}/attestations</code></td>
859 <td>Issue a new attestation. Body is <code>AttestationRequest</code>. Idempotent on <code>(attester, subject, claim, issued_at)</code>.</td>
860 </tr>
861 <tr>
862 <td><code>DELETE</code></td>
863 <td><code>/api/profiles/{handle}/attestations/{id}?revoker={handle}</code></td>
864 <td>Soft-revoke. Sets <code>revoked_at</code>; row is preserved for audit. Only the original attester succeeds.</td>
865 </tr>
866 <tr>
867 <td><code>GET</code></td>
868 <td><code>/api/profiles/attestation-types</code></td>
869 <td>List all registered claim types (add <code>?include_deprecated=true</code> for retired entries).</td>
870 </tr>
871 </tbody>
872 </table>
873 </div>
874
875 <div class="devdocs-callout devdocs-callout--warn">
876 {{ icon("alert", 16, "devdocs-callout-icon") }}
877 <div>
878 Revocation is <strong>soft</strong> — the row is kept with a
879 <code>revoked_at</code> timestamp set, never deleted. This is by design:
880 downstream consumers may have already cached or relied on the attestation, and the
881 audit trail is what makes the system tamper-evident. Default queries exclude
882 revoked rows; pass <code>--include-revoked</code> or
883 <code>?include_revoked=true</code> to see them.
884 </div>
885 </div>
886 </section>
887
888 {# ── Invariants ──────────────────────────────────────────────────────────── #}
889 <section class="devdocs-section" id="invariants">
890 <h2 class="devdocs-section-title"><a href="#invariants">The three invariants</a></h2>
891
892 <h3 class="devdocs-subsection-title"><a href="#i1">I1 — Acyclicity</a></h3>
893 <p>
894 The identity graph must be a DAG at all times. Before accepting any new
895 edge <code>from → to</code>, the validator runs a DFS from <code>to</code>
896 and rejects the push if it reaches <code>from</code>. Self-loops
897 (<code>from == to</code>) are rejected immediately without traversal.
898 Both <code>spawns</code> and <code>member_of</code> edges share the same DAG
899 universe — a cross-type cycle (e.g. <code>alice →spawns→ bot →member_of→ alice</code>)
900 is equally rejected.
901 </p>
902
903 <h3 class="devdocs-subsection-title"><a href="#i2">I2 — Root distance</a></h3>
904 <p>
905 Every node should have a path to at least one human root. The validator
906 runs a multi-source BFS from all <code>HUMAN</code> nodes and reports any
907 node with no path as an orphan. An orphaned agent or org is not
908 necessarily a bug — the push is accepted — but the node is flagged in
909 <code>ValidationResult.warnings</code> for the caller to inspect.
910 </p>
911
912 <h3 class="devdocs-subsection-title"><a href="#i3">I3 — Authorization</a></h3>
913 <p>
914 Every relationship must carry cryptographic signatures proving consent.
915 The rules differ by edge type:
916 </p>
917
918 <table class="devdocs-table">
919 <thead><tr><th>Edge type</th><th>Required signers</th><th>Notes</th></tr></thead>
920 <tbody>
921 <tr>
922 <td><code>spawns(A → B)</code></td>
923 <td><code>A</code> must sign</td>
924 <td>Only the spawner can authorise the spawn. Extra signers are permitted.</td>
925 </tr>
926 <tr>
927 <td><code>member_of(M → Org)</code> — founding</td>
928 <td><code>M</code> must self-sign</td>
929 <td>Org has zero prior members. Bootstrap pattern.</td>
930 </tr>
931 <tr>
932 <td><code>member_of(M → Org)</code> — subsequent</td>
933 <td><code>min(quorum, |prior|)</code> existing members</td>
934 <td>Processed in input order. Each relationship sees only members committed before it.</td>
935 </tr>
936 </tbody>
937 </table>
938
939 <div class="devdocs-code-block">
940 <div class="devdocs-code-header">
941 <span class="devdocs-code-lang">python</span>
942 <span class="devdocs-code-label">IdentityPushValidator</span>
943 </div>
944 <pre><code><span class="tok-kw">from</span> musehub.graph.push_validator <span class="tok-kw">import</span> IdentityPushValidator
945
946 validator = IdentityPushValidator()
947 result = validator.validate(
948 identities=[
949 {<span class="tok-str">"handle"</span>: <span class="tok-str">"gabriel"</span>, <span class="tok-str">"type"</span>: <span class="tok-str">"human"</span>, <span class="tok-str">"quorum"</span>: None},
950 {<span class="tok-str">"handle"</span>: <span class="tok-str">"claude-code"</span>, <span class="tok-str">"type"</span>: <span class="tok-str">"agent"</span>, <span class="tok-str">"quorum"</span>: None},
951 {<span class="tok-str">"handle"</span>: <span class="tok-str">"acme"</span>, <span class="tok-str">"type"</span>: <span class="tok-str">"org"</span>, <span class="tok-str">"quorum"</span>: <span class="tok-num">2</span>},
952 ],
953 relationships=[
954 {
955 <span class="tok-str">"from_handle"</span>: <span class="tok-str">"gabriel"</span>,
956 <span class="tok-str">"to_handle"</span>: <span class="tok-str">"claude-code"</span>,
957 <span class="tok-str">"edge_type"</span>: <span class="tok-str">"spawns"</span>,
958 <span class="tok-str">"authorized_by"</span>: [
959 {<span class="tok-str">"signer"</span>: <span class="tok-str">"gabriel"</span>, <span class="tok-str">"signature"</span>: <span class="tok-str">"ed25519:..."</span>, <span class="tok-str">"signed_at"</span>: <span class="tok-str">"2026-04-21T..."</span>}
960 ],
961 },
962 ],
963 )
964 <span class="tok-cmt"># result.valid → True / False</span>
965 <span class="tok-cmt"># result.errors → ["I1 violation: ...", "I3 violation: ..."]</span>
966 <span class="tok-cmt"># result.warnings → ["I2 warning: 'acme' has no path to any human root"]</span></code></pre>
967 </div>
968 </section>
969
970 {# ── Quorum ──────────────────────────────────────────────────────────────── #}
971 <section class="devdocs-section" id="quorum">
972 <h2 class="devdocs-section-title"><a href="#quorum">Quorum authorization</a></h2>
973
974 <p>
975 Organisations have no key of their own. Their authority <em>is</em> the
976 quorum of their members — distributed threshold authorisation rather than
977 a single key that can be stolen or lost. The <code>quorum</code> field on an
978 org record sets the threshold; members carry fractional
979 <code>weight</code> values so voting power can be non-uniform.
980 </p>
981
982 <p>
983 Relationships are processed in input order — each one sees only the members
984 committed before it in the same push. This mirrors an append-only event log
985 and solves the bootstrap problem cleanly: the first member of an empty org
986 self-authorises; subsequent members need <code>min(quorum, |prior|)</code>
987 existing member signatures.
988 </p>
989
990 <div class="devdocs-code-block">
991 <div class="devdocs-code-header">
992 <span class="devdocs-code-lang">text</span>
993 <span class="devdocs-code-label">quorum scenarios — graph-lab (quorum=2)</span>
994 </div>
995 <pre><code><span class="tok-cmt">Step 1: gabriel joins graph-lab (0 prior members)</span>
996 → bootstrap: gabriel self-signs ✓ required: 1 sig (self)
997
998 <span class="tok-cmt">Step 2: claude-code joins graph-lab (1 prior member: gabriel)</span>
999 → min(quorum=2, prior=1) = 1 ✓ required: 1 sig from {gabriel}
1000
1001 <span class="tok-cmt">Step 3: musehub-org joins graph-lab (2 prior members: gabriel, claude-code)</span>
1002 → min(quorum=2, prior=2) = 2 ✓ required: 2 sigs from {gabriel, claude-code}
1003
1004 <span class="tok-cmt">Step 4: alice tries to join (3 prior members: gabriel, claude-code, musehub-org)</span>
1005 → min(quorum=2, prior=3) = 2 ✓ required: any 2 of the 3 existing members</code></pre>
1006 </div>
1007
1008 <p>
1009 Sub-organisations can vote in their own member_of relationships, but only
1010 if their own quorum is independently met. The
1011 <code>QuorumEngine.effective_weight()</code> method descends recursively to
1012 validate that an org's vote is backed by sufficient member signatures before
1013 counting it toward the parent org's threshold.
1014 </p>
1015
1016 <div class="devdocs-code-block">
1017 <div class="devdocs-code-header">
1018 <span class="devdocs-code-lang">python</span>
1019 <span class="devdocs-code-label">musehub.graph.quorum</span>
1020 </div>
1021 <pre><code><span class="tok-kw">from</span> musehub.graph.quorum <span class="tok-kw">import</span> OrgSpec, QuorumEngine, VoteRecord
1022 <span class="tok-kw">from</span> decimal <span class="tok-kw">import</span> Decimal
1023
1024 orgs = {
1025 <span class="tok-str">"graph-lab"</span>: OrgSpec(
1026 handle=<span class="tok-str">"graph-lab"</span>,
1027 quorum=<span class="tok-num">2</span>,
1028 member_weights={
1029 <span class="tok-str">"gabriel"</span>: Decimal(<span class="tok-str">"1"</span>),
1030 <span class="tok-str">"claude-code"</span>: Decimal(<span class="tok-str">"1"</span>),
1031 },
1032 ),
1033 }
1034 engine = QuorumEngine(orgs)
1035
1036 votes = [
1037 VoteRecord(voter_handle=<span class="tok-str">"gabriel"</span>, org_handle=<span class="tok-str">"graph-lab"</span>),
1038 VoteRecord(voter_handle=<span class="tok-str">"claude-code"</span>, org_handle=<span class="tok-str">"graph-lab"</span>),
1039 ]
1040 engine.is_quorum_met(<span class="tok-str">"graph-lab"</span>, votes) <span class="tok-cmt"># → True</span></code></pre>
1041 </div>
1042
1043 <h3 class="devdocs-subsection-title"><a href="#quorum-multisig">Worked 2-of-3 example — admitting a new member</a></h3>
1044
1045 <p>
1046 The hub verifies quorum at push time by inspecting the <code>authorized_by</code>
1047 array on each relationship. Here is the full push payload for admitting
1048 <code>alice</code> to <code>graph-lab</code> (quorum=2, three prior members):
1049 </p>
1050
1051 <div class="devdocs-code-block">
1052 <div class="devdocs-code-header">
1053 <span class="devdocs-code-lang">json</span>
1054 <span class="devdocs-code-label">identity push payload — 2-of-3 multi-sig membership</span>
1055 </div>
1056 <pre><code>{
1057 <span class="tok-key">"identities"</span>: [
1058 { <span class="tok-key">"handle"</span>: <span class="tok-str">"alice"</span>, <span class="tok-key">"type"</span>: <span class="tok-str">"human"</span>, <span class="tok-key">"quorum"</span>: <span class="tok-kw">null</span>,
1059 <span class="tok-key">"public_key"</span>: <span class="tok-str">"ed25519:aL1mN2oP3qR4sT5uV6wX7yZ8aB9cD0eF1gH2iJ3kL4"</span> }
1060 ],
1061 <span class="tok-key">"relationships"</span>: [
1062 {
1063 <span class="tok-key">"from_handle"</span>: <span class="tok-str">"alice"</span>,
1064 <span class="tok-key">"to_handle"</span>: <span class="tok-str">"graph-lab"</span>,
1065 <span class="tok-key">"edge_type"</span>: <span class="tok-str">"member_of"</span>,
1066 <span class="tok-key">"authorized_by"</span>: [
1067 {
1068 <span class="tok-key">"signer"</span>: <span class="tok-str">"gabriel"</span>,
1069 <span class="tok-key">"signature"</span>: <span class="tok-str">"ed25519:Xe3kA7mBnC2dE5fG8hI1jK4lM6nO9pQ0rS3tU6vW9"</span>,
1070 <span class="tok-key">"signed_at"</span>: <span class="tok-str">"2026-04-21T16:00:00Z"</span>
1071 },
1072 {
1073 <span class="tok-key">"signer"</span>: <span class="tok-str">"claude-code"</span>,
1074 <span class="tok-key">"signature"</span>: <span class="tok-str">"ed25519:Yf4lB8nCdD3eF6gH9iJ2kL5mN7oP1qR4sT7uV0wX3"</span>,
1075 <span class="tok-key">"signed_at"</span>: <span class="tok-str">"2026-04-21T16:01:00Z"</span>
1076 }
1077 ]
1078 }
1079 ]
1080 }</code></pre>
1081 </div>
1082 <div class="devdocs-code-block devdocs-code-block--output">
1083 <div class="devdocs-code-header">
1084 <span class="devdocs-code-lang">json</span>
1085 <span class="devdocs-code-label">hub response — valid push</span>
1086 </div>
1087 <pre><code>{
1088 <span class="tok-key">"valid"</span>: <span class="tok-kw">true</span>,
1089 <span class="tok-key">"errors"</span>: [],
1090 <span class="tok-key">"warnings"</span>: []
1091 }</code></pre>
1092 </div>
1093
1094 <p>
1095 If only one signature is provided — below the quorum threshold — the hub
1096 rejects the push immediately:
1097 </p>
1098
1099 <div class="devdocs-code-block devdocs-code-block--output">
1100 <div class="devdocs-code-header">
1101 <span class="devdocs-code-lang">json</span>
1102 <span class="devdocs-code-label">hub response — quorum not met</span>
1103 </div>
1104 <pre><code>{
1105 <span class="tok-key">"valid"</span>: <span class="tok-kw">false</span>,
1106 <span class="tok-key">"errors"</span>: [
1107 <span class="tok-str">"I3 violation: member_of(alice → graph-lab) requires 2 signatures from existing members, got 1"</span>
1108 ],
1109 <span class="tok-key">"warnings"</span>: []
1110 }</code></pre>
1111 </div>
1112
1113 <h3 class="devdocs-subsection-title"><a href="#quorum-governance">Quorum in governance.json — handle-based members</a></h3>
1114
1115 <p>
1116 Repo governance uses the same quorum concept. A repo is governed when its HEAD
1117 snapshot contains a <code>governance.json</code> file. The hub resolves member
1118 handles to fingerprints at merge time by reading each member's identity repo HEAD
1119 — so key rotation propagates automatically without updating <code>governance.json</code>.
1120 </p>
1121
1122 <div class="devdocs-code-block">
1123 <div class="devdocs-code-header">
1124 <span class="devdocs-code-lang">json</span>
1125 <span class="devdocs-code-label">governance.json — handle-based members (recommended)</span>
1126 </div>
1127 <pre><code>{
1128 <span class="tok-key">"quorum"</span>: {
1129 <span class="tok-key">"threshold"</span>: <span class="tok-num">2</span>,
1130 <span class="tok-key">"members"</span>: [<span class="tok-str">"gabriel"</span>, <span class="tok-str">"alice"</span>]
1131 }
1132 }</code></pre>
1133 </div>
1134
1135 <p>
1136 Raw <code>sha256:</code> fingerprints are also accepted for backward compatibility,
1137 but handles are preferred — they track key rotation without any update to the file.
1138 </p>
1139
1140 <div class="devdocs-code-block">
1141 <div class="devdocs-code-header">
1142 <span class="devdocs-code-lang">json</span>
1143 <span class="devdocs-code-label">governance.json — raw fingerprints (legacy / backward compat)</span>
1144 </div>
1145 <pre><code>{
1146 <span class="tok-key">"quorum"</span>: {
1147 <span class="tok-key">"threshold"</span>: <span class="tok-num">2</span>,
1148 <span class="tok-key">"members"</span>: [<span class="tok-str">"sha256:a3f2c9d8..."</span>, <span class="tok-str">"sha256:b7e1a4c2..."</span>]
1149 }
1150 }</code></pre>
1151 </div>
1152
1153 <p>
1154 Handle resolution reads <code>identities/{handle}.json</code> from the member's
1155 identity repo HEAD, decodes the <code>pubkey</code> field, and derives the
1156 <code>sha256:</code> fingerprint. If the identity repo is absent or the handle
1157 cannot be resolved, that member is skipped and cannot contribute to the quorum.
1158 </p>
1159 </section>
1160
1161 {# ── Auth CLI ────────────────────────────────────────────────────────────── #}
1162 <section class="devdocs-section" id="cli">
1163 <h2 class="devdocs-section-title"><a href="#cli">Auth CLI</a></h2>
1164
1165 <table class="devdocs-table">
1166 <thead><tr><th>Task</th><th>Command</th></tr></thead>
1167 <tbody>
1168 <tr><td>Generate new keypair</td><td><code>muse auth keygen --hub &lt;hub-url&gt;</code></td></tr>
1169 <tr><td>Register key on hub</td><td><code>muse auth register --hub &lt;url&gt; --handle &lt;handle&gt;</code></td></tr>
1170 <tr><td>Show current identity</td><td><code>muse auth whoami --json</code></td></tr>
1171 <tr><td>Full HD identity details</td><td><code>muse auth show --json</code></td></tr>
1172 <tr><td>Rotate to next HD index</td><td><code>muse auth rotate --hub &lt;url&gt; --json</code></td></tr>
1173 <tr><td>Recover key from mnemonic</td><td><code>muse auth recover --hub &lt;url&gt; --json</code></td></tr>
1174 <tr><td>Decommission (logout)</td><td><code>muse auth logout --hub &lt;url&gt; --json</code></td></tr>
1175 <tr><td>Decode an HD path to human-readable labels</td><td><code>muse path annotate &lt;hd-path&gt; --json</code></td></tr>
1176 <tr><td>Migrate pre-Phase-1 keys to hash-derived paths</td><td><code>muse migrate domain-integers --json</code></td></tr>
1177 <tr><td>Dry-run migration (no writes)</td><td><code>muse migrate domain-integers --dry-run --json</code></td></tr>
1178 </tbody>
1179 </table>
1180
1181 <div class="devdocs-code-block">
1182 <div class="devdocs-code-header">
1183 <span class="devdocs-code-lang">bash</span>
1184 <span class="devdocs-code-label">first-time setup</span>
1185 </div>
1186 <pre><code><span class="tok-cmt"># 1. Generate keypair — outputs a BIP-39 mnemonic; store it securely</span>
1187 muse auth keygen --hub {{ site_base_url() }}
1188
1189 <span class="tok-cmt"># 2. Register the public key with the hub</span>
1190 muse auth register --hub {{ site_base_url() }} --handle gabriel
1191
1192 <span class="tok-cmt"># 3. Verify</span>
1193 muse auth whoami
1194 <span class="tok-cmt"># → handle: gabriel hub: &lt;your-hub-host&gt; algo: ed25519</span></code></pre>
1195 </div>
1196
1197 <div class="devdocs-callout">
1198 {{ icon("info", 16, "devdocs-callout-icon") }}
1199 <div>
1200 Agentception spawns agents by passing a 64-byte sub-seed via
1201 <code>MUSE_AGENT_KEY_FD</code> (a pipe file descriptor) and the agent's handle
1202 via <code>MUSE_AGENT_HANDLE</code>. The agent's <code>get_signing_identity()</code>
1203 reads the fd first, derives its Ed25519 key via SLIP-0010, then falls back to
1204 <code>~/.muse/identity.toml</code>. The root mnemonic never leaves the parent process.
1205 </div>
1206 </div>
1207 </section>
1208
1209 {# ── Key rotation ─────────────────────────────────────────────────────── #}
1210 <section class="devdocs-section" id="key-rotation">
1211 <h2 class="devdocs-section-title"><a href="#key-rotation">Key rotation</a></h2>
1212
1213 <p>
1214 Three distinct values describe a Muse cryptographic identity. Confusing them
1215 is the most common source of auth bugs — especially after key rotation.
1216 </p>
1217
1218 <table class="devdocs-table">
1219 <thead>
1220 <tr><th>Field</th><th>Derived from</th><th>Changes on rotation?</th><th>Where it lives</th></tr>
1221 </thead>
1222 <tbody>
1223 <tr>
1224 <td><code>identity_id</code></td>
1225 <td><code>sha256(first_registered_key_bytes)</code></td>
1226 <td><strong>Never</strong> — immutable anchor</td>
1227 <td><code>musehub_identities.identity_id</code></td>
1228 </tr>
1229 <tr>
1230 <td><code>fingerprint</code></td>
1231 <td><code>sha256(current_key_bytes)</code></td>
1232 <td>Yes — new value per rotation</td>
1233 <td><code>musehub_auth_keys.fingerprint</code></td>
1234 </tr>
1235 <tr>
1236 <td><code>public_key_b64</code></td>
1237 <td>Raw 32-byte Ed25519 public key, base64url (no padding)</td>
1238 <td>Yes — new value per rotation</td>
1239 <td><code>musehub_auth_keys.public_key_b64</code></td>
1240 </tr>
1241 </tbody>
1242 </table>
1243
1244 <p>
1245 <code>identity_id</code> is computed once when you first register and stored in
1246 <code>musehub_identities</code>. It is the stable anchor for your identity — every
1247 repo you create, every commit you sign, and every key you ever rotate to is linked
1248 back to this single value. It never changes.
1249 </p>
1250 <p>
1251 <code>fingerprint</code> and <code>public_key_b64</code> belong to individual keys,
1252 not the identity. They live in <code>musehub_auth_keys</code>, one row per
1253 registered key. An identity can have many registered keys simultaneously — one per
1254 device, one per agent, one from before a rotation. Deleting a row immediately
1255 revokes that key.
1256 </p>
1257
1258 <h3 class="devdocs-subsection-title"><a href="#keygen-vs-rotate"><code>muse auth keygen</code> vs <code>muse auth rotate</code></a></h3>
1259
1260 <p>
1261 These two commands look similar but have fundamentally different effects:
1262 </p>
1263
1264 <table class="devdocs-table">
1265 <thead>
1266 <tr><th></th><th><code>muse auth keygen</code></th><th><code>muse auth rotate</code></th></tr>
1267 </thead>
1268 <tbody>
1269 <tr>
1270 <td>What it does</td>
1271 <td>Generates a <strong>new BIP-39 mnemonic</strong>, derives key at index&nbsp;0</td>
1272 <td>Derives the <strong>next HD index</strong> from the existing mnemonic</td>
1273 </tr>
1274 <tr>
1275 <td><code>identity_id</code></td>
1276 <td>New value — a new identity</td>
1277 <td>Unchanged — same identity</td>
1278 </tr>
1279 <tr>
1280 <td>If handle is already registered</td>
1281 <td><code>409 Conflict</code> — handle is taken</td>
1282 <td>Succeeds — adds a new key row to the existing identity</td>
1283 </tr>
1284 <tr>
1285 <td>Old key after the operation</td>
1286 <td>Still registered (on the old identity)</td>
1287 <td>Still valid until you explicitly revoke it</td>
1288 </tr>
1289 <tr>
1290 <td>Use when</td>
1291 <td>First-time setup only, or you intentionally want a fresh identity</td>
1292 <td>Routine rotation, machine migration, compromise response</td>
1293 </tr>
1294 </tbody>
1295 </table>
1296
1297 <div class="devdocs-callout devdocs-callout--warn">
1298 {{ icon("alert", 16, "devdocs-callout-icon") }}
1299 <div>
1300 Running <code>muse auth keygen</code> when you already have a registered handle
1301 will <strong>not</strong> replace your existing identity. It generates a completely
1302 new mnemonic and a new <code>identity_id</code>. Attempting to register the new
1303 key under your existing handle returns <code>409 Conflict</code>. Use
1304 <code>muse auth rotate</code> for key rotation.
1305 </div>
1306 </div>
1307
1308 <h3 class="devdocs-subsection-title"><a href="#rotation-flow">Rotation flow</a></h3>
1309
1310 <div class="devdocs-code-block">
1311 <div class="devdocs-code-header">
1312 <span class="devdocs-code-lang">bash</span>
1313 <span class="devdocs-code-label">rotate to the next HD index</span>
1314 </div>
1315 <pre><code><span class="tok-cmt"># Derive next key, register it with the hub, update identity.toml</span>
1316 muse auth rotate --hub {{ site_base_url() }} --json
1317
1318 <span class="tok-cmt"># identity_id is unchanged. A second row now exists in musehub_auth_keys.</span>
1319 <span class="tok-cmt"># The old key is still valid. Revoke it once you confirm the new key works:</span>
1320 muse auth whoami --json <span class="tok-cmt"># confirm new fingerprint is active</span>
1321 muse auth logout --hub {{ site_base_url() }} --key-id &lt;old-key-id&gt; --json</code></pre>
1322 </div>
1323
1324 <p>
1325 After rotation the hub holds two rows for your identity in
1326 <code>musehub_auth_keys</code>:
1327 </p>
1328
1329 <div class="devdocs-code-block">
1330 <div class="devdocs-code-header">
1331 <span class="devdocs-code-lang">json</span>
1332 <span class="devdocs-code-label">musehub_auth_keys — two rows, same identity_id</span>
1333 </div>
1334 <pre><code>[
1335 {
1336 <span class="tok-key">"identity_id"</span>: <span class="tok-str">"sha256:41dfaf04…"</span>, <span class="tok-cmt">// ← never changes</span>
1337 <span class="tok-key">"fingerprint"</span>: <span class="tok-str">"sha256:a3f2c9d8…"</span>, <span class="tok-cmt">// old key</span>
1338 <span class="tok-key">"algorithm"</span>: <span class="tok-str">"ed25519"</span>,
1339 <span class="tok-key">"public_key_b64"</span>: <span class="tok-str">"3aB7kLmN…"</span>,
1340 <span class="tok-key">"label"</span>: <span class="tok-str">"macbook-pro"</span>,
1341 <span class="tok-key">"created_at"</span>: <span class="tok-str">"2026-04-01T10:00:00Z"</span>
1342 },
1343 {
1344 <span class="tok-key">"identity_id"</span>: <span class="tok-str">"sha256:41dfaf04…"</span>, <span class="tok-cmt">// ← same identity_id</span>
1345 <span class="tok-key">"fingerprint"</span>: <span class="tok-str">"sha256:d8faf800…"</span>, <span class="tok-cmt">// new key after rotation</span>
1346 <span class="tok-key">"algorithm"</span>: <span class="tok-str">"ed25519"</span>,
1347 <span class="tok-key">"public_key_b64"</span>: <span class="tok-str">"qR5sT6uV…"</span>,
1348 <span class="tok-key">"label"</span>: <span class="tok-str">"macbook-pro (rotated)"</span>,
1349 <span class="tok-key">"created_at"</span>: <span class="tok-str">"2026-05-01T09:00:00Z"</span>
1350 }
1351 ]</code></pre>
1352 </div>
1353
1354 <h3 class="devdocs-subsection-title"><a href="#profile-display">What the profile page displays</a></h3>
1355
1356 <p>
1357 The profile hero strip always shows the <strong>most recently registered key</strong>
1358 for the identity — the row from <code>musehub_auth_keys</code> with the latest
1359 <code>created_at</code> for that <code>identity_id</code>. It does not display the
1360 <code>identity_id</code> as the key — the <code>identity_id</code> is a stable
1361 account anchor, not a per-request credential.
1362 </p>
1363
1364 <table class="devdocs-table">
1365 <thead>
1366 <tr><th>Profile strip row</th><th>Source</th></tr>
1367 </thead>
1368 <tbody>
1369 <tr>
1370 <td>Algorithm label (<code>ed25519</code>)</td>
1371 <td><code>musehub_auth_keys.algorithm</code> — most recent key</td>
1372 </tr>
1373 <tr>
1374 <td>Public key (<code>3aB7kLmN…</code>)</td>
1375 <td><code>musehub_auth_keys.public_key_b64</code> — most recent key</td>
1376 </tr>
1377 <tr>
1378 <td>Fingerprint (<code>sha256:d8fa…</code>)</td>
1379 <td><code>musehub_auth_keys.fingerprint</code> — most recent key</td>
1380 </tr>
1381 <tr>
1382 <td>Registered date</td>
1383 <td><code>musehub_identities.created_at</code> — identity registration date, not key rotation date</td>
1384 </tr>
1385 </tbody>
1386 </table>
1387
1388 <p>
1389 If no key row exists for an identity (e.g. the identity was created through a code
1390 path that bypassed the standard auth flow), the profile falls back to displaying
1391 the <code>identity_id</code> as the fingerprint. This is a degraded state — the
1392 correct fix is to run <code>muse auth rotate</code> to register an active key.
1393 </p>
1394
1395 <h3 class="devdocs-subsection-title"><a href="#rotation-invariant">The rotation invariant</a></h3>
1396
1397 <p>
1398 <strong>The identity is the mnemonic, not the key.</strong> Keys are disposable
1399 credentials derived from the mnemonic. You can rotate as often as you like and hold
1400 multiple active keys simultaneously. None of this changes who you are — your
1401 <code>identity_id</code>, your repos, your commits, your attestations are all
1402 anchored to the mnemonic, not to any individual key.
1403 </p>
1404 </section>
1405
1406 {# ── Agent sub-keys ──────────────────────────────────────────────────── #}
1407 <section class="devdocs-section" id="agent-keys">
1408 <h2 class="devdocs-section-title"><a href="#agent-keys">Agent sub-keys</a></h2>
1409
1410 <p>
1411 Every agent in the ecosystem inherits its signing key from the parent's root
1412 mnemonic via HD derivation — not a fresh mnemonic. The parent process (human
1413 shell or Agentception orchestrator) derives a 64-byte sub-seed at a specific
1414 HD path and passes it to the spawned agent over a pipe. The agent's process
1415 never sees the root mnemonic.
1416 </p>
1417
1418 <h3 class="devdocs-subsection-title"><a href="#key-injection">Key injection — two environment variables</a></h3>
1419
1420 <table class="devdocs-table">
1421 <thead><tr><th>Variable</th><th>Value</th><th>Purpose</th></tr></thead>
1422 <tbody>
1423 <tr>
1424 <td><code>MUSE_AGENT_KEY_FD</code></td>
1425 <td>integer (file descriptor number)</td>
1426 <td>Read end of a pipe carrying 64 raw bytes — the agent's sub-seed</td>
1427 </tr>
1428 <tr>
1429 <td><code>MUSE_AGENT_HANDLE</code></td>
1430 <td>string</td>
1431 <td>The agent's registered handle on the hub; paired with the injected key</td>
1432 </tr>
1433 </tbody>
1434 </table>
1435
1436 <h3 class="devdocs-subsection-title"><a href="#identity-resolution">Identity resolution order</a></h3>
1437
1438 <p>
1439 <code>get_signing_identity()</code> in <code>muse.core.auth</code> checks
1440 these sources in order, using the first one that is set:
1441 </p>
1442
1443 <div class="devdocs-code-block">
1444 <div class="devdocs-code-header">
1445 <span class="devdocs-code-lang">text</span>
1446 <span class="devdocs-code-label">resolution order</span>
1447 </div>
1448 <pre><code>1. MUSE_AGENT_KEY_FD set?
1449 → read exactly 64 bytes from that fd
1450 → derive Ed25519 keypair via SLIP-0010
1451 → handle = MUSE_AGENT_HANDLE
1452
1453 2. MUSE_AGENT_HD_SEED set?
1454 → treat as hex-encoded 64-byte sub-seed
1455 → derive Ed25519 keypair via SLIP-0010
1456
1457 3. ~/.muse/identity.toml present?
1458 → load matching [host] or [host#handle] section
1459
1460 4. None of the above → IdentityNotFound raised</code></pre>
1461 </div>
1462
1463 <h3 class="devdocs-subsection-title"><a href="#agentception-injection">Agentception injection sequence</a></h3>
1464
1465 <div class="devdocs-code-block">
1466 <div class="devdocs-code-header">
1467 <span class="devdocs-code-lang">python</span>
1468 <span class="devdocs-code-label">orchestrator — deriving and injecting a sub-seed</span>
1469 </div>
1470 <pre><code><span class="tok-kw">import</span> os, subprocess
1471 <span class="tok-kw">from</span> muse.core.identity <span class="tok-kw">import</span> derive_sub_seed
1472
1473 <span class="tok-cmt"># Derive a domain-scoped sub-seed for agent slot 7</span>
1474 sub_seed = derive_sub_seed(
1475 root_seed=orchestrator_root_seed,
1476 domain=<span class="tok-num">678195575</span>, <span class="tok-cmt"># Code domain (muse/code)</span>
1477 entity_type=<span class="tok-num">1</span>, <span class="tok-cmt"># Agent</span>
1478 entity_id=<span class="tok-num">7</span>, <span class="tok-cmt"># Unique slot</span>
1479 ) <span class="tok-cmt"># → 64 bytes; cannot produce Identity or Payment keys</span>
1480
1481 <span class="tok-cmt"># Open a pipe; write the sub-seed into the write end and close it</span>
1482 read_fd, write_fd = os.pipe()
1483 os.write(write_fd, sub_seed) <span class="tok-cmt"># exactly 64 bytes</span>
1484 os.close(write_fd)
1485
1486 <span class="tok-cmt"># Spawn the agent with the read fd and handle in its environment</span>
1487 proc = subprocess.Popen(
1488 [<span class="tok-str">"python3"</span>, <span class="tok-str">"-m"</span>, <span class="tok-str">"my_agent"</span>],
1489 env={
1490 **os.environ,
1491 <span class="tok-str">"MUSE_AGENT_KEY_FD"</span>: str(read_fd),
1492 <span class="tok-str">"MUSE_AGENT_HANDLE"</span>: <span class="tok-str">"worker-7"</span>,
1493 },
1494 pass_fds=(read_fd,),
1495 )
1496 os.close(read_fd) <span class="tok-cmt"># orchestrator closes its copy after spawn</span></code></pre>
1497 </div>
1498
1499 <div class="devdocs-code-block">
1500 <div class="devdocs-code-header">
1501 <span class="devdocs-code-lang">python</span>
1502 <span class="devdocs-code-label">spawned agent — identity resolves automatically</span>
1503 </div>
1504 <pre><code><span class="tok-kw">from</span> muse.core.auth <span class="tok-kw">import</span> get_signing_identity
1505
1506 <span class="tok-cmt"># muse commit, muse push, muse sign all call this internally.</span>
1507 <span class="tok-cmt"># The agent process never needs to call it directly.</span>
1508 identity = get_signing_identity(hub=<span class="tok-str">"{{ site_base_url() }}"</span>)
1509 <span class="tok-cmt"># → SigningIdentity(handle="worker-7", algorithm="ed25519",</span>
1510 <span class="tok-cmt"># public_key="ed25519:...")</span></code></pre>
1511 </div>
1512
1513 <div class="devdocs-callout">
1514 {{ icon("info", 16, "devdocs-callout-icon") }}
1515 <div>
1516 The sub-seed is domain-scoped by construction. A Code-domain sub-seed
1517 (<code>domain=678195575</code>) can only derive Code-domain keys — it physically
1518 cannot produce an Identity (<code>domain=1660078172'</code>) or Payment
1519 (<code>domain=284229149'</code>) key. There is no policy rule enforcing this;
1520 the HD math simply does not connect those branches.
1521 </div>
1522 </div>
1523 </section>
1524
1525 {# ── Identity repos ──────────────────────────────────────────────────── #}
1526 <section class="devdocs-section" id="identity-repos">
1527 <h2 class="devdocs-section-title"><a href="#identity-repos">Identity repos</a></h2>
1528
1529 <p>
1530 Every registered identity on MuseHub has a dedicated Muse repository:
1531 <code>{handle}/identity</code> with <code>domain="identity"</code>.
1532 This repo is the <strong>canonical source of truth</strong> for a principal's
1533 public key, quorum threshold, and relationship graph — the PostgreSQL tables
1534 are a queryable index rebuilt from it, not the other way around.
1535 </p>
1536
1537 <p>
1538 Because the identity repo is a normal Muse repo, you get content-addressed
1539 history for free: every key rotation, every membership change, and every
1540 spawns relationship is a commit with a tamper-evident SHA-256 ID, auditable
1541 with <code>muse log</code>.
1542 </p>
1543
1544 <h3 class="devdocs-subsection-title"><a href="#identity-record">IdentityRecord — the file stored in the repo</a></h3>
1545
1546 <p>
1547 Each identity repo stores one JSON file at
1548 <code>identities/{handle}.json</code> (the <code>IdentityRecord</code>)
1549 and zero or more relationship files at
1550 <code>relationships/{from}--{edge}--{to}.json</code>:
1551 </p>
1552
1553 <div class="devdocs-code-block">
1554 <div class="devdocs-code-header">
1555 <span class="devdocs-code-lang">json</span>
1556 <span class="devdocs-code-label">identities/gabriel.json — IdentityRecord</span>
1557 </div>
1558 <pre><code>{
1559 <span class="tok-key">"handle"</span>: <span class="tok-str">"gabriel"</span>,
1560 <span class="tok-key">"type"</span>: <span class="tok-str">"human"</span>,
1561 <span class="tok-key">"pubkey"</span>: <span class="tok-str">"ed25519:scbtcAeEYMv3cCBNcYJU153gqaT1UpSBVDVttTj_9-Y"</span>,
1562 <span class="tok-key">"quorum"</span>: <span class="tok-kw">null</span>,
1563 <span class="tok-key">"registered_at"</span>: <span class="tok-str">"2026-04-21T14:32:07Z"</span>,
1564 <span class="tok-key">"metadata"</span>: { <span class="tok-key">"display_name"</span>: <span class="tok-str">"Gabriel"</span> }
1565 }</code></pre>
1566 </div>
1567
1568 <table class="devdocs-table">
1569 <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
1570 <tbody>
1571 <tr><td><code>handle</code></td><td>str</td><td>Hub username — matches the repo owner slug</td></tr>
1572 <tr><td><code>type</code></td><td><code>"human" | "agent" | "org"</code></td><td>Principal type</td></tr>
1573 <tr><td><code>pubkey</code></td><td><code>ed25519:&lt;base64url&gt;</code> | null</td><td>Current signing public key. Null for orgs (authority is quorum, not a single key)</td></tr>
1574 <tr><td><code>quorum</code></td><td>int | null</td><td>Approval threshold for org governance. Null for humans and agents</td></tr>
1575 <tr><td><code>registered_at</code></td><td>ISO-8601 string</td><td>When this identity was first registered</td></tr>
1576 <tr><td><code>metadata</code></td><td>object</td><td>Optional display fields: <code>display_name</code>, etc.</td></tr>
1577 </tbody>
1578 </table>
1579
1580 <h3 class="devdocs-subsection-title"><a href="#identity-repo-layout">Repo layout</a></h3>
1581
1582 <div class="devdocs-code-block">
1583 <div class="devdocs-code-header">
1584 <span class="devdocs-code-lang">text</span>
1585 <span class="devdocs-code-label">gabriel/identity repo — HEAD snapshot</span>
1586 </div>
1587 <pre><code>identities/
1588 gabriel.json ← IdentityRecord
1589
1590 relationships/
1591 gabriel--spawns--claude-code.json ← RelationshipRecord
1592 gabriel--member_of--acme-org.json ← RelationshipRecord</code></pre>
1593 </div>
1594
1595 <h3 class="devdocs-subsection-title"><a href="#identity-repo-api">REST API — reading from the repo</a></h3>
1596
1597 <p>
1598 <code>GET /api/identities/{handle}</code> reads <code>pubkey</code>,
1599 <code>quorum</code>, <code>identity_type</code>, and <code>display_name</code>
1600 from the identity repo HEAD. If the repo is absent (e.g. pre-registration or
1601 migration period) the response falls back to the DB values.
1602 </p>
1603
1604 <div class="devdocs-code-block">
1605 <div class="devdocs-code-header">
1606 <span class="devdocs-code-lang">bash</span>
1607 </div>
1608 <pre><code>curl {{ site_base_url() }}/api/identities/gabriel</code></pre>
1609 </div>
1610 <div class="devdocs-code-block devdocs-code-block--output">
1611 <div class="devdocs-code-header">
1612 <span class="devdocs-code-lang">json</span>
1613 </div>
1614 <pre><code>{
1615 <span class="tok-key">"handle"</span>: <span class="tok-str">"gabriel"</span>,
1616 <span class="tok-key">"identity_type"</span>: <span class="tok-str">"human"</span>,
1617 <span class="tok-key">"pubkey"</span>: <span class="tok-str">"ed25519:scbtcAeEYMv3cCBNcYJU153gqaT1UpSBVDVttTj_9-Y"</span>,
1618 <span class="tok-key">"quorum"</span>: <span class="tok-kw">null</span>,
1619 <span class="tok-key">"display_name"</span>: <span class="tok-str">"Gabriel"</span>,
1620 <span class="tok-key">"fingerprint"</span>: <span class="tok-str">"sha256:220897cdf3a..."</span>,
1621 <span class="tok-key">"created_at"</span>: <span class="tok-str">"2026-04-21T14:32:07Z"</span>
1622 }</code></pre>
1623 </div>
1624
1625 <h3 class="devdocs-subsection-title"><a href="#identity-repo-rotation">Key rotation as a commit</a></h3>
1626
1627 <p>
1628 <code>muse auth rotate</code> does two things atomically: it re-registers the new
1629 public key with the hub and commits an updated <code>IdentityRecord</code> to the
1630 identity repo. This gives a full, tamper-evident rotation history:
1631 </p>
1632
1633 <div class="devdocs-code-block">
1634 <div class="devdocs-code-header">
1635 <span class="devdocs-code-lang">bash</span>
1636 </div>
1637 <pre><code>muse auth rotate --hub {{ site_base_url() }} --json
1638 <span class="tok-cmt"># Rotates to the next HD index, updates hub registration,</span>
1639 <span class="tok-cmt"># and commits a new identities/gabriel.json to gabriel/identity.</span>
1640
1641 <span class="tok-cmt"># View the full rotation history</span>
1642 muse -C gabriel/identity log --json</code></pre>
1643 </div>
1644
1645 <p>
1646 The Python API for reading the canonical record directly from the repo:
1647 </p>
1648
1649 <div class="devdocs-code-block">
1650 <div class="devdocs-code-header">
1651 <span class="devdocs-code-lang">python</span>
1652 <span class="devdocs-code-label">muse.plugins.identity.records</span>
1653 </div>
1654 <pre><code><span class="tok-kw">from</span> muse.plugins.identity.records <span class="tok-kw">import</span> (
1655 IdentityRecord, RelationshipRecord,
1656 identity_path, relationship_path,
1657 record_to_bytes,
1658 )
1659
1660 <span class="tok-cmt"># Build a record</span>
1661 record: IdentityRecord = {
1662 <span class="tok-str">"handle"</span>: <span class="tok-str">"gabriel"</span>,
1663 <span class="tok-str">"type"</span>: <span class="tok-str">"human"</span>,
1664 <span class="tok-str">"pubkey"</span>: <span class="tok-str">"ed25519:..."</span>,
1665 <span class="tok-str">"quorum"</span>: <span class="tok-kw">None</span>,
1666 <span class="tok-str">"registered_at"</span>: <span class="tok-str">"2026-04-21T14:32:07Z"</span>,
1667 <span class="tok-str">"metadata"</span>: {<span class="tok-str">"display_name"</span>: <span class="tok-str">"Gabriel"</span>},
1668 }
1669
1670 <span class="tok-cmt"># File paths in the identity repo</span>
1671 identity_path(<span class="tok-str">"gabriel"</span>) <span class="tok-cmt"># → "identities/gabriel.json"</span>
1672 relationship_path(<span class="tok-str">"gabriel"</span>, <span class="tok-str">"spawns"</span>, <span class="tok-str">"claude-code"</span>) <span class="tok-cmt"># → "relationships/gabriel--spawns--claude-code.json"</span></code></pre>
1673 </div>
1674 </section>
1675
1676 {# ── Recovery ─────────────────────────────────────────────────────────── #}
1677 <section class="devdocs-section" id="recover">
1678 <h2 class="devdocs-section-title"><a href="#recover">Key recovery</a></h2>
1679
1680 <p>
1681 If <code>~/.muse/identity.toml</code> is lost or you move to a new machine,
1682 <code>muse auth recover</code> rebuilds your identity from the 24-word BIP-39
1683 mnemonic stored in your password manager. Every derived key is deterministic —
1684 the same mnemonic always produces the same key tree.
1685 </p>
1686
1687 <div class="devdocs-code-block">
1688 <div class="devdocs-code-header">
1689 <span class="devdocs-code-lang">bash</span>
1690 <span class="devdocs-code-label">muse auth recover — interactive transcript</span>
1691 </div>
1692 <pre><code>$ muse auth recover --hub {{ site_base_url() }}
1693
1694 Enter your 24-word BIP-39 mnemonic (words separated by spaces):
1695 &gt; abandon ability able about above absent absorb abstract absurd abuse
1696 access accident account accuse achieve acid acoustic acquire across
1697 act action actor actress actual
1698
1699 ✔ Mnemonic valid (24 words, checksum OK)
1700
1701 Deriving keys from mnemonic...
1702 domain=1660078172' (Identity), entity_type=0' (Human), entity_id=0', index=0'
1703 hd_path: m/1075233755'/1660078172'/0'/0'/0'/0'
1704 public_key: ed25519:3aB7kLmNpQ2rS4tU5vW6xY7zA8bC9dE0fG1hI2jK3lM
1705 fingerprint: a3f2c9d8e1b4...
1706
1707 Checking hub registration...
1708 ✔ Key registered — handle: gabriel
1709
1710 Restored ~/.muse/identity.toml
1711 [staging.musehub.ai]
1712 handle = "gabriel"
1713 fingerprint = "a3f2c9d8e1b4..."
1714 hd_path = "m/1075233755'/1660078172'/0'/0'/0'/0'"</code></pre>
1715 </div>
1716
1717 <div class="devdocs-callout devdocs-callout--warn">
1718 {{ icon("alert", 16, "devdocs-callout-icon") }}
1719 <div>
1720 Back up the mnemonic immediately after <code>muse auth keygen</code> — pipe it
1721 from the keychain directly to your password manager so it never appears in
1722 terminal scrollback:
1723 <code>security find-generic-password -s muse -a mnemonic -w | pbcopy</code> (macOS)
1724 or
1725 <code>secret-tool lookup service muse account mnemonic | xclip -selection clipboard</code> (Linux).
1726 Losing the mnemonic means permanent loss of all derived keys.
1727 </div>
1728 </div>
1729 </section>
1730
1731 {# ── Migration ───────────────────────────────────────────────────────── #}
1732 <section class="devdocs-section" id="migrate">
1733 <h2 class="devdocs-section-title"><a href="#migrate">Migration — pre-Phase-1 keys</a></h2>
1734
1735 <p>
1736 Muse Phase 1 replaced sequential domain integers (0–6) with hash-derived
1737 values computed as
1738 <code>int.from_bytes(sha256(name.encode())[:4], "big") &amp; 0x7FFFFFFF</code>.
1739 Users who generated their identity before Phase 1 have keys derived at the
1740 old paths. <code>muse migrate domain-integers</code> detects those entries in
1741 <code>~/.muse/identity.toml</code>, re-derives the correct keys at the new
1742 paths, and re-registers each fingerprint with its hub.
1743 </p>
1744
1745 <div class="devdocs-code-block">
1746 <div class="devdocs-code-header">
1747 <span class="devdocs-code-lang">bash</span>
1748 <span class="devdocs-code-label">migrate legacy keys</span>
1749 </div>
1750 <pre><code><span class="tok-cmt"># Inspect — see what would change (no writes)</span>
1751 muse migrate domain-integers --dry-run --json
1752
1753 <span class="tok-cmt"># Apply — re-derive keys and re-register with each hub</span>
1754 muse migrate domain-integers --json
1755
1756 <span class="tok-cmt"># Re-derive keys and update identity.toml, but skip hub re-registration</span>
1757 muse migrate domain-integers --no-register --json</code></pre>
1758 </div>
1759
1760 <div class="devdocs-code-block devdocs-code-block--output">
1761 <div class="devdocs-code-header">
1762 <span class="devdocs-code-lang">json</span>
1763 <span class="devdocs-code-label">--dry-run output</span>
1764 </div>
1765 <pre><code>{
1766 <span class="tok-key">"dry_run"</span>: <span class="tok-kw">true</span>,
1767 <span class="tok-key">"entries_found"</span>: <span class="tok-num">1</span>,
1768 <span class="tok-key">"entries_migrated"</span>: <span class="tok-num">0</span>,
1769 <span class="tok-key">"results"</span>: [
1770 {
1771 <span class="tok-key">"hub_key"</span>: <span class="tok-str">"staging.musehub.ai"</span>,
1772 <span class="tok-key">"old_hd_path"</span>: <span class="tok-str">"m/1075233755'/0'/0'/0'/0'/0'"</span>,
1773 <span class="tok-key">"new_hd_path"</span>: <span class="tok-str">"m/1075233755'/1660078172'/0'/0'/0'/0'"</span>,
1774 <span class="tok-key">"old_fingerprint"</span>: <span class="tok-str">"sha256:a3f2c9d8..."</span>,
1775 <span class="tok-key">"new_fingerprint"</span>: <span class="tok-str">"sha256:b7e1a4c2..."</span>,
1776 <span class="tok-key">"hub_registered"</span>: <span class="tok-kw">false</span>
1777 }
1778 ]
1779 }</code></pre>
1780 </div>
1781
1782 <div class="devdocs-callout devdocs-callout--warn">
1783 {{ icon("alert", 16, "devdocs-callout-icon") }}
1784 <div>
1785 Migration is irreversible in the sense that the hub now expects the new
1786 fingerprint. Always run <code>--dry-run</code> first to inspect what will
1787 change. The mnemonic in the keychain is unchanged — only the derived HD
1788 path and hub registration are updated. If hub re-registration fails, the
1789 key is still re-derived locally; run the command again once the hub is
1790 reachable.
1791 </div>
1792 </div>
1793 </section>
1794
1795 {# ── MPay ─────────────────────────────────────────────────────────────── #}
1796 <section class="devdocs-section" id="mpay">
1797 <h2 class="devdocs-section-title"><a href="#mpay">MPay — Micropayments</a></h2>
1798 <p>
1799 MPay is the native micropayment layer built directly on the same Ed25519 identity
1800 infrastructure as MSign. There are no payment processors, no API keys, no OAuth
1801 scopes — a payment is just a canonical message signed by the sender's key.
1802 Units are <strong>nanoMUSE</strong> (10<sup>−9</sup> MUSE).
1803 </p>
1804
1805 <h3 class="devdocs-subsection-title"><a href="#mpay-canonical">Canonical message</a></h3>
1806 <p>
1807 The message signed by the sender is UTF-8 encoded, newline-separated:
1808 </p>
1809 <div class="devdocs-code-block">
1810 <div class="devdocs-code-header">
1811 <span class="devdocs-code-lang">text</span>
1812 <span class="devdocs-code-label">MPay canonical message format</span>
1813 </div>
1814 <pre><code>MPAY
1815 {sender_handle}
1816 {recipient_handle}
1817 {amount_nano}
1818 {nonce_hex}</code></pre>
1819 </div>
1820 <p>
1821 <code>nonce_hex</code> is a random hex string generated by the sender for each
1822 payment. The server enforces a unique constraint on nonces — submitting the same
1823 nonce twice returns the existing claim (idempotent).
1824 </p>
1825
1826 <h3 class="devdocs-subsection-title"><a href="#mpay-sign">Full sign → submit → verify worked example</a></h3>
1827
1828 <p>Step 1 — sign the payment. <code>muse sign payment</code> prints the canonical
1829 message, the Ed25519 signature, and the complete <code>MPayClaimRequest</code>
1830 payload ready to POST:</p>
1831
1832 <div class="devdocs-code-block">
1833 <div class="devdocs-code-header">
1834 <span class="devdocs-code-lang">bash</span>
1835 <span class="devdocs-code-label">sign a 1 µMUSE payment</span>
1836 </div>
1837 <pre><code>muse sign payment \
1838 --from gabriel \
1839 --to alice \
1840 --amount <span class="tok-num">1000000</span> \
1841 --nonce a3f9c2d1b4e87f560c8a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3 \
1842 --json</code></pre>
1843 </div>
1844 <div class="devdocs-code-block devdocs-code-block--output">
1845 <div class="devdocs-code-header">
1846 <span class="devdocs-code-lang">json</span>
1847 <span class="devdocs-code-label">output</span>
1848 </div>
1849 <pre><code>{
1850 <span class="tok-key">"canonical_message"</span>: <span class="tok-str">"MPAY\ngabriel\nalice\n1000000\na3f9c2d1b4e87f560c8a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"</span>,
1851 <span class="tok-key">"signature"</span>: <span class="tok-str">"ed25519:Xe3kA7mBnC2dE5fG8hI1jK4lM6nO9pQ0rS3tU6vW9yZ1"</span>,
1852 <span class="tok-key">"sender_public_key"</span>: <span class="tok-str">"ed25519:3aB7kLmNpQ2rS4tU5vW6xY7zA8bC9dE0fG1hI2jK3lM"</span>,
1853 <span class="tok-key">"claim_request"</span>: {
1854 <span class="tok-key">"sender"</span>: <span class="tok-str">"gabriel"</span>,
1855 <span class="tok-key">"recipient"</span>: <span class="tok-str">"alice"</span>,
1856 <span class="tok-key">"amount_nano"</span>: <span class="tok-num">1000000</span>,
1857 <span class="tok-key">"nonce_hex"</span>: <span class="tok-str">"a3f9c2d1b4e87f560c8a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"</span>,
1858 <span class="tok-key">"signature"</span>: <span class="tok-str">"ed25519:Xe3kA7mBnC2dE5fG8hI1jK4lM6nO9pQ0rS3tU6vW9yZ1"</span>,
1859 <span class="tok-key">"sender_public_key"</span>: <span class="tok-str">"ed25519:3aB7kLmNpQ2rS4tU5vW6xY7zA8bC9dE0fG1hI2jK3lM"</span>,
1860 <span class="tok-key">"memo"</span>: <span class="tok-kw">null</span>
1861 }
1862 }</code></pre>
1863 </div>
1864
1865 <p>Step 2 — submit the claim. Pipe the <code>claim_request</code> object to
1866 <code>POST /mpay/claim</code> using <code>muse sign request</code> to
1867 attach the MSign Authorization header:</p>
1868
1869 <div class="devdocs-code-block">
1870 <div class="devdocs-code-header">
1871 <span class="devdocs-code-lang">bash</span>
1872 <span class="devdocs-code-label">submit claim to the hub</span>
1873 </div>
1874 <pre><code><span class="tok-cmt"># Write the claim_request object to a file</span>
1875 muse sign payment --from gabriel --to alice --amount <span class="tok-num">1000000</span> \
1876 --nonce a3f9c2d1b4e87f560c8a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3 --json \
1877 | python3 -c "import sys,json; json.dump(json.load(sys.stdin)['claim_request'], open('/tmp/claim.json','w'))"
1878
1879 <span class="tok-cmt"># Sign and POST</span>
1880 muse sign request \
1881 --method POST \
1882 --url {{ site_base_url() }}/mpay/claim \
1883 --body-file /tmp/claim.json \
1884 --json</code></pre>
1885 </div>
1886 <div class="devdocs-code-block devdocs-code-block--output">
1887 <div class="devdocs-code-header">
1888 <span class="devdocs-code-lang">json</span>
1889 <span class="devdocs-code-label">MPayClaimResponse</span>
1890 </div>
1891 <pre><code>{
1892 <span class="tok-key">"claim_id"</span>: <span class="tok-str">"sha256:7f4a2b8cd3e9f1a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8"</span>,
1893 <span class="tok-key">"sender"</span>: <span class="tok-str">"gabriel"</span>,
1894 <span class="tok-key">"recipient"</span>: <span class="tok-str">"alice"</span>,
1895 <span class="tok-key">"amount_nano"</span>: <span class="tok-num">1000000</span>,
1896 <span class="tok-key">"nonce_hex"</span>: <span class="tok-str">"a3f9c2d1b4e87f560c8a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"</span>,
1897 <span class="tok-key">"signature"</span>: <span class="tok-str">"ed25519:Xe3kA7mBnC2dE5fG8hI1jK4lM6nO9pQ0rS3tU6vW9yZ1"</span>,
1898 <span class="tok-key">"sender_public_key"</span>: <span class="tok-str">"ed25519:3aB7kLmNpQ2rS4tU5vW6xY7zA8bC9dE0fG1hI2jK3lM"</span>,
1899 <span class="tok-key">"memo"</span>: <span class="tok-kw">null</span>,
1900 <span class="tok-key">"created_at"</span>: <span class="tok-str">"2026-04-21T15:00:00Z"</span>,
1901 <span class="tok-key">"confirmed_at"</span>: <span class="tok-kw">null</span>,
1902 <span class="tok-key">"voided_at"</span>: <span class="tok-kw">null</span>
1903 }</code></pre>
1904 </div>
1905
1906 <p>Step 3 — query the ledger to verify settlement:</p>
1907
1908 <div class="devdocs-code-block">
1909 <div class="devdocs-code-header">
1910 <span class="devdocs-code-lang">bash</span>
1911 <span class="devdocs-code-label">query sender ledger</span>
1912 </div>
1913 <pre><code>muse sign request \
1914 --method GET \
1915 --url {{ site_base_url() }}/mpay/ledger/gabriel \
1916 --json</code></pre>
1917 </div>
1918
1919 <h3 class="devdocs-subsection-title"><a href="#mpay-request">MPayClaimRequest fields</a></h3>
1920 <table class="devdocs-table">
1921 <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
1922 <tbody>
1923 <tr><td><code>sender</code></td><td>str</td><td>Sending identity handle</td></tr>
1924 <tr><td><code>recipient</code></td><td>str</td><td>Receiving identity handle</td></tr>
1925 <tr><td><code>amount_nano</code></td><td>int</td><td>Amount in nanoMUSE (10<sup>−9</sup> MUSE)</td></tr>
1926 <tr><td><code>nonce_hex</code></td><td>str</td><td>Exactly 64-char hex string (32 random bytes); server enforces uniqueness per sender</td></tr>
1927 <tr><td><code>signature</code></td><td>str</td><td><code>ed25519:&lt;base64url&gt;</code> — signature over the canonical message</td></tr>
1928 <tr><td><code>sender_public_key</code></td><td>str</td><td><code>ed25519:&lt;base64url&gt;</code> — sender's public key for verification</td></tr>
1929 <tr><td><code>memo</code></td><td>str | null</td><td>Optional human-readable note attached to the payment</td></tr>
1930 </tbody>
1931 </table>
1932
1933 <h3 class="devdocs-subsection-title"><a href="#mpay-response">MPayClaimResponse fields</a></h3>
1934 <table class="devdocs-table">
1935 <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
1936 <tbody>
1937 <tr><td><code>claim_id</code></td><td>str</td><td><code>sha256:&lt;64-hex&gt;</code> content-addressed claim ID</td></tr>
1938 <tr><td><code>sender</code></td><td>str</td><td>Sending identity handle</td></tr>
1939 <tr><td><code>recipient</code></td><td>str</td><td>Receiving identity handle</td></tr>
1940 <tr><td><code>amount_nano</code></td><td>int</td><td>Payment amount in nanoMUSE</td></tr>
1941 <tr><td><code>nonce_hex</code></td><td>str</td><td>64-char hex nonce from the original request</td></tr>
1942 <tr><td><code>signature</code></td><td>str</td><td><code>ed25519:&lt;base64url&gt;</code> — sender's signature</td></tr>
1943 <tr><td><code>sender_public_key</code></td><td>str</td><td><code>ed25519:&lt;base64url&gt;</code> — sender's public key</td></tr>
1944 <tr><td><code>memo</code></td><td>str | null</td><td>Optional payment memo</td></tr>
1945 <tr><td><code>created_at</code></td><td>str</td><td>UTC timestamp of claim creation</td></tr>
1946 <tr><td><code>confirmed_at</code></td><td>str | null</td><td>UTC timestamp of settlement; null if pending</td></tr>
1947 <tr><td><code>voided_at</code></td><td>str | null</td><td>UTC timestamp if claim was voided; null otherwise</td></tr>
1948 </tbody>
1949 </table>
1950
1951 <h3 class="devdocs-subsection-title"><a href="#mpay-ledger">Ledger query</a></h3>
1952 <p>
1953 The ledger endpoint returns all sent and received claims for an identity,
1954 with running totals:
1955 </p>
1956 <div class="devdocs-code-block devdocs-code-block--output">
1957 <div class="devdocs-code-header">
1958 <span class="devdocs-code-lang">json</span>
1959 <span class="devdocs-code-label">GET /mpay/ledger/gabriel — response</span>
1960 </div>
1961 <pre><code>{
1962 <span class="tok-key">"handle"</span>: <span class="tok-str">"gabriel"</span>,
1963 <span class="tok-key">"sent"</span>: [
1964 {
1965 <span class="tok-key">"claim_id"</span>: <span class="tok-str">"sha256:7f4a2b8cd3e9f1a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8"</span>,
1966 <span class="tok-key">"sender"</span>: <span class="tok-str">"gabriel"</span>,
1967 <span class="tok-key">"recipient"</span>: <span class="tok-str">"alice"</span>,
1968 <span class="tok-key">"amount_nano"</span>: <span class="tok-num">1000000</span>,
1969 <span class="tok-key">"nonce_hex"</span>: <span class="tok-str">"a3f9c2d1b4e87f560c8a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3"</span>,
1970 <span class="tok-key">"signature"</span>: <span class="tok-str">"ed25519:Xe3kA7mBnC2dE5fG8hI1jK4lM6nO9pQ0rS3tU6vW9yZ1"</span>,
1971 <span class="tok-key">"sender_public_key"</span>: <span class="tok-str">"ed25519:3aB7kLmNpQ2rS4tU5vW6xY7zA8bC9dE0fG1hI2jK3lM"</span>,
1972 <span class="tok-key">"memo"</span>: <span class="tok-kw">null</span>,
1973 <span class="tok-key">"created_at"</span>: <span class="tok-str">"2026-04-21T15:00:00Z"</span>,
1974 <span class="tok-key">"confirmed_at"</span>: <span class="tok-str">"2026-04-21T15:00:03Z"</span>,
1975 <span class="tok-key">"voided_at"</span>: <span class="tok-kw">null</span>
1976 }
1977 ],
1978 <span class="tok-key">"received"</span>: [],
1979 <span class="tok-key">"total_sent_nano"</span>: <span class="tok-num">1000000</span>,
1980 <span class="tok-key">"total_received_nano"</span>: <span class="tok-num">0</span>
1981 }</code></pre>
1982 </div>
1983
1984 <div class="devdocs-callout devdocs-callout--warn">
1985 {{ icon("alert", 16, "devdocs-callout-icon") }}
1986 <div>
1987 Verification rejects the claim if any of the four canonical fields
1988 (<code>sender</code>, <code>recipient</code>, <code>amount_nano</code>,
1989 <code>nonce_hex</code>) do not match the signature. The server also checks
1990 that <code>sender_public_key</code> is registered to the <code>sender</code>
1991 handle in <code>MusehubAuthKey</code> before accepting.
1992 </div>
1993 </div>
1994 </section>
1995
1996 {# ── Phase nav ────────────────────────────────────────────────────────── #}
1997 <nav class="devdocs-phase-nav" aria-label="Phase navigation">
1998 <a class="devdocs-phase-nav-btn devdocs-phase-nav-btn--prev" href="/muse/foundations">
1999 {{ icon("arrow-left", 14) }}
2000 Phase 01: Foundations
2001 </a>
2002 <a class="devdocs-phase-nav-btn devdocs-phase-nav-btn--next" href="/muse/domains">
2003 Phase 03: Domain Protocol
2004 {{ icon("arrow-right", 14) }}
2005 </a>
2006 </nav>
2007
2008 </article>
2009 </div>
2010 </div>
2011
2012 {% endblock %}
File History 17 commits
sha256:8a1389ae62d0a688d5e027763249bd8faedfc39ab00857d65da6620d1ab8a689 Merge 'feat/9a-4-f7-overseer-provenance' into 'dev' — propo… Human 4 days ago
sha256:bee12c5cbde2334f98421c6c209d768fa6b8004d6705c9ea798ce6c1651bc11f Merge 'infra/database-phase3-4-cleanup' into 'dev' — propos… Human 5 days ago
sha256:7c5915d3a65660061405c2cc04bcb297f7c97157c71629b856bd0261c4cc53ac docs: add v0.2.0-nightly.3 changelog Sonnet 5 68 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 68 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 71 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 74 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 76 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 89 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 93 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 93 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 95 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 95 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 95 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 95 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 107 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 109 days ago
sha256:57334c567d77a3ecd8acc5f7423dd470505cb7d3ef0f6d76f231f3c68db64d05 feat: add Vision, CRDT Primitives, Security Reference doc pages Sonnet 4.6 minor 110 days ago