mists.py
python
sha256:400438cf8bc700a611f1ba798aa9def68290f487dc19f7dbf317985ad17050c9
chore: delete muse/prose domain — hallucinated, never existed
Sonnet 4.6
minor
⚠ breaking
34 days ago
| 1 | """Pydantic wire models for the Mists API. |
| 2 | |
| 3 | All models inherit from :class:`~musehub.models.base.CamelModel` so they |
| 4 | serialize to ``camelCase`` on the wire (``model_dump(by_alias=True)``) while |
| 5 | Python code uses ``snake_case`` field names internally. |
| 6 | |
| 7 | Shapes |
| 8 | ------ |
| 9 | :class:`MistResponse` |
| 10 | Full mist detail — returned by ``GET /api/mists/{id}`` and |
| 11 | ``POST /api/mists``. Includes the full artifact content. |
| 12 | |
| 13 | :class:`MistListEntry` |
| 14 | Condensed row — returned in the ``mists`` list of |
| 15 | :class:`MistListResponse`. Excludes content for bandwidth. |
| 16 | |
| 17 | :class:`MistListResponse` |
| 18 | Cursor-paginated list — returned by ``GET /api/{handle}/mists`` and |
| 19 | ``GET /mists/explore``. |
| 20 | |
| 21 | :class:`MistCreateRequest` |
| 22 | Validated POST body for ``POST /api/mists``. |
| 23 | |
| 24 | :class:`MistUpdateRequest` |
| 25 | Validated PATCH body for ``PATCH /api/mists/{id}``. |
| 26 | |
| 27 | :class:`MistForkResponse` |
| 28 | Condensed response for ``POST /api/mists/{id}/fork``. |
| 29 | |
| 30 | :class:`MistEmbedResponse` |
| 31 | Embed code triple — returned by ``GET /api/{owner}/mists/{id}/embed``. |
| 32 | """ |
| 33 | |
| 34 | from datetime import datetime |
| 35 | |
| 36 | from pydantic import Field, field_validator |
| 37 | |
| 38 | from musehub.models.base import CamelModel |
| 39 | |
| 40 | # --------------------------------------------------------------------------- |
| 41 | # Validation constants (mirrors muse/cli/commands/mist.py) |
| 42 | # --------------------------------------------------------------------------- |
| 43 | |
| 44 | _VALID_ARTIFACT_TYPES = frozenset( |
| 45 | {"code", "midi", "schema", "json_schema", "abi", "unknown"} |
| 46 | ) |
| 47 | _VALID_VISIBILITY = frozenset({"public", "secret"}) |
| 48 | _MAX_TAGS = 10 |
| 49 | _MAX_TAG_LENGTH = 64 |
| 50 | _MAX_CONTENT_BYTES = 10 * 1024 * 1024 # 10 MiB — enforced by ContentSizeLimitMiddleware, not Pydantic |
| 51 | |
| 52 | # --------------------------------------------------------------------------- |
| 53 | # Full detail response |
| 54 | # --------------------------------------------------------------------------- |
| 55 | |
| 56 | class MistResponse(CamelModel): |
| 57 | """Full mist detail — returned by GET /api/mists/{id} and POST /api/mists. |
| 58 | |
| 59 | Fields |
| 60 | ------ |
| 61 | mist_id : str |
| 62 | 12-character base-58 content address. Globally unique. |
| 63 | url : str |
| 64 | Canonical public URL: ``https://musehub.ai/{owner}/mists/{mist_id}``. |
| 65 | owner : str |
| 66 | MSign handle of the mist owner. |
| 67 | artifact_type : str |
| 68 | One of: ``code`` | ``midi`` | ``schema`` | ``json_schema`` | |
| 69 | ``abi`` | ``unknown``. |
| 70 | language : str |
| 71 | Detected programming language (empty string for non-code mists). |
| 72 | filename : str |
| 73 | Sanitised original filename. |
| 74 | title : str |
| 75 | Human-readable title; empty string when absent. |
| 76 | description : str |
| 77 | Markdown description; empty string when absent. |
| 78 | content : str |
| 79 | Raw artifact content as a UTF-8 string; binary mists use base64. |
| 80 | size_bytes : int |
| 81 | Byte length of the original artifact. |
| 82 | commit_id : str | None |
| 83 | Muse commit ID of the latest version; None before first push. |
| 84 | snapshot_id : str | None |
| 85 | Content-addressed object store ID; None before first push. |
| 86 | version : int |
| 87 | Number of commits on the mist's main branch. |
| 88 | signed : bool |
| 89 | True when ``gpg_signature`` is non-empty. |
| 90 | agent_id : str |
| 91 | MSign agent identifier; empty string for human-authored mists. |
| 92 | model_id : str |
| 93 | Model identifier; empty string for human-authored mists. |
| 94 | fork_parent_id : str | None |
| 95 | ``mist_id`` of the upstream mist; None for originals. |
| 96 | fork_depth : int |
| 97 | Fork chain depth (0 = original). |
| 98 | fork_count : int |
| 99 | view_count : int |
| 100 | embed_count : int |
| 101 | visibility : str |
| 102 | ``"public"`` or ``"secret"``. |
| 103 | tags : list[str] |
| 104 | Freeform tags attached at create time. |
| 105 | symbol_anchors : list[str] |
| 106 | Extracted symbol addresses for code mists (e.g. |
| 107 | ``"validate_assignee.py::_validate_assignee"``); empty for non-code. |
| 108 | created_at : datetime |
| 109 | updated_at : datetime |
| 110 | """ |
| 111 | |
| 112 | mist_id: str = Field(..., description="12-character base-58 content address.") |
| 113 | url: str = Field("", description="Canonical public URL for this mist.") |
| 114 | owner: str = Field(..., description="MSign handle of the mist owner.") |
| 115 | artifact_type: str = Field(..., description="code|midi|schema|json_schema|abi|unknown") |
| 116 | language: str = Field("", description="Detected programming language (empty for non-code).") |
| 117 | filename: str = Field(..., description="Sanitised original filename.") |
| 118 | title: str = Field("", description="Human-readable title.") |
| 119 | description: str = Field("", description="Markdown description.") |
| 120 | content: str = Field("", description="Artifact content (UTF-8 or base64 for binary).") |
| 121 | size_bytes: int = Field(0, description="Byte length of the original artifact.") |
| 122 | commit_id: str | None = Field(None, description="Muse commit ID of the latest version.") |
| 123 | snapshot_id: str | None = Field(None, description="Object store ID of the latest snapshot.") |
| 124 | version: int = Field(1, description="Number of commits on the mist's main branch.") |
| 125 | signed: bool = Field(False, description="True when gpg_signature is non-empty.") |
| 126 | agent_id: str = Field("", description="MSign agent identifier; empty for human-authored.") |
| 127 | model_id: str = Field("", description="Model identifier; empty for human-authored.") |
| 128 | fork_parent_id: str | None = Field(None, description="mist_id of the upstream mist.") |
| 129 | fork_depth: int = Field(0, description="Fork chain depth (0 = original).") |
| 130 | fork_count: int = Field(0) |
| 131 | view_count: int = Field(0) |
| 132 | embed_count: int = Field(0) |
| 133 | visibility: str = Field("public", description='"public" or "secret".') |
| 134 | tags: list[str] = Field(default_factory=list) |
| 135 | symbol_anchors: list[str] = Field( |
| 136 | default_factory=list, |
| 137 | description='Symbol addresses for code mists, e.g. "utils.py::compute".', |
| 138 | ) |
| 139 | created_at: datetime = Field(..., description="Creation timestamp (ISO-8601 UTC).") |
| 140 | updated_at: datetime = Field(..., description="Last update timestamp (ISO-8601 UTC).") |
| 141 | |
| 142 | # --------------------------------------------------------------------------- |
| 143 | # List entry (condensed — no content) |
| 144 | # --------------------------------------------------------------------------- |
| 145 | |
| 146 | class MistListEntry(CamelModel): |
| 147 | """Condensed mist row for list/explore pages. |
| 148 | |
| 149 | Excludes full content to keep list responses compact. Includes all |
| 150 | signals needed for the Mission Control display: type, language, provenance, |
| 151 | fork graph, and view counts. |
| 152 | |
| 153 | Fields |
| 154 | ------ |
| 155 | mist_id : str |
| 156 | url : str |
| 157 | Canonical public URL: ``https://musehub.ai/{owner}/mists/{mist_id}``. |
| 158 | owner : str |
| 159 | artifact_type : str |
| 160 | language : str |
| 161 | filename : str |
| 162 | title : str |
| 163 | size_bytes : int |
| 164 | version : int |
| 165 | signed : bool |
| 166 | agent_id : str |
| 167 | model_id : str |
| 168 | fork_parent_id : str | None |
| 169 | fork_depth : int |
| 170 | fork_count : int |
| 171 | view_count : int |
| 172 | visibility : str |
| 173 | tags : list[str] |
| 174 | primary_symbol : str | None |
| 175 | First symbol anchor; ``None`` for non-code mists. |
| 176 | created_at : datetime |
| 177 | updated_at : datetime |
| 178 | """ |
| 179 | |
| 180 | mist_id: str |
| 181 | url: str = Field("", description="Canonical public URL for this mist.") |
| 182 | owner: str |
| 183 | artifact_type: str |
| 184 | language: str = "" |
| 185 | filename: str |
| 186 | title: str = "" |
| 187 | size_bytes: int = 0 |
| 188 | version: int = 1 |
| 189 | signed: bool = False |
| 190 | agent_id: str = "" |
| 191 | model_id: str = "" |
| 192 | fork_parent_id: str | None = None |
| 193 | fork_depth: int = 0 |
| 194 | fork_count: int = 0 |
| 195 | view_count: int = 0 |
| 196 | visibility: str = "public" |
| 197 | tags: list[str] = Field(default_factory=list) |
| 198 | primary_symbol: str | None = Field( |
| 199 | None, description="First symbol anchor; None for non-code mists." |
| 200 | ) |
| 201 | created_at: datetime |
| 202 | updated_at: datetime |
| 203 | |
| 204 | # --------------------------------------------------------------------------- |
| 205 | # Paginated list response |
| 206 | # --------------------------------------------------------------------------- |
| 207 | |
| 208 | class MistListResponse(CamelModel): |
| 209 | """Cursor-paginated list of mists. |
| 210 | |
| 211 | Returned by ``GET /api/{handle}/mists`` and ``GET /mists/explore``. |
| 212 | |
| 213 | Fields |
| 214 | ------ |
| 215 | total : int |
| 216 | Total number of mists matching the query (before pagination). |
| 217 | next_cursor : str | None |
| 218 | Opaque cursor for the next page; ``None`` on the last page. |
| 219 | mists : list[MistListEntry] |
| 220 | Current page of condensed mist entries. |
| 221 | """ |
| 222 | |
| 223 | total: int = Field(0, description="Total mists matching the query.") |
| 224 | next_cursor: str | None = Field(None, description="Cursor for the next page; None on last page.") |
| 225 | mists: list[MistListEntry] = Field(default_factory=list) |
| 226 | |
| 227 | # --------------------------------------------------------------------------- |
| 228 | # Create request |
| 229 | # --------------------------------------------------------------------------- |
| 230 | |
| 231 | class MistCreateRequest(CamelModel): |
| 232 | """Validated POST body for ``POST /api/mists``. |
| 233 | |
| 234 | Fields |
| 235 | ------ |
| 236 | filename : str |
| 237 | Original filename. Must pass ``validate_mist_filename()`` — |
| 238 | no null bytes, no path separators, no traversal, ≤ 255 chars. |
| 239 | content : str |
| 240 | Artifact content as a UTF-8 string; send base64 for binary artifacts. |
| 241 | title : str |
| 242 | Optional human-readable title. |
| 243 | description : str |
| 244 | Optional Markdown description. |
| 245 | visibility : str |
| 246 | ``"public"`` (default) or ``"secret"`` (direct-URL only). |
| 247 | tags : list[str] |
| 248 | Up to 10 freeform tags; each ≤ 64 chars, no HTML specials. |
| 249 | agent_id : str |
| 250 | Optional MSign agent identifier for AI-authored mists. |
| 251 | model_id : str |
| 252 | Optional model identifier for AI provenance. |
| 253 | gpg_signature : str | None |
| 254 | Optional ASCII-armoured Ed25519 signature of the artifact bytes. |
| 255 | """ |
| 256 | |
| 257 | filename: str = Field(..., min_length=1, max_length=255) |
| 258 | content: str = Field(..., description="UTF-8 content or base64 for binary artifacts.") |
| 259 | title: str = Field("", max_length=500) |
| 260 | description: str = Field("", max_length=10_000) |
| 261 | visibility: str = Field("public") |
| 262 | tags: list[str] = Field(default_factory=list) |
| 263 | agent_id: str = Field("", max_length=255) |
| 264 | model_id: str = Field("", max_length=255) |
| 265 | gpg_signature: str | None = Field(None) |
| 266 | |
| 267 | @field_validator("visibility") |
| 268 | @classmethod |
| 269 | def validate_visibility(cls, v: str) -> str: |
| 270 | """Ensure visibility is one of the allowed values. |
| 271 | |
| 272 | Args: |
| 273 | v: The visibility string from the request body. |
| 274 | |
| 275 | Returns: |
| 276 | The validated visibility string. |
| 277 | |
| 278 | Raises: |
| 279 | ValueError: When the value is not ``"public"`` or ``"secret"``. |
| 280 | """ |
| 281 | if v not in _VALID_VISIBILITY: |
| 282 | raise ValueError(f"visibility must be 'public' or 'secret', got {v!r}") |
| 283 | return v |
| 284 | |
| 285 | @field_validator("tags") |
| 286 | @classmethod |
| 287 | def validate_tags(cls, tags: list[str]) -> list[str]: |
| 288 | """Validate each tag: count, length, and content. |
| 289 | |
| 290 | Args: |
| 291 | tags: The tag list from the request body. |
| 292 | |
| 293 | Returns: |
| 294 | The validated tag list. |
| 295 | |
| 296 | Raises: |
| 297 | ValueError: When there are more than 10 tags, any tag exceeds 64 |
| 298 | characters, or any tag contains a null byte or HTML special character. |
| 299 | """ |
| 300 | if len(tags) > _MAX_TAGS: |
| 301 | raise ValueError(f"Too many tags: max {_MAX_TAGS}, got {len(tags)}") |
| 302 | for tag in tags: |
| 303 | if len(tag) > _MAX_TAG_LENGTH: |
| 304 | raise ValueError(f"Tag exceeds {_MAX_TAG_LENGTH} chars: {tag!r}") |
| 305 | if "\x00" in tag: |
| 306 | raise ValueError(f"Tag contains null byte: {tag!r}") |
| 307 | for ch in ("<", ">", '"', "'", "&"): |
| 308 | if ch in tag: |
| 309 | raise ValueError(f"Tag contains HTML special character {ch!r}: {tag!r}") |
| 310 | return tags |
| 311 | |
| 312 | @field_validator("filename") |
| 313 | @classmethod |
| 314 | def validate_filename(cls, v: str) -> str: |
| 315 | """Validate the filename through the mist plugin security gate. |
| 316 | |
| 317 | Delegates to ``muse.plugins.mist.plugin.validate_mist_filename`` — |
| 318 | the same validation used by the CLI. |
| 319 | |
| 320 | Args: |
| 321 | v: The filename from the request body. |
| 322 | |
| 323 | Returns: |
| 324 | The validated filename. |
| 325 | |
| 326 | Raises: |
| 327 | ValueError: When the filename fails any security check. |
| 328 | """ |
| 329 | try: |
| 330 | from muse.plugins.mist.plugin import validate_mist_filename |
| 331 | |
| 332 | validate_mist_filename(v) |
| 333 | except ValueError as exc: |
| 334 | raise ValueError(str(exc)) from exc |
| 335 | return v |
| 336 | |
| 337 | # --------------------------------------------------------------------------- |
| 338 | # Update request |
| 339 | # --------------------------------------------------------------------------- |
| 340 | |
| 341 | class MistUpdateRequest(CamelModel): |
| 342 | """Validated PATCH body for ``PATCH /api/mists/{id}``. |
| 343 | |
| 344 | All fields are optional. Only provided fields are updated — ``None`` |
| 345 | means "leave unchanged" (not "set to None"). |
| 346 | |
| 347 | Fields |
| 348 | ------ |
| 349 | title : str | None |
| 350 | description : str | None |
| 351 | visibility : str | None |
| 352 | tags : list[str] | None |
| 353 | content : str | None |
| 354 | When provided, the artifact content is replaced and a new Muse commit |
| 355 | is created on the mist's repo. |
| 356 | """ |
| 357 | |
| 358 | title: str | None = Field(None, max_length=500) |
| 359 | description: str | None = Field(None, max_length=10_000) |
| 360 | visibility: str | None = Field(None) |
| 361 | tags: list[str] | None = Field(None) |
| 362 | filename: str | None = Field(None, max_length=255) |
| 363 | content: str | None = Field(None) |
| 364 | |
| 365 | @field_validator("visibility") |
| 366 | @classmethod |
| 367 | def validate_visibility(cls, v: str | None) -> str | None: |
| 368 | """Ensure visibility is one of the allowed values when provided. |
| 369 | |
| 370 | Args: |
| 371 | v: The visibility string from the request body, or None to skip. |
| 372 | |
| 373 | Returns: |
| 374 | The validated visibility string, or None. |
| 375 | |
| 376 | Raises: |
| 377 | ValueError: When the value is not None, ``"public"``, or ``"secret"``. |
| 378 | """ |
| 379 | if v is not None and v not in _VALID_VISIBILITY: |
| 380 | raise ValueError(f"visibility must be 'public' or 'secret', got {v!r}") |
| 381 | return v |
| 382 | |
| 383 | # --------------------------------------------------------------------------- |
| 384 | # Fork response |
| 385 | # --------------------------------------------------------------------------- |
| 386 | |
| 387 | class MistForkResponse(CamelModel): |
| 388 | """Condensed response for ``POST /api/mists/{id}/fork``. |
| 389 | |
| 390 | Fields |
| 391 | ------ |
| 392 | mist_id : str |
| 393 | mist_id of the newly created fork. |
| 394 | url : str |
| 395 | Canonical URL for the fork. |
| 396 | owner : str |
| 397 | Handle of the fork owner (the caller). |
| 398 | fork_parent_id : str |
| 399 | mist_id of the original mist. |
| 400 | artifact_type : str |
| 401 | language : str |
| 402 | filename : str |
| 403 | created_at : datetime |
| 404 | """ |
| 405 | |
| 406 | mist_id: str |
| 407 | url: str = "" |
| 408 | owner: str |
| 409 | fork_parent_id: str |
| 410 | artifact_type: str |
| 411 | language: str = "" |
| 412 | filename: str |
| 413 | created_at: datetime |
| 414 | |
| 415 | # --------------------------------------------------------------------------- |
| 416 | # Embed response |
| 417 | # --------------------------------------------------------------------------- |
| 418 | |
| 419 | class MistEmbedResponse(CamelModel): |
| 420 | """Embed code triple returned by ``GET /api/{owner}/mists/{id}/embed``. |
| 421 | |
| 422 | Fields |
| 423 | ------ |
| 424 | mist_id : str |
| 425 | owner : str |
| 426 | iframe : str |
| 427 | HTML ``<iframe>`` tag pointing to the ``/embed`` endpoint. |
| 428 | js : str |
| 429 | JavaScript ``<script>`` snippet for dynamic embedding. |
| 430 | badge : str |
| 431 | Markdown badge string (``[](/owner/mists/id)``). |
| 432 | """ |
| 433 | |
| 434 | mist_id: str |
| 435 | owner: str |
| 436 | iframe: str |
| 437 | js: str |
| 438 | badge: str |
File History
3 commits
sha256:400438cf8bc700a611f1ba798aa9def68290f487dc19f7dbf317985ad17050c9
chore: delete muse/prose domain — hallucinated, never existed
Sonnet 4.6
minor
⚠
34 days ago
sha256:3707eba7ad42cadedf18c8b9c534d839b88cfd1c30924c3c5a3edc74e1d809de
feat: add url field to mist, issue, and proposal list/read …
Sonnet 4.6
minor
⚠
34 days ago
sha256:aaf8bf405c0538f98feeff2a703d5e17e7e17565b56d73c5e711d57766086e78
confirm updating primary file in mist is surfaced in GUI.
Human
minor
⚠
40 days ago