Commit Graph

101 Commits

Author SHA1 Message Date
m17hr1l
6dcaae39c3 stage-net-b network view: public endpoint + signed payload 2026-06-07 00:37:12 +02:00
m17hr1l
fbad78a611 stage-net-a network view: data model + local view builder 2026-06-07 00:36:29 +02:00
m17hr1l
77533eccb1 ui: widen content to 1600px + force long URLs/hashes to wrap (fixes 4K/1920 overflow) 2026-06-06 21:29:16 +02:00
m17hr1l
3e737d61b3 stage-34 wire admin nav: federation hub links to discovery + SW v4 2026-06-06 21:17:57 +02:00
m17hr1l
a53aacfdd8 merge auto-response: severity/quorum/local-only gated execution
# Conflicts:
#	src/psyc/db.py
2026-06-06 21:17:20 +02:00
m17hr1l
53ba537ce8 merge vouching+translog: web-of-trust + signed merkle audit log
# Conflicts:
#	src/psyc/_federation_cli.py
#	src/psyc/cockpit/federation_routes.py
2026-06-06 21:15:11 +02:00
m17hr1l
726117b19b merge discovery: DNS-SD walker + public peers endpoint 2026-06-06 21:13:29 +02:00
m17hr1l
c5472b3134 stage-auto-e pulse: tests for auto-response gating
Cover the auto-fire decision matrix:
- _severity_rank ordering
- mode != auto-execute → never fires (auto-propose, manual)
- below-threshold action is skipped + audited
- federation case + no quorum → skipped + audited "no quorum"
- federation case + quorum met → fires
- local case + quorum required + local-only on → still fires
- local case + quorum required + local-only off → still fires
- quorum gating disabled → federation cases fire too
- kill switch armed → tick() skips everything
- pulse_audit records both auto-fire and skip rows
- audit_count_since returns the per-action counts the cockpit needs
- config round-trips through pulse_settings

Tests patch federation.is_quorum_met (raising=False so the sibling
agent can ship the real function later without breaking these), and
swap respond.execute_action for a counter so no SOAR sink call escapes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:12:02 +02:00
m17hr1l
f5ca928f92 stage-auto-d pulse: cockpit auto-response state panel + CLI
Cockpit:
- /admin/pulse now renders an "AUTO-RESPONSE STATE" panel above the
  pipeline table — mode badge (traffic-light colored), threshold,
  quorum on/off, local-only on/off, auto-fired-in-24h count, last 5
  audit entries, and a one-form save for threshold + gates.
- POST /admin/pulse/respond-config writes the new gates.

CLI:
- pulse-respond-config [--threshold …] [--quorum/--no-quorum]
                       [--local-only/--no-local-only]
  Args left unset are unchanged; echoes the post-state.
- pulse-respond-status prints mode, gates, and the last 10 audit
  entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:11:52 +02:00
m17hr1l
e66c3d3359 stage-auto-c pulse: respond runner with gates + auto-fire path
Replace the propose-only respond runner with a two-phase runner: phase 1
always proposes actions for fresh high-severity cases (unchanged); phase 2
fires when pipeline mode is auto-execute and the action clears all gates.

Gates:
- severity ≥ configured threshold
- if require_quorum is on, federation-sourced cases must hit
  federation.is_quorum_met (wrapped in try/except so we tolerate the
  sibling agent not having shipped that function yet — fallback is
  "no quorum metric → don't auto-fire", the safe default)
- locally-generated cases (no row in federation_signals for their case_id)
  bypass the quorum check
- when local-only is armed, federation-sourced cases never auto-fire
  even with quorum

Every decision (auto-fire, skipped, error) records a pulse_audit row so
the cockpit and CLI can show history. Per-action try/except keeps one
bad action from aborting the whole batch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:11:39 +02:00
m17hr1l
f4148d86a6 stage-vouch-e federation: tests for vouching + quorum gate
test_vouching covers the contract auto-response and other agents will
gate on:

- issue_vouch round-trip (sign + verify under our own pubkey)
- accept_vouch rejects expired vouches
- accept_vouch rejects mismatched signatures
- accept_vouch rejects vouchers whose peers.status != "trusted"
- accept_vouch happy path
- is_vouched needs DISTINCT vouchers (two upserts from one peer == 1)
- is_vouched clears threshold with two distinct trusted vouchers
- is_quorum_met counts only listening-eligible peers (untrusted +
  duplicate rows don't count)
- quorum_config defaults + pulse_settings persistence
- import_signed_feed rejects unknown peer ("not trusted")
- import_signed_feed accepts directly-trusted peer
- import_signed_feed accepts a peer made eligible via two vouches
- import_signed_feed stores vouches embedded in a trusted peer's feed

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:11:18 +02:00
m17hr1l
0e56fa70af stage-vouch-d federation: cockpit pages + CLI + public endpoints
Public JSON endpoints (no auth):
- GET /federation/vouches — our_vouches() so peers can pull our trust
- GET /federation/log — last 100 transparency-log entries
- GET /federation/log/verify — re-walks the chain, returns
  {verified, head_hash} or 409 with {error, head_hash}

Admin pages (TOTP-gated):
- /admin/federation/vouches — issued list, issue form, revoke
  buttons, per-peer quorum-met table
- /admin/federation/log — chain verification status + last 200 entries
- /admin/federation/quorum — config form + per-peer eligibility +
  per-signal-hash distinct-eligible-peer counts

CLI: fed-vouch / fed-unvouch / fed-vouches / fed-quorum-set / fed-log /
fed-log-verify, plus existing fed-* commands untouched.

The base /admin/federation page now links to the three new sub-pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:11:03 +02:00
m17hr1l
31ec1557ec stage-auto-b pulse: pulse_audit table + history
Add a per-decision audit log so the cockpit + CLI can show what the
auto-response runner did each tick:
- pulse_audit table: id, pipeline, action ('auto-fire'|'skipped'|'error'),
  action_id, case_id, detail, timestamp
- helpers: pulse_audit_record, pulse_audit_recent, pulse_audit_count_since
- indexes on (pipeline, timestamp desc) and on action_id

Also add db.signals_for_case(case_id) — checks the federation_signals
buffer to tell whether a case was peer-sourced. Used by the runner to
decide if a quorum check is required.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:10:38 +02:00
m17hr1l
eadd1aea3b stage-vouch-c federation: import gate + translog hook (stage-trans-b)
import_signed_feed now refuses any feed whose declared fingerprint isn't
peer_is_listening_eligible (directly trusted OR vouched in), returning
Err("peer not trusted: …") before any signal lands.

For every case/IOC it does record, it also appends a "signal" entry to
the transparency log (best-effort — logger warns but doesn't abort
ingest if the append fails). This is the stage-trans-b hook: the
import path is the chokepoint, so attaching the chain there gives
us coverage of every peer-originated signal we've ever accepted.

build_signed_feed now includes our_vouches() in the feed body so vouches
propagate. On import we accept_vouch each one — but only if the embedded
voucher_fingerprint matches the peer we just authenticated, so a peer
can't forge vouches "from" someone else through us.

test_federation: the long-standing round-trip test now first registers
the synthetic peer as trusted so the gate lets it through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:10:36 +02:00
m17hr1l
234e6d98ba stage-vouch-b federation: vouch sign/verify + quorum API
Add the web-of-trust primitives, all keyed off the existing node keypair:

- Vouch + QuorumConfig Pydantic models
- vouch_payload_bytes — canonical-JSON body that the voucher signs
- issue_vouch / accept_vouch / revoke_vouch / our_vouches / vouches_for
- is_vouched(target, min_vouchers) — counts DISTINCT trusted vouchers,
  ignoring expired vouches and re-using QuorumConfig defaults
- peer_is_listening_eligible(fp) — direct-trust OR vouched-in
- is_quorum_met(signal_hash, k) — distinct listening-eligible peers
  reporting the same hash
- quorum_evidence(signal_hash) — (peer_fp, received_at) tuples for UI
- quorum_config / set_quorum_config — persisted in pulse_settings

accept_vouch is paranoid: rejects expired vouches, vouchers that aren't
currently "trusted" in our peers table, mismatched pubkey-fingerprint
pairs, malformed base64, and Ed25519 verification failures — each with
a short Err reason.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:10:03 +02:00
m17hr1l
0dbeb056c5 stage-auto-a pulse: respond config (threshold/quorum/local-only)
Persisted-key/value helpers for the respond-pipeline auto-fire gates:
- respond_auto_threshold / set_… (Severity, default HIGH)
- respond_require_quorum / set_… (bool, default True)
- respond_local_only / set_…     (bool, default False)
Plus a _severity_rank helper for threshold comparison.

Backing store is the existing pulse_settings table; this commit also adds
generic pulse_setting_get / pulse_setting_set helpers in db.py so future
pulse settings don't each need their own column-pair helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:09:46 +02:00
m17hr1l
7a510c7acf stage-trans-a translog: append-only signed merkle chain + tests
translog.append computes
sha256(canonical({prev_hash, entry_type, entry_data, timestamp})) and
writes one row per call; the first entry uses prev_hash = "0"*64.
verify_chain walks rows in id order, re-hashes each, and returns
Err("broken at id=X expected=... got=...") on the first mismatch — so
tampering with either entry_data or prev_hash invalidates every
downstream row. recent / entries_after / head support peer sync and UI.

Tests cover: genesis prev_hash, chained prev_hash, full-chain verify,
tampered-data detection, tampered-prev_hash detection, slicing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:09:32 +02:00
m17hr1l
4a9f6ceb7f stage-vouch-a federation: vouches table + DB helpers
Add `vouches` (voucher_fp, target_fp, issued_at, expires_at, signature)
with unique (voucher, target) and target index, plus `translog` for the
append-only signed merkle chain (id, prev_hash, entry_type, entry_data,
timestamp, entry_hash). Also surface `setting_get` / `setting_set`
helpers on pulse_settings so the quorum config has a place to live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:09:25 +02:00
m17hr1l
ff88aba569 stage-disc-e discovery: tests 2026-06-06 21:08:15 +02:00
m17hr1l
9b49f768ca stage-disc-d discovery: cockpit + CLI 2026-06-06 21:06:39 +02:00
m17hr1l
ddb40ff92c stage-disc-c discovery: pulse pipeline wiring + seeds settings 2026-06-06 21:04:54 +02:00
m17hr1l
6241a21af5 stage-disc-b discovery: peer walker (BFS) 2026-06-06 21:04:17 +02:00
m17hr1l
de6204819b stage-disc-a discovery: dnssd resolver + public peers endpoint 2026-06-06 21:03:33 +02:00
m17hr1l
1675a2326e stage-33 wire pulse + federation: cockpit routes, CLI, nav links, SW bump 2026-06-06 16:15:48 +02:00
m17hr1l
de5ff09815 merge federation: ed25519 identity + signed feeds
# Conflicts:
#	src/psyc/db.py
2026-06-06 16:13:36 +02:00
m17hr1l
02ce6d791c merge pulse: scheduler line + autonomy dial 2026-06-06 16:11:17 +02:00
m17hr1l
d4229dd264 stage-fed-g federation: tests 2026-06-06 16:10:31 +02:00
m17hr1l
2ef0448165 stage-fed-f federation: CLI commands 2026-06-06 16:10:26 +02:00
m17hr1l
17b94acf6b stage-fed-e federation: cockpit admin page + public feed routes 2026-06-06 16:10:19 +02:00
m17hr1l
55ffd9da3d stage-fed-d federation: signed feed export + verified import 2026-06-06 16:09:53 +02:00
m17hr1l
63e3ff2777 stage-fed-c federation: db tables for peers + signal buffer 2026-06-06 16:08:36 +02:00
m17hr1l
50158f7fa8 stage-fed-b federation: dns record format 2026-06-06 16:08:31 +02:00
m17hr1l
4c35aad2bb stage-fed-a federation: ed25519 keypair + fingerprint 2026-06-06 16:08:03 +02:00
m17hr1l
a7c59c9faa stage-33e pulse: tests 2026-06-06 16:06:54 +02:00
m17hr1l
e071f289f2 stage-33d pulse: CLI commands 2026-06-06 16:05:14 +02:00
m17hr1l
26fbe08b65 stage-33c pulse: admin cockpit page 2026-06-06 16:04:39 +02:00
m17hr1l
4d67605371 stage-33b pulse: db tables + persistence 2026-06-06 16:03:30 +02:00
m17hr1l
e710be6ebd stage-33a pulse: scheduler module with pipeline registry 2026-06-06 16:03:22 +02:00
m17hr1l
6356c5535b stage-32: add Umami analytics tracking script in base.html
Inserted the analytics.neuronetz.ai tracking script tag in the <head>
of base.html with the project's website-id. defer attribute means it
never blocks page render. Cookieless / privacy-respecting (Umami).
Applies to every page that extends base — the admin gate's standalone
template intentionally skips it (no point tracking the login screen).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 20:09:54 +02:00
m17hr1l
43c7c199c3 stage-31 polish: featured hero — particles sync with sweep, stat chips, radar emblem
1. Particles now blink in time with the scan column.
   Each <circle>/<rect> in the case hero SVG carries class="hero-particle"
   and a CSS animation-delay equal to -(arrival-time-of-sweep-at-its-x),
   so as the sweep marches left-to-right the particles light up in
   sequence — a real "the scanner just touched this point" effect. The
   sweep is now linear / non-alternating on a 12s cycle so the phase
   stays predictable. Drop-shadow + opacity flash at the peak.

2. Stat chips added under the featured title.
   Up to five compact chips showing incident type, total IOC count with
   per-type breakdown (U/D/I/H/C), victim country if mapped, confidence
   level, malware family. Each chip is color-coded.

3. Radar emblem in the top-right of the hero.
   Pure SVG: three concentric range rings, cross-hairs, a translucent
   sweep arm that spins every 4.5s, a center dot. The whole emblem
   inherits the cycling-hero hue cycle so it changes color with the
   grid. Smaller + dimmer on mobile; honors prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:38:31 +02:00
m17hr1l
977c3670f3 stage-31 polish: featured hero — full-bleed animated grid cycling colors
The hero is now layered:
  base   — radial accent + dark gradient (truly full-width, min 240px /
           up to 320px tall, no aspect-ratio cap)
  grid   — full-bleed CSS grid (38px cells), line color cycles through
           cyan → red → gold → green → violet over 18s, plus a slow
           grid-drift so the lattice subtly slides; a glowing column
           sweeps left-to-right across the hero every 12s for depth
  particles — the existing case-specific SVG sits on top at lower
           opacity (mix-blend-mode: screen) so the per-case identity
           is preserved as a constellation glittering over the grid
  overlay — title + Open-case CTA pinned to the bottom

Severity just nudges the grid opacity (CRITICAL/HIGH get a bit denser);
the color cycle stays on for all severities. Honors prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:32:27 +02:00
m17hr1l
3f1f7cc420 stage-31 polish: featured-threat banner — uniform tint, full width
The banner gradient was a horizontal accent → dark fade, so visually
only the left ~30% read as a banner; the right looked unstyled. Swapped
for a uniform severity-tinted background (subtle top-to-bottom gradient
for depth, but no horizontal falloff). Border + inset shadow on the
bottom edge give it a real divider between the banner and the hero
beneath. Per-severity tints (CRITICAL red, HIGH amber) get the same
treatment — full-width, uniform.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:28:26 +02:00
m17hr1l
04e0d3323f stage-31 fix: featured card — display:block on the new <a> wrapper
When I refactored the featured card from <section>→<a> for full-card
clicking, I forgot the <a> defaults to display:inline. That collapsed
the hero+overlay layout — the title rendered word-per-line down the
left edge instead of spanning the hero. display:block (+ inherit
color, no text-decoration) restores the layout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:15:48 +02:00
m17hr1l
5cf7cb5655 stage-31 polish: featured banner header + clickable news cards w/ hover
Two enhancements from the screenshot annotations:

1. The featured card now has a proper banner header above the hero —
   gradient strip with "⌖ Featured Threat" label, a pulsing severity
   dot, severity/TLP badges, source + ingest time. The header is
   sev-themed (red border + glow for CRITICAL, amber for HIGH), and an
   "Open case →" CTA at the bottom of the hero with an arrow that
   nudges right on hover.

2. News items are now full-card clickable (transparent overlay link)
   with rich severity-tinted hover transitions:
   - Lifts (translateY -1px), tinted background + border that matches
     severity (red CRITICAL, amber HIGH, pale MEDIUM/LOW), per-kind
     accents for enforced/submitted/rejected/failed items.
   - A right-side arrow (→) slides in on hover signaling "click to open".
   - Case glyph SVG gets a subtle drop-shadow + 1.04x scale on hover.
   - 180ms ease transitions across background/border/transform/shadow.
   - Respects prefers-reduced-motion (turns animations off).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:12:55 +02:00
m17hr1l
f51e672ad3 stage-31 fix: home page 500 — replace bad jinja sum with precomputed count
Template had `{{ buckets|sum(attribute='items')|length }}` which tried
to sum lists starting at 0 → TypeError. Switched to a Python-side
`total_items = sum(len(b.items) for b in buckets)` passed through the
context and rendered directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:05:25 +02:00
m17hr1l
76a0b0b636 stage-31: Newsline polish — featured case, time buckets, severity accents, generated visuals
The home page goes from a flat event stream to something that reads like
a news blog:

- Featured-case hero card at the top, picked as the highest-severity
  case (CRITICAL > HIGH, tie-break recency) from the last 7 days. Wide,
  with a procedurally generated SVG hero behind a gradient overlay that
  carries title + severity + TLP + feed + ingest time.
- Recent activity is now grouped under Today / Yesterday / Earlier this
  week / Older bucket headers.
- Each item gets a left-border severity accent (red CRITICAL, amber
  HIGH, muted MEDIUM/LOW) so the page is scannable at a glance.

Images: new cockpit/case_visuals.py generates SVGs from case data —
zero external image gen, zero curated assets. Every visual is
deterministic from case_id (so a case keeps its identity across
sessions) and themed to its severity:

- case_hero_svg() — 880x220 hero with severity radial glow, a faint
  scan grid, a particle constellation with auto-connecting lines, HUD
  corner brackets, and the case id whispered in the bottom-right.
- case_glyph_svg() — small mirror-symmetric identicon (5-grid),
  severity-colored, shown beside each case news item in place of an
  emoji icon. Two case_ids → two distinct glyphs; same id → same glyph.

7 news tests pass; visual sanity print confirms hero is deterministic
and uses the right severity accent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:02:22 +02:00
m17hr1l
4d36db90f1 stage-30 fix: SW cache strategy — bump version + stale-while-revalidate
User reported mobile nav still looked unresponsive. Verified the new
nav-toggle CSS and markup are deployed; the browser's service worker
was serving the prior cockpit.css cache-first from psyc-v1.

Two fixes so this can't recur:
- CACHE_VERSION bumped to psyc-v2 → the activate handler now wipes
  the v1 cache on the user's next visit.
- Static-asset strategy changed from cache-first to
  stale-while-revalidate: serve from cache for instant render, then
  refresh in the background so the *next* load picks up the new CSS/JS
  with no manual cache-busting. Falls back to network if cold.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:36:08 +02:00
m17hr1l
88e4fb1dcd stage-30 fix: proper responsive nav (hamburger drawer) + cases-pipeline fix
Replaced the horizontal-scroll hack with a real mobile pattern:
- Hamburger button (☰) appears below 760px width, animates into an X when
  open. Tap to slide the full nav into a drawer below the topbar, each
  link a tap-target-sized row.
- Nav links auto-close the drawer on click so navigating dismisses it.
- aria-expanded / aria-controls wired up so screen readers track state.
- Below 420px the model status chip + family icon hide to keep the
  topbar compact; brand-sub already hidden by the earlier mobile pass.

Discovered while debugging: prod had 377 unclassified cases (only
fetch-all was run, not classify-all/prove-all/reindex), so the journey
page's Classifier beat skipped the live model verdict entirely. Ran
the full pipeline on prod — the psyc-v5 model chip now renders on
case journeys. Worth noting for the deploy runbook: after psyc fetch-all
on a new env, always also run classify-all + prove-all + reindex to
prime the pipeline state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:27:11 +02:00
m17hr1l
16cf873044 stage-30: home page (Newsline digest) + PWA + mobile pass
NEW / start page replaces the redirect-to-/cases:
- KPI strip (cases, IOCs, +24h, high/critical,  enforced 24h, ledger total) —
  clickable, responsive grid (2 cols mobile, 3 mid, 6 desktop).
- Recent activity feed: ledger events (enforced/submitted/rejected/failed) +
  newest case ingests, interleaved newest-first, with severity badges, icons,
  case links. Sources via lines/news.py.
- Feed health sidebar: per-feed count + last ingest time.

PWA:
- /static/manifest.json declares a standalone install with theme colors.
- /static/sw.js — cache-first for static, network-first for HTML/API, with a
  graceful offline page. Registered from / scope via a dedicated /sw.js route
  that sets Service-Worker-Allowed: /.
- viewport + apple-touch-icon + theme-color meta tags in base.html.

Mobile pass on the chrome:
- Topbar wraps; nav horizontally scrolls instead of crowding; brand-sub hides.
- Tables (cases, ledger) scroll horizontally on narrow screens instead of
  exploding the layout.
- Hero / KPI / news-list layouts collapse cleanly at < 720px.

4 news tests; verified locally — home page renders, /sw.js serves with
Service-Worker-Allowed: /, manifest is valid JSON.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:18:40 +02:00
m17hr1l
7a57a7390a stage-29 fix: inference service — wire build: directive in compose
The inference service declared 'image: psyc-trainer' but no build: stanza,
so 'docker compose build inference' was a silent no-op and 'compose up
inference' tried to pull from a registry that doesn't exist (denied).
Added build context pointing at Dockerfile.train so future production
deploys can rebuild it via the compose lifecycle instead of needing a
manual 'docker build' on the side.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:08:50 +02:00