gabriel / musehub public
coord.py python
172 lines 6.4 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
1 """Pydantic models for the MuseHub coordination bus API.
2
3 Push/pull/watch endpoints use these models for request validation and
4 response serialization. All models are strict about field types so that
5 malformed agent payloads are rejected at the boundary rather than
6 propagating corrupt data into the database.
7
8 Security
9 --------
10 - ``record_id`` is validated against a safe-ID pattern — prevents agents from
11 using path-traversal characters (``..``, ``/``, null bytes) as record IDs.
12 Accepts sha256 genesis IDs (``sha256:<64-hex>``, 71 chars) and opaque
13 alphanumeric run_id strings up to 128 chars.
14 - ``kind`` is constrained to the known coordination record types — prevents
15 arbitrary string injection into the ``kind`` column.
16 - ``run_id`` is capped at 255 characters and stripped of control characters
17 at the service layer before storage.
18 - ``payload`` is accepted as a free-form dict — content is agent-defined and
19 opaque to the server. The server stores it verbatim and returns it verbatim.
20 """
21
22 import re
23 from datetime import datetime
24 from typing import Literal
25
26 from pydantic import BaseModel, Field, field_validator
27 from musehub.types.pydantic_types import PydanticJson
28
29 # Coordination record kinds that the server accepts.
30 _VALID_KINDS = frozenset({
31 "reservation",
32 "intent",
33 "release",
34 "heartbeat",
35 "dependency",
36 "task",
37 "claim",
38 })
39
40 # Safe record ID pattern — accepts sha256 genesis IDs (sha256:<64-hex>)
41 # and opaque alphanumeric run_id strings. Rejects path traversal chars.
42 _SAFE_RECORD_ID_RE = re.compile(r"^[a-zA-Z0-9_\-:]{1,128}$")
43
44 def _validate_record_id(v: str) -> str:
45 if not _SAFE_RECORD_ID_RE.match(v):
46 raise ValueError(f"record_id must be alphanumeric/hyphens/underscores/colons (max 128 chars), got: {v!r}")
47 return v
48
49 # ── Shared record shape ────────────────────────────────────────────────────────
50
51 class CoordRecordIn(BaseModel):
52 """A single coordination record to be pushed to MuseHub.
53
54 Attributes:
55 kind: Record type — must be one of the known coordination kinds.
56 record_id: ID of the local coordination record — a sha256 genesis ID
57 (``sha256:<64-hex>``) or an opaque alphanumeric string up
58 to 128 chars.
59 run_id: Agent/pipeline identifier. Empty string is allowed
60 (for records that have no explicit agent identity).
61 payload: Full JSON body of the coordination record.
62 expires_at: Optional expiry timestamp. ISO-8601 string or ``None``.
63 """
64
65 kind: str = Field(..., description="Coordination record kind.")
66 record_id: str = Field(..., description="ID of the local coordination record.")
67 run_id: str = Field(default="", max_length=255)
68 payload: PydanticJson = Field(..., description="Full record payload.")
69 expires_at: datetime | None = Field(default=None)
70
71 @field_validator("kind")
72 @classmethod
73 def _validate_kind(cls, v: str) -> str:
74 if v not in _VALID_KINDS:
75 raise ValueError(
76 f"kind must be one of {sorted(_VALID_KINDS)}, got: {v!r}"
77 )
78 return v
79
80 @field_validator("record_id")
81 @classmethod
82 def _validate_record_id_field(cls, v: str) -> str:
83 return _validate_record_id(v)
84
85 class CoordRecordOut(BaseModel):
86 """A coordination record returned from MuseHub (pull or watch).
87
88 Attributes:
89 id: Server-assigned auto-increment cursor ID.
90 repo_id: Repository ID.
91 kind: Record type.
92 record_id: ID of the original local coordination record.
93 run_id: Agent/pipeline identifier.
94 payload: Full record payload.
95 created_at: Server-side insertion timestamp (UTC).
96 expires_at: Optional expiry timestamp (UTC).
97 """
98
99 id: int
100 repo_id: str
101 kind: str
102 record_id: str
103 run_id: str
104 payload: PydanticJson
105 created_at: datetime
106 expires_at: datetime | None = None
107
108 # ── Push ───────────────────────────────────────────────────────────────────────
109
110 class CoordPushRequest(BaseModel):
111 """Request body for ``POST /{owner}/{slug}/coord/push``.
112
113 Attributes:
114 records: List of coordination records to push. At most 500 per call.
115 """
116
117 records: list[CoordRecordIn] = Field(
118 ...,
119 min_length=1,
120 max_length=500,
121 description="Coordination records to push (1–500).",
122 )
123
124 class CoordPushResponse(BaseModel):
125 """Response for ``POST /{owner}/{slug}/coord/push``.
126
127 Attributes:
128 inserted: Count of records newly written to the database.
129 skipped: Count of records that already existed (idempotent re-push).
130 """
131
132 inserted: int
133 skipped: int
134
135 # ── Pull ───────────────────────────────────────────────────────────────────────
136
137 class CoordPollRequest(BaseModel):
138 """Request body for ``POST /{owner}/{slug}/coord/pull``.
139
140 Attributes:
141 since_id: Return only records with ``id > since_id``.
142 Pass ``0`` (default) to fetch all records.
143 kinds: Filter by record kind. Empty list = all kinds.
144 limit: Maximum number of records to return (1–1000, default 500).
145 """
146
147 since_id: int = Field(default=0, ge=0)
148 kinds: list[str] = Field(default_factory=list)
149 limit: int = Field(default=500, ge=1, le=1000)
150
151 @field_validator("kinds")
152 @classmethod
153 def _validate_kinds(cls, v: list[str]) -> list[str]:
154 for kind in v:
155 if kind not in _VALID_KINDS:
156 raise ValueError(
157 f"kind must be one of {sorted(_VALID_KINDS)}, got: {kind!r}"
158 )
159 return v
160
161 class CoordPullResponse(BaseModel):
162 """Response for ``POST /{owner}/{slug}/coord/pull``.
163
164 Attributes:
165 records: Coordination records in insertion order (oldest first).
166 cursor: The ``id`` of the last record in this response.
167 Pass this as ``since_id`` in the next pull to resume.
168 ``0`` if no records were returned.
169 """
170
171 records: list[CoordRecordOut]
172 cursor: int
File History 14 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 11 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago