fix: harden gossip, HLC restarts, watcher races, MCP args, pagination (#3)

Implements the top findings from the codebase review, verified with tests and live CLI/MCP checks.

**Gossip integrity**
- Push validation: 4 MiB body cap, 1000-row batch cap; rows claiming the local node id (vector-poisoning), empty node ids, and negative HCLs rejected (internal/watch/gossip.go, internal/db/gossip.go)
- Reconcile-on-pull: Run returns the pulled count, syncGossip rebuilds derived state when > 0 — entry-count comparison could never fire, so synced observations never materialized into searchable entries

**Data-layer safety**
- HLC resumed from MAX(hcl) at Open (hlc.SeekTo): a restart with a regressed wall clock cannot reissue values the (node_id, hcl) locator and pull cursors depend on
- Writer serialization: _txlock=immediate DSN + SetMaxOpenConns(1) + per-KnoxDB mutex around RecordObservation's check-then-insert dedup (closes duplicate-row race)

**Watch daemon**
- Ticker guard flags now atomic.Bool (was a cross-goroutine data race)
- Trailing-edge per-path debounce (timer-based, pruned on fire/delete)
- Recursive watches (startup tree walk + watcher.Add on dir Create); Rename re-ingests, Remove cancels pending ingests

**MCP + CLI**
- Strict arg validation, no silent clamping: thread_id 0 errors instead of renaming thread #1; empty knox_thread_link {} errors instead of false success; thread existence checked before writes; golden-thread tool nil-safe
- --page 0 errors instead of panicking; query/recent pagination actually pages (page x limit)

**Tests** (new internal/hlc and internal/db packages): SeekTo monotonicity, concurrent dedup race, push validation, reopen HCL monotonicity, batch caps, self-spoof rejection, idempotency on observation counts.

Verified: go build, go vet, full suite with -race, live MCP stdio transcripts against a scratch DB.
Reviewed-on: #3
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
This commit was merged in pull request #3.
This commit is contained in:
2026-09-17 09:06:08 +00:00
committed by david
parent d6d2a24ddc
commit bb852faa27
10 changed files with 733 additions and 116 deletions
+14
View File
@@ -3,6 +3,7 @@ package db
import (
"database/sql"
"fmt"
"log"
)
// GossipObservation is the serializable wire form of an observation exchanged
@@ -40,6 +41,19 @@ func (k *KnoxDB) PushObservations(rows []GossipObservation) (int, error) {
inserted := 0
for _, o := range rows {
// Reject malformed or forged rows. Nodes only ever push their own
// observations, so a row claiming this node's id cannot be legitimate:
// accepting it would let a peer poison our knowledge vector (a forged
// max-HCL makes peers believe they have our whole history and stop
// pulling). Empty node ids and negative HCLs are likewise never produced
// by a real node.
if o.NodeID == "" || o.HCL < 0 {
continue
}
if o.NodeID == k.nodeID {
log.Printf("[gossip] dropped pushed row claiming local node_id (spoof?)")
continue
}
res, err := tx.Exec(
`INSERT OR IGNORE INTO observations
(fingerprint, source_id, source_path, project, content_type, title, summary,