gabriel / musehub public
Closed #28 Feature
filed by gabriel human · 43 days ago

feat: attestation system — design, claim taxonomy, CLI, and seven-tier TDD

0 Anchors
Blast radius
Churn 30d
0 Proposals

Background

Attestations are the trust layer of the Muse ecosystem. An attestation is a cryptographically-signed statement by one identity about another identity — non-forgeable, tamper-evident, and anchored to the same Ed25519 keypair that signs commits. Because every identity's key traces back to a shared HD wallet root, an attestation is as strong as the signing identity itself.

This makes Muse attestations fundamentally different from social endorsements. They are closer to W3C Verifiable Credentials, but native to the ecosystem: the same key that signs `muse commit --sign` signs `muse hub attestation create`.

Canonical message (domain-separated, replay-resistant): ``` ATTEST {attester} {subject} {claim_json} {issued_at_iso} ```

The `ATTEST` prefix domain-separates this from MSign auth headers and MPay payment messages, preventing cross-protocol replay attacks.


Design Questions (resolve before implementation)

1. Open vs. closed claim type registry

Option A — Open schema: any valid JSON object is a claim; `claim_type` is the top-level key. Maximum composability. No registry needed. Hard to enumerate, hard to render in UI without knowing the schema.

Option B — Closed registry: claim types must be declared in a registry (DB table or code constant). Unknown types rejected at write time. Enables indexed search, typed UI rendering, and forward-compatible schema evolution.

Option C — Hybrid (recommended): a `type` field from a well-known enum is required; an optional `metadata` object is freeform. The registry defines the semantics; metadata carries domain-specific payload.

2. Scope: identity-only or referenceable?

Current model: attestation is always `attester → subject (handle)`.

Future option: attestation can reference a specific commit, repo, or symbol: `attester → subject::repo_id::commit_id`. This enables per-artifact claims like `stems:verified` on a specific commit, not just on a handle.

3. Expiry / TTL

Current model: permanent until explicitly revoked by attester.

Option: optional `expires_at` field. Useful for time-bounded trust grants (e.g. a contractor relationship that lapses). Default: no expiry (current behavior preserved).

4. Transitivity

If Gabriel attests agentx as `trusted`, and agentx attests agent-y as `spawned-by`, does that create a verifiable trust chain? The current `TrustChainEntry` model suggests yes, but the chain is implicit.

Design question: should transitivity be declared (attester opts in to chain propagation) or computed (hub derives trust graphs from attestation DAG)?

5. On-chain anchoring (ERC-8004 trajectory)

Long-term: attestation hashes anchored to Avalanche L1 via the ERC-8004 agent identity contract. The attestation_id (SHA-256 of canonical message) is the natural on-chain anchor. Out of scope for this issue — flagged for Phase 3.


Claim Type Taxonomy (proposed)

Category Type Semantics
Identity `human` Attester vouches subject is a verified human
Identity `org` Attester vouches subject is a legitimate org
Identity `agent` Attester vouches subject is a trustworthy agent
Provenance `spawned-by` Subject agent was spawned by attester
Provenance `delegate` Attester delegated authority to subject
Collaboration `collab` Attester and subject collaborated
Collaboration `co-author` Subject co-authored something with attester
Collaboration `contractor` Subject performed contracted work for attester
Trust `trusted` Attester generally trusts subject
Code `code:reviewed` Attester reviewed subject's code
Code `code:approved` Attester approved a specific delivery
Code `deploy:approved` Attester approved a deployment
Music `stems:verified` Attester verified authenticity of subject's stems
Music `mix:approved` Attester approved a mix by subject
Music `midi:generated` Attester confirms subject generated the MIDI
Music `master:approved` Attester approved a master by subject
Skill `skill:verified` Attester verified a declared skill of subject

Registry is additive: new types can be declared without migration.


Implementation Plan

Phase 1 — Claim type registry + schema hardening

  • Add `musehub_attestation_claim_types` table (or in-code registry constant): `type_key`, `category`, `description`, `introduced_at`
  • Add DB CHECK constraint on `musehub_attestations.claim` validating `type` key is in the registry (enforced at application layer; soft constraint at DB)
  • Add optional `expires_at` column with NULL default (non-breaking)
  • Add optional `scope` column (`identity` | `repo` | `commit`) with default `identity` (non-breaking)
  • Alembic migration; backwards-compatible

Phase 2 — `muse hub attestation` CLI commands

```bash muse hub attestation create \ --subject <handle> \ --type <claim_type> \ [--metadata '{"key":"val"}'] \ [--expires-in 90d] \ --json

muse hub attestation list [--subject <handle>] [--attester <handle>] \ [--type <claim_type>] [--json]

muse hub attestation revoke <attestation_id> --json ```

The `create` command:

  1. Resolves attester handle from `muse auth whoami`
  2. Fetches attester public key from `~/.muse/identity.toml`
  3. Constructs canonical message
  4. Signs with Ed25519 private key
  5. POSTs `AttestationRequest` to `POST /api/profiles/{subject}/attestations`
  6. Returns `AttestationResponse` as JSON

Phase 3 — Trust graph + transitivity (future)

  • `GET /api/trust-graph/{handle}` — DAG of attestations reachable from handle
  • Transitivity declarations: opt-in chain propagation
  • On-chain anchoring prep (ERC-8004)

Test Plan — Seven Tiers

All tests written RED first before implementation.

Tier 1 — Unit

  • `verify_attestation_signature`: valid sig passes; tampered claim fails; wrong key fails; cross-protocol message (MSign prefix) fails
  • `_canonical_message`: output is deterministic; field order is fixed; ATTEST prefix present; newline separators correct
  • `compute_attestation_id`: same inputs → same ID; different issued_at → different ID
  • Claim type registry: known type passes; unknown type raises `ValueError`; registry is additive (adding new type doesn't break existing)
  • `AttestationRequest` / `AttestationBadge` Pydantic validation: required fields, type coercion, datetime handling

Tier 2 — Integration

  • `issue_attestation`: valid sig → persisted; invalid sig → `ValueError`; duplicate → idempotent (returns existing, no second row)
  • `revoke_attestation`: attester can revoke own attestation; wrong revoker → 403; non-existent → 404; revoked row has `revoked_at` set
  • `get_attestations_for_subject`: returns only active (non-revoked) by default; `include_revoked=True` returns all; filters by claim type; paginates correctly
  • `get_attestations_by_attester`: same filter/pagination coverage
  • `issue_attestation` with `expires_at`: expired attestation excluded from `get_attestations_for_subject` live results; included with `include_expired=True`
  • Scope field: `identity`-scoped attestation stored and retrieved correctly
  • API route `POST /api/profiles/{handle}/attestations`: 201 on success; 400 on invalid sig; 409 on duplicate (idempotent); 404 unknown subject

Tier 3 — End-to-End

  • Full CLI round-trip: `muse hub attestation create` signs → posts → hub stores → `muse hub attestation list` retrieves; profile page renders the badge
  • Revoke round-trip: create → revoke → profile page no longer shows badge
  • Expires round-trip: create with `--expires-in 1s` → wait → list returns empty
  • Cross-identity: human attests agent; agent attests human (both directions work)
  • Unknown claim type: `muse hub attestation create --type nonsense` → CLI error before any HTTP call

Tier 4 — Stress

  • 1,000 concurrent `issue_attestation` calls for the same canonical payload → exactly 1 row in DB (idempotency under concurrent writes)
  • 10,000 attestations for a single subject → `get_attestations_for_subject` returns in < 200ms with index
  • 100 concurrent revocations of different attestations → all succeed without deadlock or row contention

Tier 5 — Data Integrity

  • Attestation row survives DB restart (durability)
  • `revoked_at` is set atomically with the revocation UPDATE; no partial-revoke state possible
  • `attestation_id` is unique; duplicate insert on same ID raises `IntegrityError` (not silently overwritten)
  • Deleting an identity (soft-delete) does not cascade-delete attestations — historical record is immutable
  • `claim` JSON is stored verbatim; no normalization that would change `claim_type` derivation

Tier 6 — Performance

  • `GET /api/profiles/{handle}` (profile page) renders attestation section in < 50ms with 500 attestations for subject (index coverage verified by EXPLAIN)
  • Bulk `get_attestations_for_subject` with pagination: page 1 of 1,000-row result in < 20ms
  • `issue_attestation` write path: p99 < 50ms under 50 concurrent writers
  • Index coverage: `(subject, revoked_at)` and `(attester, revoked_at)` confirmed in query plan

Tier 7 — Security

  • Signature replay: posting the same `AttestationRequest` twice is idempotent, not a double-write (content-addressed ID prevents it)
  • Cross-protocol replay: an MSign auth header cannot be submitted as an attestation signature (domain separation test: prepend `MUSE-SIGN-V1` instead of `ATTEST`)
  • Attester impersonation: posting a request with `attester=gabriel` but signing with a different key → 400 invalid signature
  • Revoker impersonation: `DELETE` with wrong authenticated identity → 403
  • Claim injection: claim JSON with `proto` or SQL meta-characters stored verbatim without execution (parameterized query coverage)
  • Key rotation: attestations issued under old key remain verifiable after key rotation (attester_public_key stored per attestation, not looked up live)
  • Expired attestation: `expires_at` in the past is excluded from default queries even if not explicitly revoked

Acceptance Criteria

  • All 7 test tiers pass GREEN before any code merges
  • `muse hub attestation create/list/revoke` commands work end-to-end on staging
  • Profile page renders received/given attestations with correct claim types
  • Claim type registry is code-level (Phase 1); DB-level registry is Phase 2
  • Strong docstrings on all public functions and CLI commands
  • Alembic migration for `expires_at` and `scope` columns
  • No git, no GitHub, no backwards-compat shims
Activity3
gabriel opened this issue 43 days ago
gabriel 43 days ago

Design Decisions — Locked

1. Scope: commit-level (maximally granular)

Attestations can target any referenceable object in the content-addressed graph:

Scope Subject format Example
`identity` `{handle}` `gabriel`
`repo` `{handle}/{repo}` `gabriel/musehub`
`commit` `{handle}/{repo}@{sha256:commit_id}` `gabriel/musehub@sha256:1fd794...`
`symbol` `{handle}/{repo}@{commit_id}::{file}::{symbol}` future — Phase 3

Commit-level scope means:

  • `stems:verified` on a specific commit, not just on an agent's identity
  • `deploy:approved` on the exact commit that was shipped
  • `code:reviewed` traceable to the commit_id that was reviewed, not "at some point"
  • The attestation becomes part of the content-addressed provenance graph — commit_id is immutable so the attestation can never silently drift to a different state

The `scope` column stores the full subject string. The `subject` column retains the handle for efficient profile-level queries. Both are indexed.

Data model addition: ```sql ALTER TABLE musehub_attestations ADD COLUMN scope TEXT NOT NULL DEFAULT 'identity' CHECK (scope IN ('identity','repo','commit','symbol')), ADD COLUMN scope_ref TEXT, -- the full subject string when scope != identity ADD COLUMN repo_id TEXT, -- FK-friendly, NULL for identity scope ADD COLUMN commit_id TEXT; -- sha256:... NULL for non-commit scopes ```

Profile page shows identity-scoped attestations only. Repo page and commit page will show repo/commit-scoped attestations in their own sections (future UI).


2. Claim type registry: DB table (runtime-extensible)

`musehub_attestation_claim_types` table — addable at runtime without deploy:

```sql CREATE TABLE musehub_attestation_claim_types ( type_key TEXT PRIMARY KEY, -- 'stems:verified', 'code:reviewed', etc. category TEXT NOT NULL, -- 'identity' | 'collab' | 'code' | 'music' | 'trust' | 'skill' label TEXT NOT NULL, -- human-readable: "Stems Verified" description TEXT NOT NULL, -- one sentence valid_scopes TEXT[] NOT NULL, -- ['identity','commit'] etc. introduced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deprecated_at TIMESTAMPTZ, -- soft-deprecate without deletion metadata_schema JSONB -- optional JSON Schema for metadata field ); ```

Benefits of DB registry over in-code constant:

  • New claim types added via `muse hub attestation type-add` without a deploy
  • `valid_scopes` enforced per type: `human` is identity-only; `stems:verified` is commit or identity; `deploy:approved` is commit-only
  • `metadata_schema` enables per-type validation of the freeform metadata field
  • `deprecated_at` allows graceful retirement without breaking existing attestations
  • UI can render type picker from a live API call rather than hardcoded list

Seed data (applied in migration):

type_key category valid_scopes
`human` identity [identity]
`org` identity [identity]
`agent` identity [identity]
`spawned-by` trust [identity]
`delegate` trust [identity]
`trusted` trust [identity]
`collab` collab [identity, repo, commit]
`co-author` collab [identity, repo, commit]
`contractor` collab [identity]
`code:reviewed` code [commit, repo]
`code:approved` code [commit, repo]
`deploy:approved` code [commit]
`stems:verified` music [identity, commit]
`mix:approved` music [identity, commit]
`midi:generated` music [identity, commit]
`master:approved` music [identity, commit]
`skill:verified` skill [identity]

Updated CLI surface

```bash

Attest an identity

muse hub attestation create --subject gabriel --type human --json

Attest a specific commit

muse hub attestation create \ --subject gabriel/musehub \ --at sha256:1fd79407... \ --type deploy:approved \ --metadata '{"environment":"staging"}' \ --json

Attest a repo

muse hub attestation create \ --subject gabriel/musehub \ --type code:reviewed \ --json

List all types in the registry

muse hub attestation types --json

Add a new type to the registry (admin or self-service?)

muse hub attestation type-add \ --key stems:verified \ --category music \ --label "Stems Verified" \ --description "Attester verified the authenticity of subject stems" \ --valid-scopes identity,commit \ --json ```


Impact on seven-tier test plan

Additional test cases unlocked by these decisions:

Unit: claim type registry lookup; `valid_scopes` enforcement per type; scope_ref parsing for `{handle}/{repo}@{commit_id}` format; deprecated type rejected at write time.

Integration: commit-scoped attestation stored with `commit_id`; queried by commit_id; profile query excludes non-identity-scoped; commit page query returns commit-scoped only; type-add persists new type; type with wrong scope for claim (e.g. `human` on a commit) → 400.

Security: `scope_ref` is validated against hub-known repo/commit IDs — cannot attest a commit that does not exist; scope and scope_ref cannot be mutated post-issuance (content-addressed ID would change, so any mutation is detectable).

Data integrity: `commit_id` in attestation references an immutable object — the commit can never be altered; attestation is permanently bound to exactly that state.

gabriel 43 days ago

Architecture Decision — Attestations are NOT a Muse domain

Locked.

Attestations are content-addressed objects in the hub's global registry, not a per-repo domain. Rationale:

  • Domains are for content with merge semantics (Harmony plugin required). Attestations are append-only with revocation — no merge conflicts, no Harmony.
  • Domains are repo-siloed. Attestations must be queryable cross-ecosystem by subject, attester, claim type, and commit_id. A domain cannot answer that without hub aggregation, which collapses back to the current model.
  • A commit-level attestation referencing sha256:1fd794... would require dual storage (repo object store + hub DB) — two sources of truth for one content-addressed object.

What they are instead: Attestations are objects in the hub's global object graph. Their SHA-256 attestation_id is the content address. The hub is itself a Muse repo — these rows in Postgres are effectively objects in that graph, globally queryable and cross-referenceable.

Bundle portability (the one thing a domain would have added) is addressed separately as a hub feature: attestation export by commit range, included in a bundle's metadata envelope. Tracked as a sub-task of Phase 3.

Implementation starts now. TDD. Phase 1 first.

gabriel 43 days ago

All acceptance criteria complete.

Phase 1 — MuseHub server-side (done)

  • ✅ Seven TDD tiers all GREEN
  • ✅ Claim type registry (17 types, DB-backed via migration 0043)
  • POST /api/profiles/{handle}/attestations — issue with Ed25519 sig verification
  • GET /api/profiles/{handle}/attestations — list with include_revoked support
  • DELETE /api/profiles/{handle}/attestations/{id} — revoke (attester-only)
  • ✅ Scope support: identity | repo | commit
  • ✅ Deterministic attestation_id (SHA-256 of canonical message) — idempotent
  • ✅ Profile page renders all 17 claim types grouped by category

Phase 2 — Muse CLI (done)

  • muse hub attestation create --subject <handle> --type <claim_type> [--scope ...] [--json]
  • muse hub attestation list --subject <handle> [--attester ...] [--type ...] [--json]
  • muse hub attestation revoke <id> --subject <handle> [--json]

All three commands sign the ATTEST canonical message with the local Ed25519 key from ~/.muse/identity.toml and POST to the hub for server-side verification.