Skip to content

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

Open
Maqbool61 wants to merge 1 commit into
DobermanCore:mainfrom
Maqbool61:main
Open

feat(FM.1): ambient activity bus — ActivityEvent, SQLite bus, and the doberman.collectors seam#512
Maqbool61 wants to merge 1 commit into
DobermanCore:mainfrom
Maqbool61:main

Conversation

@Maqbool61

Copy link
Copy Markdown
Contributor

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.

…ors seam

Closes DobermanCore#236

- Add ActivityEvent to models.py: frozen, extra='forbid', wraps a
  SecurityObject plus collector id, HMAC entity/session fingerprints,
  and string class metadata. Redaction is enforced by construction —
  no field can carry a raw secret.

- Add activity_events + monitor_state tables to storage/db.py
  (schema v14, both CREATE TABLE IF NOT EXISTS — no migration needed).

- Add storage/activity.py: emit_activity_event (best-effort, never
  raises, rejects and counts oversize events), cursor-based
  read_activity_events (no replay, no loss), purge_activity_events
  (bounded retention, refuses naive datetimes), save/load_cursor.

- Add COLLECTOR_GROUP constant and discover_collectors() to
  engine/registry.py: doberman.collectors entry-point group, collect()
  contract enforced structurally, failures isolated like every other
  plugin group.

- Add import-linter contract: policy core must not import
  doberman.monitor (bus stays off the decision path at import level).

- Add 26 tests proving every spec requirement: secret never stored,
  oversize rejection, cursor resume, purge, stub/raising collector
  isolation, lint-imports green.

26/26 tests pass. Full suite: 3420 passed, 3 skipped. Coverage 91.51%.
@Maqbool61 Maqbool61 closed this Aug 30, 2026
@Maqbool61 Maqbool61 reopened this Aug 30, 2026
@Maqbool61

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ambient activity bus: ActivityEvent, SQLite bus, and the doberman.collectors seam (FM.1)

1 participant