Skip to content

0.8.0-dev2: Reflex — registry + runner + 3 first-party handlers#2

Merged
stevenca merged 2 commits into
mainfrom
feat/0.8.0-dev2-reflex-skeleton
Jun 1, 2026
Merged

0.8.0-dev2: Reflex — registry + runner + 3 first-party handlers#2
stevenca merged 2 commits into
mainfrom
feat/0.8.0-dev2-reflex-skeleton

Conversation

@stevenca
Copy link
Copy Markdown
Owner

@stevenca stevenca commented Jun 1, 2026

Summary

Second sub-step of the brain refactor. Stands up the reflex layer — fast deterministic responders that subscribe to the thalamus and produce ReflexOutcome records.

Nothing publishes to the bus yet (that lands in 0.8.0-dev3), so the handlers idle. The plumbing they idle on is fully tested against InMemoryEventBus, which — per 0.8.0-dev1's contract suite — behaves identically to the production NatsEventBus. When the first publisher lands, the path lights up end-to-end with no changes here.

Public surface (netcortex/reflex/)

  • protocol.pyReflexHandler Protocol + frozen ReflexOutcome dataclass + Severity / OutcomeKind literal types. Severity is a four-bucket scale (info | warn | high | critical) so downstream alerting can pattern-match without parsing free-form strings.
  • registry.py — process-wide register_handler / get_handler / all_handlers / clear_registry. Idempotent re-registration of the same instance; duplicate-id collisions raise DuplicateHandlerError (handler ids appear on every persisted outcome — silent shadowing would be an operator footgun).
  • runner.pyReflexRunner wires the registry to one bus, spawns one asyncio task per handler, isolates per-handler exceptions (raising handler → errored outcome with truncated traceback, dispatcher continues). Idempotent start() / stop().

First-party handlers (all currently idle)

Handler Subject Severity Notes
link_down sensory.snmp.trap.link_down.> high Caps upstream key echo at 16
security_webhook sensory.meraki.webhook.security.> from payload Coarse Meraki severity map
bgp_drop sensory.snmp.trap.bgp_backward_transition.> high Target = device|peer when both known

Each handler is intentionally minimal — capture event, extract target, return logged outcome. Semantic-memory lookup, maintenance-window check, dedup, NetBox journal mirror all land in later sub-steps once a publisher exists to drive them.

Test plan

CI auto-runs:

  • Unit tests — picks up new tests/reflex/ directory (~29 cases across 3 files)
    • test_registry.py (7 cases): register/lookup, ordering, dup rejection, idempotent re-register, type rejection, missing-key, clear. Save/restore fixture prevents leaking cleared state to siblings.
    • test_runner.py (8 cases against InMemoryEventBus): dispatch, pattern filtering, fan-out, exception isolation, None-outcome skip, idempotent start/stop, stop-without-start safety, registry enumeration.
    • test_handlers.py (14 cases): pin operator-facing surface (id + pattern) and exercise per-handler outcome shape + target extraction fallbacks.
  • Contract tests against both InMemoryEventBus AND real NatsEventBus (unchanged from dev1)
  • Golden / lint / mypy / security / SBOM (unchanged)

Pre-push smoke against InMemoryEventBus: 3 publishes → 3 outcomes routed to the right handlers with correct severity and target. NatsEventBus path will behave identically (proven by the dev1 contract suite).

Roadmap

# Branch What lands Status
1 feat/0.8.0-dev1-thalamus-nats NATS infra + NatsEventBus + contract parametrization ✅ merged
2 feat/0.8.0-dev2-reflex-skeleton netcortex/reflex/ skeleton + 3 idle handlers this PR
3 feat/0.8.0-dev3-first-publisher First poller dual-writes to bus; reflex actually fires; outcomes persisted to Neo4j next
4 feat/0.8.0-dev4-module-renames adapters/*sensory/poll/*, graph/correlate.pyassociation/
5 feat/0.8.0-dev5-cutover Pollers ONLY publish to bus; legacy direct path removed
6 tag 0.8.0 Smoke test on microk8s, tag

Made with Cursor

stevenca and others added 2 commits June 1, 2026 05:23
Second sub-step of the brain refactor. Stands up the REFLEX LAYER —
fast deterministic responders that subscribe to the thalamus and
produce ReflexOutcome records. Nothing publishes to the bus yet (that
lands in 0.8.0-dev3), so the handlers idle. The plumbing they idle on
is fully tested against InMemoryEventBus, which (per 0.8.0-dev1's
contract suite) behaves identically to the production NatsEventBus —
so when the first publisher lands the path lights up end-to-end.

PUBLIC SURFACE — netcortex/reflex/

  * protocol.py — ReflexHandler Protocol + frozen ReflexOutcome
    dataclass + Severity / OutcomeKind literal types. Severity is a
    four-bucket scale (info | warn | high | critical) so downstream
    alerting can pattern-match without parsing free-form strings.
  * registry.py — process-wide register_handler / get_handler /
    all_handlers / clear_registry. Idempotent re-registration of the
    same instance; duplicate-id collisions raise DuplicateHandlerError
    (handler ids appear on every persisted outcome — silent shadowing
    would be an operator footgun).
  * runner.py — ReflexRunner wires the registry to one bus, spawns
    one asyncio task per handler, isolates per-handler exceptions
    (raising handler → "errored" outcome with truncated traceback in
    diagnostic, dispatcher continues). Idempotent start/stop;
    ready_event for tests that need cross-handler ordering.

FIRST-PARTY HANDLERS (all currently idle)

  * link_down — sensory.snmp.trap.link_down.> → high severity. Caps
    upstream key echo at 16 to bound outcome size.
  * security_webhook — sensory.meraki.webhook.security.> → severity
    derived from the Meraki payload via a coarse map
    (informational/warning/high/critical → info/warn/high/critical),
    defaults to warn for unknown values.
  * bgp_drop — sensory.snmp.trap.bgp_backward_transition.> → high
    severity. Target composed as "device|peer" when both are known,
    falls back to whichever is present.

Each handler is intentionally minimal — captures the event, extracts
a target, returns a "logged" outcome. The richer behavior (semantic
memory lookup, maintenance-window check, dedup, NetBox journal mirror)
lands in later sub-steps once the first publisher exists to drive it.

TESTS — tests/reflex/

  * test_registry.py: 7 cases — register/lookup, insertion ordering,
    duplicate rejection, idempotent re-register, type rejection,
    missing-key, clear. Save/restore fixture so cleared state does not
    leak to sibling test files.
  * test_runner.py: 8 cases against InMemoryEventBus — dispatch
    matching events, pattern-filter non-matching, fan-out to multiple
    handlers, exception isolation, None outcome not recorded,
    idempotent start/stop, stop-without-start safety, registry
    enumeration default-arg path.
  * test_handlers.py: 14 cases pinning the operator-facing surface
    (handler id + subscription pattern) and exercising each handler's
    outcome-shape contract + target-extraction fallbacks.

End-to-end smoke against InMemoryEventBus: 3 publishes → 3 outcomes
routed to the right handlers with correct severity and target
extraction. NatsEventBus path will work identically (contract suite
proves both backends satisfy the same Protocol).

NOT YET WIRED

  * Still no publishers. Pollers continue to call correlator +
    writeback directly. First dual-write publisher lands in
    0.8.0-dev3.
  * Outcomes are logged only — Neo4j :ReflexEvent persistence + NetBox
    journal mirror land in 0.8.0-dev3 once the writer Protocols have
    a consumer to justify them.

Co-authored-by: Cursor <cursoragent@cursor.com>
test_handler_registered_with_expected_pattern was synchronous but
the module's pytestmark = pytest.mark.asyncio still applied to it,
producing 3 warnings per CI run. Future strict-mode pytest-asyncio
may upgrade those warnings to errors.

Trivially fixed by declaring the test async (no await required).
Splitting it into its own file just to avoid the marker would be a
worse trade-off.

Co-authored-by: Cursor <cursoragent@cursor.com>
@stevenca stevenca merged commit c8914aa into main Jun 1, 2026
8 checks passed
@stevenca stevenca deleted the feat/0.8.0-dev2-reflex-skeleton branch June 1, 2026 14:20
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.

1 participant