Skip to content

Commit 60c11a8

Browse files
authored
feat(mesh): GDI-2b — pluggable MeshTransport + in-process bus (agentplane live wire) (#31)
GDI-2 flagged 'swap for a bus later without changing this contract'. This delivers that swap, dependency-free: - MeshTransport protocol (poll/ack/reject/publish_finding) — the consume loop's transport contract; a filesystem mailbox, an in-process bus, or a networked broker all satisfy it and consume() is identical over any. - FilesystemMailbox: the GDI-2 behavior refactored behind the interface. - InMemoryBus: a thread-safe, dependency-free in-process bus and the agentplane live-wire seam — agentplane's MeshRush adapter calls publish_telemetry(); GDI drains it with the same consume() contract. A networked broker (NATS/Redis/Kafka) is a further transport behind this interface — no heavy dependency pinned (sovereign). - consume(transport, schema): fail-closed + idempotent core. - consume_once(input_dir, output_dir, ...): back-compat wrapper (serve.py + existing tests unchanged). +4 tests (bus valid->finding+ack, malformed->rejected, idempotent across identical events, bus==mailbox over the same contract). make test 49 green; make validate green.
1 parent 024a07f commit 60c11a8

2 files changed

Lines changed: 199 additions & 31 deletions

File tree

tools/mesh_consume.py

Lines changed: 147 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88
ingests them here, normalizes, and turns the governance signal (e.g. refused
99
slots) into an ops finding.
1010
11-
Transport is a filesystem mailbox (MESH_INPUT_DIR / MESH_OUTPUT_DIR) — a real,
12-
dependency-free minimal mesh; swap for a bus later without changing this contract.
11+
Transport is **pluggable** (``MeshTransport``, GDI-2b): the default
12+
``FilesystemMailbox`` (MESH_INPUT_DIR / MESH_OUTPUT_DIR) is a real, dependency-free
13+
minimal mesh; ``InMemoryBus`` is an in-process message bus and the **agentplane
14+
live-wire seam** — agentplane's MeshRush adapter calls ``publish_telemetry`` and GDI
15+
drains it with the *same* ``consume`` contract. A networked bus (NATS/Redis/Kafka)
16+
is a further transport behind this same interface — no heavy broker dependency is
17+
pinned into GDI (sovereign, dependency-light).
1318
1419
Fail-closed: an envelope that does not conform is rejected (recorded, no finding
1520
produced) — a malformed telemetry item can never silently become a finding.
@@ -26,7 +31,10 @@
2631
import os
2732
import re
2833
import sys
34+
import threading
35+
from collections import OrderedDict
2936
from pathlib import Path
37+
from typing import Protocol
3038

3139
ROOT = Path(__file__).resolve().parents[1]
3240
SCHEMA = ROOT / "open-ai4it-spec/contracts/schemas/event-envelope.schema.json"
@@ -104,57 +112,166 @@ def _finding_for(event: dict, ts_ms: int, utc: str) -> dict:
104112
return finding
105113

106114

107-
def consume_once(input_dir: Path, output_dir: Path, schema: dict, *, clock=_now_fields) -> dict:
108-
"""Consume every *.json envelope in ``input_dir``; write findings to ``output_dir``.
115+
def _dedup_key(raw: "bytes | str") -> str:
116+
"""Content id of a source event — the idempotency key for its finding."""
117+
data = raw if isinstance(raw, bytes) else raw.encode("utf-8")
118+
return hashlib.sha256(data).hexdigest()[:16]
119+
120+
121+
class MeshTransport(Protocol):
122+
"""The consume-loop's transport contract. A filesystem mailbox, an in-process
123+
bus, or a networked broker all satisfy it; ``consume`` is identical over any."""
124+
125+
def poll(self) -> "list[tuple[str, bytes]]":
126+
"""Return pending ``(message_id, raw_bytes)`` telemetry, oldest first."""
127+
128+
def ack(self, message_id: str) -> None:
129+
"""Mark a message consumed (it must not be polled again)."""
130+
131+
def reject(self, message_id: str, reason: str) -> None:
132+
"""Mark a message rejected (recorded, removed from the inbox)."""
133+
134+
def publish_finding(self, finding: dict, *, dedup_key: str) -> "tuple[str, bool]":
135+
"""Publish a finding idempotently by ``dedup_key``; return ``(id, created)``."""
109136

110-
Returns stats ``{consumed, produced, rejected}``. Rejected envelopes are recorded
111-
in ``output_dir/_rejected.log`` with their errors; no finding is produced for them.
112-
"""
113-
output_dir.mkdir(parents=True, exist_ok=True)
114-
processed_dir = output_dir / "_processed"
115-
rejected_dir = output_dir / "_rejected"
116-
stats = {"consumed": 0, "produced": 0, "rejected": 0}
117-
rejected_log: list[str] = []
118137

119-
def _drain(path: Path, dest_dir: Path) -> None:
120-
# Move the source out of the inbox so a long-running loop consumes it once.
138+
class FilesystemMailbox:
139+
"""A ``MeshTransport`` over a directory pair (the GDI-2 default). Telemetry is
140+
``input_dir/*.json``; findings and drained sources land under ``output_dir``."""
141+
142+
def __init__(self, input_dir: Path, output_dir: Path) -> None:
143+
self.input_dir = input_dir
144+
self.output_dir = output_dir
145+
self._pending: "dict[str, Path]" = {}
146+
147+
def poll(self) -> "list[tuple[str, bytes]]":
148+
out: "list[tuple[str, bytes]]" = []
149+
if not self.input_dir.exists():
150+
return out
151+
for path in sorted(self.input_dir.glob("*.json")):
152+
self._pending[path.name] = path
153+
out.append((path.name, path.read_bytes()))
154+
return out
155+
156+
def _drain(self, message_id: str, subdir: str) -> None:
157+
path = self._pending.pop(message_id, None)
158+
if path is None or not path.exists():
159+
return
160+
dest_dir = self.output_dir / subdir
121161
dest_dir.mkdir(parents=True, exist_ok=True)
122162
dest = dest_dir / path.name
123163
if dest.exists(): # avoid clobbering on name reuse
124164
dest = dest_dir / f"{path.stem}.{hashlib.sha256(path.name.encode()).hexdigest()[:8]}{path.suffix}"
125165
os.replace(path, dest)
126166

127-
for path in sorted(input_dir.glob("*.json")) if input_dir.exists() else []:
167+
def ack(self, message_id: str) -> None:
168+
self._drain(message_id, "_processed")
169+
170+
def reject(self, message_id: str, reason: str) -> None:
171+
self.output_dir.mkdir(parents=True, exist_ok=True)
172+
with (self.output_dir / "_rejected.log").open("a", encoding="utf-8") as fh:
173+
fh.write(f"{message_id}: {reason}\n")
174+
self._drain(message_id, "_rejected")
175+
176+
def publish_finding(self, finding: dict, *, dedup_key: str) -> "tuple[str, bool]":
177+
self.output_dir.mkdir(parents=True, exist_ok=True)
178+
out = self.output_dir / f"finding-{dedup_key}.json"
179+
created = not out.exists()
180+
if created:
181+
out.write_text(json.dumps(finding, indent=2) + "\n", encoding="utf-8")
182+
return out.name, created
183+
184+
185+
class InMemoryBus:
186+
"""A thread-safe, dependency-free in-process ``MeshTransport`` — the agentplane
187+
live-wire seam. agentplane's MeshRush adapter calls ``publish_telemetry``; GDI
188+
drains it via ``consume``. The same contract fronts a networked broker later."""
189+
190+
def __init__(self) -> None:
191+
self._lock = threading.Lock()
192+
self._inbox: "OrderedDict[str, bytes]" = OrderedDict()
193+
self._findings: "OrderedDict[str, dict]" = OrderedDict()
194+
self._rejected: "list[tuple[str, str]]" = []
195+
self._seq = 0
196+
197+
def publish_telemetry(self, raw: "bytes | str") -> str:
198+
"""Publish a telemetry envelope onto the bus (the producer/agentplane side)."""
199+
data = raw if isinstance(raw, bytes) else raw.encode("utf-8")
200+
with self._lock:
201+
message_id = f"msg-{self._seq}"
202+
self._seq += 1
203+
self._inbox[message_id] = data
204+
return message_id
205+
206+
def poll(self) -> "list[tuple[str, bytes]]":
207+
with self._lock:
208+
return list(self._inbox.items())
209+
210+
def ack(self, message_id: str) -> None:
211+
with self._lock:
212+
self._inbox.pop(message_id, None)
213+
214+
def reject(self, message_id: str, reason: str) -> None:
215+
with self._lock:
216+
self._inbox.pop(message_id, None)
217+
self._rejected.append((message_id, reason))
218+
219+
def publish_finding(self, finding: dict, *, dedup_key: str) -> "tuple[str, bool]":
220+
with self._lock:
221+
created = dedup_key not in self._findings
222+
if created:
223+
self._findings[dedup_key] = finding
224+
return dedup_key, created
225+
226+
def findings(self) -> "list[dict]":
227+
"""All findings published so far (deduped), in first-seen order."""
228+
with self._lock:
229+
return list(self._findings.values())
230+
231+
def rejected(self) -> "list[tuple[str, str]]":
232+
"""All ``(message_id, reason)`` rejections so far."""
233+
with self._lock:
234+
return list(self._rejected)
235+
236+
237+
def consume(transport: MeshTransport, schema: dict, *, clock=_now_fields) -> dict:
238+
"""Drain ``transport``'s pending telemetry into findings, fail-closed + idempotent.
239+
240+
Returns stats ``{consumed, produced, rejected}``. A malformed or non-conforming
241+
envelope is rejected (no finding); a finding is published idempotently by the
242+
source event's content, so re-draining never duplicates.
243+
"""
244+
stats = {"consumed": 0, "produced": 0, "rejected": 0}
245+
for message_id, raw in transport.poll():
128246
stats["consumed"] += 1
129-
raw = path.read_bytes()
130247
try:
131248
event = json.loads(raw)
132-
except (json.JSONDecodeError, OSError) as exc:
249+
except (json.JSONDecodeError, ValueError) as exc:
133250
stats["rejected"] += 1
134-
rejected_log.append(f"{path.name}: unreadable/invalid JSON ({exc})")
135-
_drain(path, rejected_dir)
251+
transport.reject(message_id, f"unreadable/invalid JSON ({exc})")
136252
continue
137253
errs = envelope_errors(event, schema)
138254
if errs:
139255
stats["rejected"] += 1
140-
rejected_log.append(f"{path.name}: {'; '.join(errs)}")
141-
_drain(path, rejected_dir)
256+
transport.reject(message_id, "; ".join(errs))
142257
continue
143258
ts_ms, utc = clock()
144259
finding = _finding_for(event, ts_ms, utc)
145-
# Idempotent: derive the finding filename from the source event content.
146-
out = output_dir / f"finding-{hashlib.sha256(raw).hexdigest()[:16]}.json"
147-
if not out.exists():
148-
out.write_text(json.dumps(finding, indent=2) + "\n", encoding="utf-8")
260+
_, created = transport.publish_finding(finding, dedup_key=_dedup_key(raw))
261+
if created:
149262
stats["produced"] += 1
150-
_drain(path, processed_dir)
151-
152-
if rejected_log:
153-
with (output_dir / "_rejected.log").open("a", encoding="utf-8") as fh:
154-
fh.write("\n".join(rejected_log) + "\n")
263+
transport.ack(message_id)
155264
return stats
156265

157266

267+
def consume_once(input_dir: Path, output_dir: Path, schema: dict, *, clock=_now_fields) -> dict:
268+
"""Consume every *.json envelope in ``input_dir``; write findings to ``output_dir``.
269+
270+
Back-compat wrapper over ``consume`` with a ``FilesystemMailbox`` transport.
271+
"""
272+
return consume(FilesystemMailbox(input_dir, output_dir), schema, clock=clock)
273+
274+
158275
def main() -> int:
159276
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
160277
inbox = Path(os.environ.get("MESH_INPUT_DIR", str(ROOT / "mesh" / "telemetry")))

tools/tests/test_mesh_consume.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
66

7-
from mesh_consume import consume_once, envelope_errors # noqa: E402
7+
from mesh_consume import ( # noqa: E402
8+
InMemoryBus,
9+
consume,
10+
consume_once,
11+
envelope_errors,
12+
)
813

914
ROOT = Path(__file__).resolve().parents[2]
1015
SCHEMA = json.loads((ROOT / "open-ai4it-spec/contracts/schemas/event-envelope.schema.json").read_text())
@@ -76,3 +81,49 @@ def test_inbox_is_drained_so_reruns_do_not_reconsume(tmp_path):
7681
assert stats2 == {"consumed": 0, "produced": 0, "rejected": 0}
7782
assert len(list(outbox.glob("finding-*.json"))) == 1
7883
assert (outbox / "_processed" / "e1.json").exists()
84+
85+
86+
# --- GDI-2b: pluggable transport / in-process bus (agentplane live wire) --------
87+
88+
def test_inmemory_bus_valid_produces_finding_and_acks():
89+
bus = InMemoryBus()
90+
bus.publish_telemetry(json.dumps(_valid_slot_fill())) # agentplane producer side
91+
stats = consume(bus, SCHEMA, clock=CLOCK)
92+
assert stats == {"consumed": 1, "produced": 1, "rejected": 0}
93+
findings = bus.findings()
94+
assert len(findings) == 1
95+
assert findings[0]["data"]["severity"] == "warn"
96+
assert envelope_errors(findings[0], SCHEMA) == []
97+
assert bus.poll() == [] # acked -> not re-polled
98+
99+
100+
def test_inmemory_bus_malformed_is_rejected_not_a_finding():
101+
bus = InMemoryBus()
102+
bus.publish_telemetry(json.dumps({"type": "MeshRush", "data": {}})) # missing/invalid fields
103+
stats = consume(bus, SCHEMA, clock=CLOCK)
104+
assert stats["rejected"] == 1 and stats["produced"] == 0
105+
assert bus.findings() == []
106+
assert len(bus.rejected()) == 1
107+
108+
109+
def test_inmemory_bus_is_idempotent_across_identical_events():
110+
bus = InMemoryBus()
111+
payload = json.dumps(_valid_slot_fill())
112+
bus.publish_telemetry(payload)
113+
bus.publish_telemetry(payload) # identical content -> same dedup key
114+
stats = consume(bus, SCHEMA, clock=CLOCK)
115+
assert stats["consumed"] == 2
116+
assert stats["produced"] == 1 # deduped: one finding only
117+
assert len(bus.findings()) == 1
118+
119+
120+
def test_bus_and_mailbox_agree_over_the_same_contract(tmp_path):
121+
# the swap-the-transport promise: identical stats over either transport.
122+
ev = _valid_slot_fill()
123+
inbox, outbox = tmp_path / "in", tmp_path / "out"
124+
_write(inbox, "e1.json", ev)
125+
fs_stats = consume_once(inbox, outbox, SCHEMA, clock=CLOCK)
126+
bus = InMemoryBus()
127+
bus.publish_telemetry(json.dumps(ev))
128+
bus_stats = consume(bus, SCHEMA, clock=CLOCK)
129+
assert fs_stats == bus_stats == {"consumed": 1, "produced": 1, "rejected": 0}

0 commit comments

Comments
 (0)