gabriel / musehub public
musehub_identity_models.py python
265 lines 13.4 KB
Raw
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