Repository navigation
Knarr v0.48.0 — The Final Gate
Release Notes — v0.48.0 "The Final Gate"
Date: 2026-03-16
Branch: feature/v0.48.0
Base: v0.47.0
Summary
Final formal sprint. Ships 9 spec items across test hygiene, economy, cross-node event bus, DHT plugin infrastructure, watchman reliability, and speculative routing. Five-model adversary gate (38 raw findings → 10 confirmed TPs, all fixed before promotion). The protocol is feature-complete; future work is experiment-driven.
Breaking Changes
None. All new config keys (tab_reminder_auto_netting, [recovery] defaults) are opt-in or have safe defaults.
Track A — Test Infrastructure & Hygiene
TEST-04: Stale Version Checkpoint
test_version_bumped hardcoded "0.45.0". Replaced with a semver regex (r"^\d+\.\d+\.\d+$").
TEST-02: Python 3.13 asyncio.get_event_loop() Compat
DHTNode.__init__ called asyncio.get_event_loop() without an active loop — raises DeprecationWarning (3.12) and RuntimeError (3.13) in non-async context. Fixed with asyncio.new_event_loop() + asyncio.set_event_loop(). Resolves 7 pre-existing test failures (test_cockpit_api ×4, test_result_mapping ×3).
TEST-03: get_or_create_ledger_entry Returns Wrong Balance
storage.py returned LedgerEntry(balance=initial_balance) but the DB row was always inserted with balance=0.0. Comment said "security rule: always 0.0" but the return contradicted it. Fixed to return LedgerEntry(balance=0.0).
BR-GATE-001-TEST: Unit Tests for tab_reminder WM Gate Bypass
Three-scenario test suite confirming the v0.47.0 WM bypass fix holds: (a) self-delivery path dispatches without WM rejection, (b) cross-node path dispatches without proof field, (c) signed commerce docs still route through WM normally.
Track B — Economy & Event Bus
BR-MAIL-001-EXT: Tab Reminder Auto-Netting Trigger
_maybe_send_tab_reminder now checks [settlement] tab_reminder_auto_netting (default false). When enabled and utilization exceeds tab_reminder_threshold, calls await _run_netting_cycle_if_due() automatically. Closes the credit discipline loop for high-utilization peers without operator intervention.
_run_netting_cycle_if_due promoted to async def; internal call site updated. The method's time-based guard (_if_due) prevents thundering herd.
A5-EXT: mail.flush_skip Reason Field
bus.emit("mail.flush_skip", ...) now includes reason="no_route". Makes the event self-describing for Thrall/Tyr consumers without requiring them to infer the skip cause from context.
B1-EXT: GET /api/settlements Input Validation
GET /api/settlements now returns HTTP 400 when limit or offset contain non-integer values. Valid integers (including negative) are clamped. The validation is applied in the existing _handle_settlements_list production path; total is a full COUNT query across all rows, not page count.
BUS-01: Cross-Node EventBus
New EventNotify wire message type (core/messages.py). A _bus_broadcast_loop serializes selected topics from the local bus and forwards them to all connected peers as EventNotify messages. _handle_event_notify receives, verifies, deserializes, and re-emits on the local bus.
Topic allowlist: {"mail.flush_skip"}. credit.change excluded (TP-6: bilateral financial data).
Re-broadcast prevention: outgoing EventNotify is tagged node_id=self.node_info.node_id; receive handler checks origin_node marker on re-emit to prevent loops.
Track C — DHT / Discovery
B2-EXT: Bootstrap Isolation Warning Uses Effective Peer List
_check_bootstrap_isolation now considers both [network] bootstrap config peers and runtime --bootstrap CLI args before firing the isolation warning. Eliminates false positives when bootstrap peers are supplied at runtime only.
C1-EXT: Speculative Routing for Explicit-Tier Peers
_select_execute_provider (now async def) queries both DHT skill announcements (query_skills_by_name) and the operator's explicit-tier address book (get_addresses_by_tier("explicit")). Explicit-tier peers are injected as plain candidates alongside DHT results. Only tier="explicit" entries are included — operator-declared trust, no special-casing.
Supports private cluster deployments where trusted peers are pre-configured rather than DHT-discovered.
Track D — Watchman Reliability
WM-I1: Graceful Shutdown via Sentinel File
Supervisor._terminate() writes knarr.stop to data_dir before initiating shutdown. The node process polls for the sentinel in its main loop, drains in-flight tasks, and exits cleanly. A configurable drain timeout ([watchman] drain_timeout) controls how long the supervisor waits before force-killing. Sentinel is deleted after confirmed clean drain.
WM-04: Auto-Upgrade Spawn Tracking Restored
After cmd_upgrade, self._proc is now correctly populated from the synchronous spawn path. The DETACHED_PROCESS workaround in v0.47.0 left self._proc = None, preventing crash detection and restart in the newly spawned process. Spawn tracking restored for all code paths.
Plugin Infrastructure (D-007 Phase D partial)
PluginContext extended with a node= factory path: PluginContext(node=node, plugin_name="test", send_fn=fn, data_dir=path). Wires three new callbacks: remove_peer → storage.remove_peer, upsert_address → storage.upsert_address, push_to_peer → send_fn. Legacy positional constructor unchanged.
D-007 Phase D KAD plugin not shipped. Core _peer_heartbeat_sweep_loop re-enabled (Mímir ruling 2026-03-16) pending full Phase D delivery. CR filed: D-007-D.
Adversary Gate
Five-model adversary panel (GPT-5.4, Gemini 3.1 Pro, Opus 4.6, Sonnet 4.6, Qwen 3.5). 38 raw findings → 13 unique → 10 confirmed TPs, all fixed:
| ID | Severity | Finding |
|---|---|---|
| TP-1 | HIGH | Missing await on _run_netting_cycle_if_due in handlers.py — auto-netting silently discarded |
| TP-2 | HIGH | origin_node not stripped from EventNotify payload → bus.emit TypeError crash (remote DoS) |
| TP-3 | HIGH | _select_execute_provider called non-existent storage methods → C1-EXT dead |
| TP-4 | MEDIUM | EventNotify.origin_node_id unverified — any peer can forge origin identity |
| TP-5 | MEDIUM | Supervisor._restart() KeyError on missing [recovery] config section |
| TP-6 | MEDIUM | credit.change in broadcast topics — leaks bilateral financial data to all peers |
| TP-7 | MEDIUM | hops in SIGNATURE_EXCLUDED_FIELDS — on-path attacker can suppress any EventNotify |
| TP-8 | MEDIUM | Sentinel knarr.stop not cleaned up after drain — spurious exit on next start |
| TP-9 | LOW | _handle_settlements never registered in routes (functionally resolved by existing path) |
| TP-10 | LOW | total returned page count not full count (functionally resolved by existing path) |
Test Coverage
New test files (Wave 1 + adversary suite):
test_event_notify_v48.py— BUS-01 EventNotify wire, re-broadcast guard, topic allowlist, TP-2/TP-6/TP-7 fixestest_flush_reason_v48.py— A5-EXT reason fieldtest_settlements_400_v48.py— B1-EXT 400 validationtest_tab_netting_v48.py— BR-MAIL-001-EXT auto-netting triggertest_wmi1_sentinel_v48.py— WM-I1 sentinel write/poll/drain/cleanuptest_watchman_br_gate_v48.py— BR-GATE-001-TEST three-scenario coveragetest_v0_32_0.py— TEST-04 semver checkpoint update
Wave 1 results (post-adversarial-fix): 54 passed, 6 skipped (D-007 KAD plugin deferred), 0 failed.
Cluster Gate
exp-1000 first-night run (2026-03-16):
- Peer count never approached the 200-node ceiling (D-007 Phase D pre-validation)
mail.flush_skipobserved in experiment logs (G5-EXP101 ✓)- Cross-node EventBus events propagating correctly (BUS-01)
- Auto-netting trigger confirmed on threshold breach (BR-MAIL-001-EXT)
Known Issues / Deferred
| Item | Status |
|---|---|
| D-007 Phase D — KAD plugin on_tick sweep | Deferred — CR D-007-D filed. Sweep loop re-enabled until Phase D lands. |
| PluginContext legacy constructor (17-arg) | Deferred — factory path added; full redesign when Phase D ready |
BCW _resolve_peer_public_key full table scan |
Deferred from v0.47.0 |
| Route BCW deposit through Storage API | Deferred from v0.47.0 |
| BCW new-peer credit miss on first deposit | Deferred from v0.47.0 |
| M-015 Tick starvation (full fix) | Deferred |
| Thrall gather rewire to punchhole | Deferred — depends on B3 backend (Viggo) |
knarr upgrade missing --data-dir flag |
Deferred |
| Relative TLS paths not rebased to data_dir | Deferred |
| sindri.node on v0.45.0 | Upgrade manually when convenient |