musehub_identity_models.py
python
sha256:73f24973678ceefd56628ee17c6d0535d86e33533828af6c63348f50941515ad
Merge branch 'fix/smoke-test-tag-label' into dev
Human
16 days ago
| 1 | """ORM models for identity, session auth, attestations, and micropayments. |
| 2 | |
| 3 | Tables: |
| 4 | - musehub_identities: Unified identity — human, agent, or org |
| 5 | - musehub_attestation_claim_types: Registry of valid attestation claim types |
| 6 | - musehub_attestations: Cryptographic attestations issued by one identity about another |
| 7 | - musehub_mpay_claims: nanoMUSE micropayment claims |
| 8 | - musehub_profile_snapshots: Pre-computed profile snapshots |
| 9 | """ |
| 10 | |
| 11 | from __future__ import annotations |
| 12 | |
| 13 | from datetime import datetime, timezone |
| 14 | |
| 15 | import sqlalchemy as sa |
| 16 | from sqlalchemy import ARRAY, Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint |
| 17 | from sqlalchemy.orm import Mapped, MappedAsDataclass, mapped_column |
| 18 | from sqlalchemy.dialects.postgresql import JSONB |
| 19 | |
| 20 | from musehub.db.database import Base |
| 21 | from musehub.types.json_types import JSONValue # noqa: F401 — needed for ForwardRef resolution in Mapped[] |
| 22 | |
| 23 | |
| 24 | def _utc_now() -> datetime: |
| 25 | return datetime.now(tz=timezone.utc) |
| 26 | |
| 27 | |
| 28 | class MusehubIdentity(MappedAsDataclass, Base): |
| 29 | """Unified identity — human, agent, or org. |
| 30 | |
| 31 | Replaces the split between ``musehub_profiles`` (humans only) and ad-hoc |
| 32 | agent references. All three identity types share one table so that |
| 33 | ``/{handle}`` resolves identically and the profile page renders adaptively. |
| 34 | |
| 35 | ``identity_type`` values: |
| 36 | ``"human"`` — a person authenticated via MSign key pair / personal access token |
| 37 | ``"agent"`` — an autonomous process (LLM, pipeline, CI bot) |
| 38 | ``"org"`` — an organisation; membership managed via a join table |
| 39 | |
| 40 | Human identities carry an ``email``; agent identities carry ``agent_model`` |
| 41 | and ``agent_capabilities`` so clients can inspect before attempting ops. |
| 42 | """ |
| 43 | |
| 44 | __tablename__ = "musehub_identities" |
| 45 | __table_args__ = ( |
| 46 | UniqueConstraint("handle", name="uq_musehub_identities_handle"), |
| 47 | sa.CheckConstraint("identity_type IN ('human', 'agent', 'org')", name="ck_musehub_identities_identity_type"), |
| 48 | # Phase 7: author_type JOIN filter — WHERE handle IN (?) AND identity_type = 'agent' |
| 49 | Index("ix_musehub_identities_handle_type", "handle", "identity_type"), |
| 50 | ) |
| 51 | |
| 52 | # --- Required fields --- |
| 53 | identity_id: Mapped[str] = mapped_column(String(128), primary_key=True) |
| 54 | # URL-safe handle — forms /{handle} and /{handle}/{repo} paths |
| 55 | handle: Mapped[str] = mapped_column(String(64), nullable=False, index=True) |
| 56 | |
| 57 | # --- Optional fields with Python-side defaults --- |
| 58 | # "human" | "agent" | "org" |
| 59 | identity_type: Mapped[str] = mapped_column(String(16), nullable=False, default="human", server_default="human") |
| 60 | display_name: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) |
| 61 | bio: Mapped[str | None] = mapped_column(Text, nullable=True, default=None) |
| 62 | avatar_url: Mapped[str | None] = mapped_column(String(2048), nullable=True, default=None) |
| 63 | website_url: Mapped[str | None] = mapped_column(String(2048), nullable=True, default=None) |
| 64 | # Humans only |
| 65 | email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True, default=None) |
| 66 | # Agents only |
| 67 | agent_model: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) |
| 68 | # JSON list of capability strings, e.g. ["push", "pull", "create-proposal"] |
| 69 | agent_capabilities: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False, default_factory=list) |
| 70 | location: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) |
| 71 | social_url: Mapped[str | None] = mapped_column(String(2048), nullable=True, default=None) |
| 72 | is_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false(), index=True) |
| 73 | # MuseHub-level privilege flag — separate from identity_type. Set by operator action, never by self-registration. |
| 74 | is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false()) |
| 75 | cc_license: Mapped[str | None] = mapped_column(String(50), nullable=True, default=None) |
| 76 | pinned_repo_ids: Mapped[list[str]] = mapped_column(ARRAY(String(128)), nullable=False, default_factory=list) |
| 77 | created_at: Mapped[datetime] = mapped_column( |
| 78 | DateTime(timezone=True), nullable=False, default_factory=_utc_now |
| 79 | ) |
| 80 | updated_at: Mapped[datetime] = mapped_column( |
| 81 | DateTime(timezone=True), nullable=False, default_factory=_utc_now, onupdate=_utc_now |
| 82 | ) |
| 83 | deleted_at: Mapped[datetime | None] = mapped_column( |
| 84 | DateTime(timezone=True), nullable=True, default=None |
| 85 | ) |
| 86 | # Agent delegation fields — null for human/org identities |
| 87 | spawned_by: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True, default=None) |
| 88 | scope: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True, default=None) |
| 89 | expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True, default=None) |
| 90 | # Compliance: ToS acceptance recorded at first key registration. |
| 91 | tos_accepted_at: Mapped[datetime | None] = mapped_column( |
| 92 | DateTime(timezone=True), nullable=True, default=None |
| 93 | ) |
| 94 | tos_version: Mapped[str | None] = mapped_column(String(16), nullable=True, default=None) |
| 95 | # AVAX C-Chain address derived from Ed25519 pubkey via SLIP-0010/BIP32 |
| 96 | avax_address: Mapped[str | None] = mapped_column(String(64), nullable=True, default=None) |
| 97 | # Org-only fields — null for human/agent identities |
| 98 | org_members: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True, default=None) |
| 99 | org_quorum: Mapped[int | None] = mapped_column(Integer, nullable=True, default=None) |
| 100 | org_treasury_address: Mapped[str | None] = mapped_column(String(64), nullable=True, default=None) |
| 101 | |
| 102 | |
| 103 | class MusehubAttestationClaimType(Base): |
| 104 | """Registry of valid attestation claim types. |
| 105 | |
| 106 | Claim types are addable at runtime without a deploy. ``valid_scopes`` |
| 107 | constrains which scopes (identity, repo, commit) a given type may be used |
| 108 | with. ``deprecated_at`` soft-retires a type without removing historical |
| 109 | attestations that used it. |
| 110 | |
| 111 | Seeded via migration 0043; extended at runtime via the hub API or CLI. |
| 112 | """ |
| 113 | |
| 114 | __tablename__ = "musehub_attestation_claim_types" |
| 115 | |
| 116 | type_key: Mapped[str] = mapped_column(String(128), primary_key=True) |
| 117 | category: Mapped[str] = mapped_column(String(64), nullable=False) |
| 118 | label: Mapped[str] = mapped_column(String(128), nullable=False) |
| 119 | description: Mapped[str] = mapped_column(Text, nullable=False) |
| 120 | # PostgreSQL TEXT[] — list of valid scope strings |
| 121 | valid_scopes: Mapped[list[str]] = mapped_column(ARRAY(Text), nullable=False) |
| 122 | introduced_at: Mapped[datetime] = mapped_column( |
| 123 | DateTime(timezone=True), nullable=False, default=_utc_now |
| 124 | ) |
| 125 | deprecated_at: Mapped[datetime | None] = mapped_column( |
| 126 | DateTime(timezone=True), nullable=True, default=None |
| 127 | ) |
| 128 | # Optional JSON Schema for the freeform ``metadata`` field in the claim payload |
| 129 | metadata_schema: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None) |
| 130 | |
| 131 | |
| 132 | class MusehubAttestation(Base): |
| 133 | """A cryptographic attestation issued by one identity about another. |
| 134 | |
| 135 | The attester signs the canonical ATTEST message with their Ed25519 private |
| 136 | key. The resulting signature is stored so any client can verify the claim |
| 137 | without trusting the server. |
| 138 | |
| 139 | Canonical message for identity scope (UTF-8, newline separated): |
| 140 | ATTEST\\n{attester}\\n{subject}\\n{claim}\\n{issued_at_iso} |
| 141 | |
| 142 | For repo/commit scope, scope_ref is appended as a sixth line: |
| 143 | ATTEST\\n{attester}\\n{subject}\\n{claim}\\n{issued_at_iso}\\n{scope_ref} |
| 144 | |
| 145 | ``scope`` is one of: ``identity`` | ``repo`` | ``commit``. |
| 146 | ``scope_ref`` encodes the full scoped target: |
| 147 | identity → None |
| 148 | repo → ``{handle}/{repo_slug}`` |
| 149 | commit → ``{handle}/{repo_slug}@{sha256:commit_id}`` |
| 150 | |
| 151 | ``claim`` is a JSON string whose top-level ``type`` key must be a registered |
| 152 | claim type. The type must be valid for the given scope. |
| 153 | |
| 154 | ``revoked_at`` is set (never deleted) when the attester retracts a claim. |
| 155 | ``expires_at`` is optional; expired attestations are excluded from live |
| 156 | queries but retained for audit purposes. |
| 157 | """ |
| 158 | |
| 159 | __tablename__ = "musehub_attestations" |
| 160 | __table_args__ = ( |
| 161 | Index("ix_musehub_attestations_subject", "subject"), |
| 162 | Index("ix_musehub_attestations_attester", "attester"), |
| 163 | Index("ix_musehub_attestations_commit_id", "commit_id"), |
| 164 | Index("ix_musehub_attestations_scope", "scope"), |
| 165 | ) |
| 166 | |
| 167 | attestation_id: Mapped[str] = mapped_column(String(128), primary_key=True) |
| 168 | attester: Mapped[str] = mapped_column(String(64), nullable=False) |
| 169 | # For identity scope: the attested handle. |
| 170 | # For repo/commit scope: the repo slug (handle/repo) for efficient joins. |
| 171 | subject: Mapped[str] = mapped_column(String(128), nullable=False) |
| 172 | # Free-form JSON; top-level "type" must be in musehub_attestation_claim_types |
| 173 | claim: Mapped[str] = mapped_column(Text, nullable=False) |
| 174 | # Ed25519 signature over canonical ATTEST message; "ed25519:<base64url>" |
| 175 | signature: Mapped[str] = mapped_column(String(128), nullable=False) |
| 176 | # Attester's Ed25519 public key at time of issuance; "ed25519:<base64url>" |
| 177 | attester_public_key: Mapped[str] = mapped_column(String(128), nullable=False) |
| 178 | issued_at: Mapped[datetime] = mapped_column( |
| 179 | DateTime(timezone=True), nullable=False, default=_utc_now |
| 180 | ) |
| 181 | # Null until the attester revokes the claim |
| 182 | revoked_at: Mapped[datetime | None] = mapped_column( |
| 183 | DateTime(timezone=True), nullable=True, default=None |
| 184 | ) |
| 185 | # Scope fields — non-null default "identity" preserves backward compatibility |
| 186 | scope: Mapped[str] = mapped_column(String(32), nullable=False, default="identity") |
| 187 | scope_ref: Mapped[str | None] = mapped_column(Text, nullable=True, default=None) |
| 188 | repo_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None) |
| 189 | commit_id: Mapped[str | None] = mapped_column(String(128), nullable=True, default=None) |
| 190 | expires_at: Mapped[datetime | None] = mapped_column( |
| 191 | DateTime(timezone=True), nullable=True, default=None |
| 192 | ) |
| 193 | |
| 194 | |
| 195 | class MusehubMPayClaim(Base): |
| 196 | """A single nanoMUSE micropayment claim, Ed25519-signed by the sender. |
| 197 | |
| 198 | The sender signs the canonical MPay message via ``muse sign payment``. |
| 199 | This record carries the full claim so recipients and auditors can verify |
| 200 | the payment independently of the server. |
| 201 | |
| 202 | ``amount_nano`` is denominated in nanoMUSE (1 MUSE = 1_000_000_000 nanoMUSE). |
| 203 | ``nonce_hex`` is the 32-byte random hex embedded in the MSign payment payload. |
| 204 | |
| 205 | ``confirmed_at`` is set by the server when the on-chain transaction is |
| 206 | confirmed (for L1-backed claims); null for off-chain credit claims. |
| 207 | ``voided_at`` is set if the server detects a double-spend or invalid sig. |
| 208 | """ |
| 209 | |
| 210 | __tablename__ = "musehub_mpay_claims" |
| 211 | __table_args__ = ( |
| 212 | Index("ix_musehub_mpay_claims_sender", "sender"), |
| 213 | Index("ix_musehub_mpay_claims_recipient", "recipient"), |
| 214 | UniqueConstraint("nonce_hex", name="uq_musehub_mpay_claims_nonce"), |
| 215 | ) |
| 216 | |
| 217 | claim_id: Mapped[str] = mapped_column(String(128), primary_key=True) |
| 218 | sender: Mapped[str] = mapped_column(String(64), nullable=False) |
| 219 | recipient: Mapped[str] = mapped_column(String(64), nullable=False) |
| 220 | amount_nano: Mapped[int] = mapped_column(Integer, nullable=False) |
| 221 | nonce_hex: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) |
| 222 | # "ed25519:<base64url>" — signature over canonical MPay message |
| 223 | signature: Mapped[str] = mapped_column(String(128), nullable=False) |
| 224 | # Sender's public key at time of signing; "ed25519:<base64url>" |
| 225 | sender_public_key: Mapped[str] = mapped_column(String(128), nullable=False) |
| 226 | # Optional memo attached to the payment |
| 227 | memo: Mapped[str | None] = mapped_column(String(500), nullable=True) |
| 228 | created_at: Mapped[datetime] = mapped_column( |
| 229 | DateTime(timezone=True), nullable=False, default=_utc_now |
| 230 | ) |
| 231 | confirmed_at: Mapped[datetime | None] = mapped_column( |
| 232 | DateTime(timezone=True), nullable=True, default=None |
| 233 | ) |
| 234 | voided_at: Mapped[datetime | None] = mapped_column( |
| 235 | DateTime(timezone=True), nullable=True, default=None |
| 236 | ) |
| 237 | |
| 238 | |
| 239 | class MusehubProfileSnapshot(Base): |
| 240 | """Pre-computed profile snapshot — one row per handle. |
| 241 | |
| 242 | Computed asynchronously by the background worker (job_type "profile.snapshot") |
| 243 | after any push to a repo owned by that handle. The SSR profile route reads |
| 244 | from this table first; if the row is missing or ``is_stale`` the route falls |
| 245 | back to live computation and enqueues a refresh job. |
| 246 | |
| 247 | ``data_json`` is a JSON object with all the expensive, cross-repo aggregates |
| 248 | that would otherwise be computed on every page request: |
| 249 | stats — {repo_count, commit_count, agent_count, avg_health} |
| 250 | repos — list of repo card dicts |
| 251 | heatmap — 52-week heatmap with streaks |
| 252 | agent_fleet — list of agent summary dicts |
| 253 | badges — list of provenance badge dicts |
| 254 | footprint — list of top-touched symbol dicts |
| 255 | activity_canvas — list of domain canvas dicts |
| 256 | """ |
| 257 | |
| 258 | __tablename__ = "musehub_profile_snapshots" |
| 259 | |
| 260 | handle: Mapped[str] = mapped_column(String(64), primary_key=True) |
| 261 | data_json: Mapped[str] = mapped_column(sa.Text, nullable=False, default="{}") |
| 262 | computed_at: Mapped[datetime] = mapped_column( |
| 263 | DateTime(timezone=True), nullable=False, default=_utc_now |
| 264 | ) |
| 265 | is_stale: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=sa.false()) |
File History
12 commits
sha256:cfefc25a166c3c3eed8ea3529aee19ea350bc05f2954d007420e924133b7d8ce
chore: pivot to nightly channel — bump version to 0.2.0.dev…
Sonnet 5
patch
14 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2
chore: bump version to 0.2.0rc15 for musehub#113 fix release
Sonnet 4.6
patch
16 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352
Merge branch 'task/version-tags-phase3-server' into dev
Human
18 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53
merge: rescue snapshot-recovery hardening (c00aa21d) into d…
Opus 4.8
minor
⚠
31 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a
fix: remove false-positive proposal_comments index drop fro…
Sonnet 4.6
patch
35 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3
feat: render markdown mists as HTML with heading anchor links
Sonnet 4.6
patch
36 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6
Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump …
Human
37 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee
Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump …
Human
37 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c
Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo…
Human
37 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd
Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop…
Human
37 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f
fix: use wire_bytes not mpack_bytes_raw in compute_object_b…
Sonnet 4.6
patch
49 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583
rename: delta_add → delta_upsert across wire format, models…
Sonnet 4.6
patch
51 days ago