MeshClient — swarph_mesh.mesh_client¶
Async HTTP wrapper over the mesh-gateway. Pairs with MeshMessage / MeshPeer wire types.
Tier 1 stability. send / fetch signatures and MeshMessage wire shape are part of the contract.
MeshClient — async wrapper around the mesh-gateway HTTP API per
PLAN.md §5.
Replaces the hand-rolled curl-based code in:
lab-orchestrator/scripts/lab_loop_drain.pyhedge-fund-mcp/scripts/mesh_inbox_watcher.py(droplet-side)hedge-fund-mcp/scripts/science_claude_inbox_drain.pyassorted ad-hoc curl invocations in CLAUDE.md / session ritual
with a single typed surface so cross-Claude DM coordination doesn’t require re-discovering the gateway shape from scratch every time.
Public surface:
client = MeshClient(node=”lab-ovh”, token=os.environ[“MESH_GATEWAY_TOKEN”]) peers = await client.list_peers() msgs = await client.fetch(unread_only=True) sent = await client.send(to=”droplet”, kind=”fyi”, content=”…”) await client.mark_read(msg_id=123) await client.register(url=”http://lab-ovh:8787”, capabilities={…})
Two structural invariants enforced:
Recipient name validation. Every
send()callsswarph_shared.validate_node_nameon the recipient — closes the Vector A (peer-onboarding chatter) + Vector B (human-prompt shorthand) framing-contagion classes documented inproject_peer_name_canonical.md.Mesh-secrets out-of-band guard. Every
send()runs a best-effort regex sniff on content for obvious credentials (pypi-...,sk-...,gh[ops]_...,eyJ...JWTs). Hits raiseMeshSecretLeakErrorBEFORE the POST. CLAUDE.md “Mesh secrets out-of-band only — NEVER ride /messages content fields” is the non-negotiable rule. Best-effort because regex misses are common — operators must still treat content fields as public. The guard catches the obvious cases.
- exception swarph_mesh.mesh_client.MeshGatewayError[source]¶
Bases:
SwarphMeshErrorGateway returned non-2xx or otherwise non-conformant response.
- exception swarph_mesh.mesh_client.MeshAuthError[source]¶
Bases:
MeshGatewayErrorAuthentication failed — token missing or rejected (401/403).
- exception swarph_mesh.mesh_client.MeshSecretLeakError[source]¶
Bases:
SwarphMeshErrorBest-effort guard caught an apparent credential in DM content.
Refuses to send. Operators must rotate the credential and resend without it.
- class swarph_mesh.mesh_client.MeshClient(node, *, token=None, gateway_url=None, timeout_seconds=10.0, validate_self_name=True)[source]¶
Bases:
objectAsync client for the mesh-gateway HTTP API.
Construct once per peer (or per cycle), reuse for the lifetime of the work. Backed by
httpx.AsyncClientso connection-pooling happens for free across manysend()calls.Use as an async context manager when you want explicit close:
- async with MeshClient(node=”lab-ovh”) as client:
await client.send(to=”droplet”, kind=”fyi”, content=”…”)
Or instantiate normally + call
aclose()when done.- Parameters:
- async register(*, url=None, capabilities=None)[source]¶
POST /peers/register— register this peer with the gateway.
- async fetch(*, to_node=None, unread_only=False, limit=None)[source]¶
GET /messages— fetch own inbox.Defaults
to_nodetoself.node. Pass an explicit value only if you have a legitimate reason to peek at another peer’s inbox (gateway access-controls this; most callers will get their own inbox only).v0.5.1 latent bug fix (discovered in Phase 5.6 daemon build): the mesh-gateway query parameter is
?to=— NOT?to_node=. The latter is silently ignored, returning ALL recent messages regardless of recipient. Pre-fix everyMeshClient.fetchcaller saw outbound bleed-through unless they Python-side filtered. The kwarg keeps its descriptive name for the caller-facing API; only the wire-shape changes.- Parameters:
- Return type:
- async send(*, to, kind, content, related_task_id=None, thread_id=None, skip_secret_check=False)[source]¶
POST /messages— send a DM.Validates
toviaswarph_shared.validate_node_name(closes Vector A + B contagion classes); raisesValueError/NotInRegistryon invalid recipient names BEFORE the POST.kindis aLiteralenum (seeDM_KINDabove) — type checkers reject unknown values at lint time, and the runtime guard below catches anyone passingkindasstr-typed from a callsite that bypassed type-checking.Runs the best-effort credential leak detector on
content. Setskip_secret_check=Trueonly when content has been inspected by the caller and the leak detector is producing a false positive (e.g. discussing a credential format in prose). False positives should be rare; treat them as a signal that the content is on a fence and rotate aggressively if you’re not 100% sure.
- async mark_read(msg_id)[source]¶
POST /messages/{msg_id}/read— flipread_aton a DM.Per CLAUDE.md droplet-side mesh DM drain rule, the cron-side watcher does NOT mark read; only the commander or the peer-in-session does. Adapter callers consuming inboxes should mark-read explicitly when they’ve actually processed the DM, not on fetch.
- Parameters:
msg_id (int)
- Return type:
None
Wire types — swarph_mesh.mesh_types¶
Typed dataclasses for mesh-gateway responses.
Shape derived from the live gateway at http://lab-ovh:8788 (2026-05-08). Stable enough to type against; Phase 3+ migrations on the gateway side will add fields rather than rename existing ones (per the swarph conventions captured in CLAUDE.md hedge-fund-mcp).
- class swarph_mesh.mesh_types.MeshPeer(*, name, url=None, capabilities=<factory>, enabled=True, last_health=None, last_seen=None, registered_at=None, **extra_data)[source]¶
Bases:
BaseModelOne row from
GET /peers.- Parameters:
- model_config = {'extra': 'allow'}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class swarph_mesh.mesh_types.MeshMessage(*, id, from_node, to_node, kind, content=None, created_at, read_at=None, related_task_id=None, thread_id=None, **extra_data)[source]¶
Bases:
BaseModelOne row from
GET /messages(or the success response ofPOST /messages).v0.5.1 fix (drop DM #722 + #728):
contentis nowOptional[str] = None. The gateway’sPOST /messagessuccess response returns{id, from_node, to_node, kind, thread_id, created_at}— no content field. Pre-fix,MeshClient.sendraisedpydantic.ValidationErroron every successful POST despite the message landing cleanly, trapping callers wrappingsend()in try/except into thinking the send failed and retrying — a DM duplication risk. The content-required model was correct forGET /messagesrows (where content always present) but wrong for POST responses; making it optional fits both shapes.- Parameters:
- model_config = {'extra': 'allow'}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].