gabriel / musehub public
mists.py python
438 lines 15.1 KB
Raw
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 (``[![Mist ...](...)](/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