coord.py
python
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