Menu

#512 feat(FM.1): ambient activity bus — ActivityEvent, SQLite bus, and the doberman.collectors seam

open
nobody
None
2026-08-30
2026-08-30
Anonymous
No

Originally created by: Maqbool61

Closes [#236]

First slice of the ambient monitor: the data plane other agent surfaces
will report through.

What this adds

ActivityEvent in models.py
Frozen, extra="forbid", wraps a SecurityObject plus a collector id,
HMAC entity/session fingerprints, and string-valued class metadata
(target_class, action_class). Redaction is enforced by construction —
there is no field a raw command, argument, or secret could ride in.

Local SQLite bus — storage/db.py + storage/activity.py
Two additive CREATE TABLE IF NOT EXISTS tables (schema v14, no
migration needed on existing DBs):

  • activity_events — append-only bus log, one row per ActivityEvent
  • monitor_state — per-reader cursor so consumers resume without
    replaying or losing rows

Three operations in storage/activity.py:

  • emit_activity_event — best-effort, never raises into the caller;
    oversize events (> 4 KiB serialized) are rejected and counted in
    _oversized_count
  • read_activity_events — cursor-based page read keyed on the integer
    primary key, immune to clock skew and concurrent writers
  • purge_activity_events — bounded retention; refuses naive datetimes
    so a caller mistake can never silently delete all rows

save_cursor / load_cursor persist each reader's position in
monitor_state.

Plugin seam — engine/registry.py
COLLECTOR_GROUP = "doberman.collectors" and discover_collectors():
loaded defensively via the same entry-point pattern as every other
plugin group. Each collector must expose collect() -> Iterable[ActivityEvent]; a raising collector is isolated — its events
are dropped for that tick, but the bus and every other collector
continue unaffected. Returns [] on a core-only install.

Import boundary — pyproject.toml
doberman.monitor joins the import-linter forbidden list alongside
doberman.proxy, doberman.hosthooks, doberman.dash, and
doberman.turngate. The bus is off the decision path at the import
level as well as the runtime level.

Test proof points (26 tests, all green)

  • Synthetic secret placed in raw input never appears in a stored event —
    verified at the byte level against the raw DB
  • extra="forbid" rejects any unknown field (including an accidental
    raw_secret kwarg) at construction time
  • Malformed and oversize events rejected and counted; emit never raises
    even on DB failure
  • Cursor resume: no event lost, no event replayed — verified across
    paged reads and a mid-stream write
  • Retention purge: old rows gone, new rows kept; naive datetime refused
  • Stub collector discovered via the entry-point seam; raising collector
    isolated — good collector still emits
  • lint-imports: 5/5 contracts kept

CI results

26 passed in 1.58s # activity bus suite
3420 passed, 3 skipped # full suite
91.51% coverage # above the 80% threshold
ruff check . ✓
ruff format --check . ✓
lint-imports: 5 kept, 0 broken ✓

Invariant preserved

Nothing on this bus can suppress, lower, or alter an inline decision.
There is no code path from it into combine(). The import-linter
contract makes this a CI-enforced invariant, not a review-only
convention.

Related

Tickets: #236

Discussion

  • Anonymous

    Anonymous - 2026-08-30

    Ticket changed by: Maqbool61

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-08-30

    Ticket changed by: Maqbool61

    • status: closed --> open
     
  • Anonymous

    Anonymous - 2026-08-30

    Originally posted by: Maqbool61

    The Windows job was cancelled by the runner at 40m — no test failures in the log, just a timeout. All three Ubuntu matrix runs (3.11, 3.12, 3.13) passed cleanly. Happy to re-run if needed.

     

Log in to post a comment.