Commit Graph

35 Commits

Author SHA1 Message Date
Indiana
f8ca4bbd9e fix: unbounded essence farming, WS crash, and two economy races
Unbounded essence/item farming: every other reward trigger (summon,
question, fragment) had both a per-user and per-IP limiter, but
ritual_start and judgment had none at all — and judgment has no
"already resolved" state either. A scripted client could replay
{"type":"judgment","verdict":"cross_over"} in a tight loop and mint
CROSS_OVER_ESSENCE (25) plus a 20% item roll every iteration, forever.
Same for ritual_start -> 4x ritual_step. Added ritual/judgment limiters
in both flavors, matching the existing pattern.

WS session crash: _handle_question did `if state.entity is None:
await _handle_summon(state)` then `assert state.entity is not None`.
_handle_summon returns early *without* setting state.entity when the
seeker is rate-limited, so the assert fired unhandled — and the message
loop only catches WebSocketDisconnect, so it killed the whole connection.
Reachable with no malice: click summon a few times impatiently, then ask a
question. Now returns cleanly (the rate_limited frame was already sent).

Essence double-spend: purchase_unlock() deliberately uses SELECT ... FOR
UPDATE to serialize concurrent purchases, but the three credit_essence
call sites in ws.py did an unlocked db.get() read-modify-write. An
unlocked read doesn't block on a row lock, so a reward computed from a
pre-purchase balance could be written after the purchase committed,
silently reverting the deduction — user keeps the unlock and the essence.
All three now lock the row the same way.

Entity mint collision: _summon does a racy check-then-insert against
Entity.signature and Entity.name, both DB-unique, with no IntegrityError
handling — a concurrent mint of the same signature crashed the session.
Forceable by a user with two accounts (anomaly frequency/magnitude are
client-controlled), and plausible without malice in wire mode, where
sample_network() reads host-wide /proc/net/dev counters so two idle
sessions genuinely measure the same traffic. Now retries once, which
re-runs the match against whatever the winner committed.

Also added a unique constraint on unlocks(user_id, unlock_key) as
defense-in-depth, with an idempotent catalog-guarded migration.

221 backend tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:30:21 +00:00
Indiana
7ebedf363b Merge Workstream B: ritual + judgment + favor + cross-over 2026-07-24 21:30:42 +00:00
Indiana
36c4a9e6e4 feat: ritual + judgment + favor + cross-over (Workstream B)
Implements Workstream B of the character-depth-ghost-log spec:
ritual_start/ritual_step/judgment WS handlers, the pure judgment.py
logic module, and the User.favor / Entity.at_peace columns + migration.

- app/judgment.py: pure ritual success roll (base 65%, floored at 30%,
  driven by an entity's power+deceptiveness difficulty), the "stuck
  spirit" cross_over rule (alignment >= 0.5 and volatility > 0.6, ~20%
  of entities), judgment correctness/favor-delta/essence-delta/
  consequence resolution for all four verdicts, favor clamping, the
  favor-to-trait-roll bias applied at mint time, and tell-line
  generation (opaque behavioral flavor text, never a raw stat).
- app/ws.py: wires ritual_start/ritual_step/judgment frames, emits
  ritual_complete/tell/judgment_result/item_drop per the spec's
  Contract; traits are added to serialize_entity for internal
  server-side use but stripped from the outbound `entity` frame via a
  new _public_entity helper so hidden ground truth never reaches the
  client outside ritual_complete; _summon excludes at-peace entities
  from signature re-contact and mints a fresh (salted-signature) entity
  instead; new entities' traits are nudged by the discovering user's
  favor before being persisted.
- models/user.py, models/entity.py, main.py: User.favor and
  Entity.at_peace columns plus their idempotent ADD COLUMN IF NOT
  EXISTS migration lines in lifespan, alongside the existing ones.
- tests/test_judgment.py, tests/test_ws_ritual_judgment.py: 56 new
  tests covering the ritual/judgment correctness matrix, favor
  clamping/bias, essence crediting, at_peace persistence + re-contact,
  and the entity-frame trait leak guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 21:27:30 +00:00
Indiana
2f4832a3a8 Wire Workstream K's summon-pipeline integration into G's ingestion handoff
Both G and K were built independently against the paper contract and
correctly left this connection point for the integrator (documented in
both their reports). Calls process_device_reading_for_summon from
_process_reading, skipping array-valued readings (no defined single
scalar baseline for those). Adds an end-to-end integration test that
connects a real /ws/session, posts device telemetry through it, and
confirms an anomalous reading actually reaches the live session as an
anomaly_ack — proving the two independently-built pieces genuinely
connect, not just that each compiles.

165/165 backend tests pass.
2026-07-24 20:30:42 +00:00
Indiana
f023b591b5 Merge Workstream K: hardware anomalies feed the summon pipeline 2026-07-24 14:57:14 +00:00
Indiana
ec3bdc086e Merge Workstream G: device pairing + ingestion + live broadcast
Resolved conflict in main.py: combined both workstreams' router imports
and registrations (device_router from G, inventory_router from C).
143/143 backend tests pass after cleaning stray pollution from an
earlier parallel workstream run against the shared test DB.
2026-07-24 14:57:10 +00:00
Indiana
f633408a16 Merge Workstream C: unlocks + inventory items + sigils + essence 2026-07-24 09:41:55 +00:00
Indiana
ff68379772 feat: unlocks, inventory items, sigils, drops, and essence (Workstream C)
Implements the backend REST surface and WS wiring for
docs/superpowers/specs/2026-07-23-character-depth-ghost-log-design.md's
Workstream C:

- New models: UnlockRecord (unlocks), InventoryItem (inventory_items),
  Sigil (sigils) — brand-new tables, picked up by main.py's existing
  create_all.
- New app/inventory.py: unlock price table, item drop table/odds,
  essence economy constants, sigil design validation, and an atomic
  (row-locked) purchase_unlock() that guards against double-spend races.
- New app/routes/inventory.py: GET unlocks/items/sigils, POST sigils
  (validates the placeholder {points, rune} shape, points capped at 12),
  POST unlocks/{unlock_key} (402 on insufficient essence, 404 on unknown
  key, idempotent re-buy).
- GET /auth/me now includes unlocks: list[str] and essence: int.
- ws.py: wires essence trickle + item_drop rolls into the one trigger
  point that exists in this worktree today (_handle_summon, covering
  every successful summon plus high-rarity summons); the other two
  contract trigger points (correct judgment, successful ritual) belong
  to Workstream B's not-yet-landed ritual/judgment WS handlers, which
  should call app.inventory's same helpers once they land.
- User.essence: int added (Workstream B owns this column per the spec;
  added here per orchestrator instruction so this workstream is
  independently testable — merge controller reconciles the duplicate
  edit).

Also fast-forwarded this worktree's branch onto master (it had fallen
behind several commits) so the files this workstream depends on
(shop.py, ws.py, entities.py, etc.) were actually present to build
against.

Tests: 109 passed (drop-roll statistical sanity with seeded RNG,
inventory/sigil CRUD, purchase success/insufficient-funds/idempotency/
unknown-key paths, /auth/me shape, ws summon-trickle and item-drop
wiring).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 03:02:20 +00:00
Indiana
004f66171f feat: hardware sensor anomalies feed the summon pipeline (Workstream K)
Adds backend/app/device_anomaly.py — per-(user_id, device_id, sensor_type)
rolling-baseline anomaly detection for continuous numeric sensors
(structurally modeled on telemetry.detect_wire_spike: min samples, an
absolute floor, 3-sigma + relative threshold, with per-sensor-type floors
since units vary wildly) plus a false->true state-transition detector for
discrete/boolean sensors like presence.

Adds a module-level active-session registry in app/ws.py
(register_active_session/unregister_active_session/get_active_session)
so hardware ingestion (a plain HTTP call, not a WS connection) can find a
user's live SeanceState.

process_device_reading_for_summon(user_id, device_id, sensor_type, value,
unit) is the self-contained entry point Workstream G's ingestion handler
will call into: classifies numeric vs. boolean, runs the reading through
the right detector, and on a genuine anomaly pushes it into the active
session via the existing _handle_anomaly path (source=sensor_type,
frequency=stable per-sensor-type constant, magnitude=deviation-from-
baseline or a fixed constant for boolean transitions) — reusing the full
existing signature/mint/Codex pipeline, no new mint logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 01:14:11 +00:00
Indiana
c88fbc843a feat: device pairing, telemetry ingestion, live dashboard WS (Workstream G)
Implements the backend half of the ESP32-P4 sensor node spec's pairing,
ingestion, and live-broadcast contract:

- New Device model (backend/app/models/device.py): id, user_id FK, name,
  token_hash (unique+indexed), created_at, last_seen_at. Reuses
  generate_session_token()/hash_token() from auth_session.py verbatim for
  the one-time raw pairing token / stored hash.
- POST /api/device, GET /api/device (session-cookie authenticated REST
  pairing endpoints) and POST /api/device/telemetry (device bearer-token
  authenticated ingestion, per-device rate limited, 16KB body cap, 64
  reading cap, strict shape validation — never a 500 on garbage input) in
  backend/app/routes/device.py.
- /ws/device-feed live dashboard WS (qm_session cookie authenticated),
  fanning out ingested readings to the owning user's connected dashboard
  sockets via an in-process dict[user_id, connections] registry, each with
  its own send-queue + single sender task (mirrors app.ws's
  SeanceState/_sender convention).
- last_seen_at updates on every successful ingestion.
- _process_reading(device, reading) left as an explicit no-op handoff point
  for Workstream K's summon-pipeline integration.

Backend suite: 102 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 01:12:21 +00:00
Indiana
e5bc253105 feat: add hidden entity traits (Workstream A) + idempotent migration
Entity.traits (alignment/power/volatility/deceptiveness, 0.0-1.0 each) is
rolled once at mint time in entities.py, seeded from the entity's
signature via random.Random(f"traits:{signature}") — a separate rng
namespace from normalize_profile's existing "norm:" rng, and never fed
into mint_prompt, so persona text stays fully decoupled from ground
truth. normalize_profile now includes "traits" in its returned dict;
fallback_profile inherits it for free since it already delegates to
normalize_profile.

Adds the new JSONB column to the Entity model (default {}) and the
idempotent `ALTER TABLE entities ADD COLUMN IF NOT EXISTS traits ...`
migration line to main.py's lifespan, per the live-Postgres migration
convention this spec introduces (no Alembic in this repo).

Tests cover trait value ranges, signature-determinism, and
persona/trait independence (same persona template pairs with a wide
spread of alignment rolls across signatures), plus a regression check
that mint_prompt's signature never grows a traits parameter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:15:25 +00:00
Indiana
58c30b7273 feat: possession presentation layer — glitchy text + degraded audio
First sub-project of the "make contact feel real" arc (spec:
docs/superpowers/specs/2026-07-23-possession-presentation-design.md).
Direct Contact replies now feel like a spirit fighting through static to
hold the channel rather than a plain chat bubble:

- backend/app/possession.py: compute_stability(rarity, magnitude, rng) —
  a 0.05-0.98 score per reply (rarer entity + stronger triggering anomaly =
  cleaner signal), rng injectable for a later quantum-RNG source.
- ws.py sends stability on reply_start; audio synthesis for that reply gets
  noise/bitcrush scaled by instability (1 - stability) via a new
  instability param on synthesize_spirit_voice — effects.py itself is
  untouched, only the params fed into it.
- frontend/src/lib/possession.ts: renderPossessedText — pure, deterministic
  (tick-seeded, no Math.random) text corruption with self-correcting
  glitch bursts, wired into Transcript.tsx's streaming reply display.

Stored transcript/reply text is unaffected — this is presentation only.
78/78 backend, 137/137 frontend tests passing.
2026-07-23 05:48:02 +00:00
Indiana
761d6171b5 fix: actually strip stale SESSION_SECRET mentions from README
The gap-g-readme merge commit (57a8914) staged this fix but never
re-staged it after editing, so the merge landed with the pre-fix content —
the working tree had the correction but git didn't. No functional change,
just closing the gap between what was intended and what was committed.
2026-07-23 03:32:21 +00:00
Indiana
8abc06e510 Merge gap-b-per-ip-limit: per-IP WS rate limiting via CF-Connecting-IP 2026-07-23 00:33:28 +00:00
Indiana
b57045eabf Merge gap-c-session-cleanup: periodic expired-session sweep 2026-07-23 00:33:26 +00:00
Indiana
6399039589 fix: resolve real visitor IP via CF-Connecting-IP for per-IP limiting
websocket.client.host is always the Cloudflare Tunnel machine's LAN IP for
every internet-facing connection (the tunnel runs on a separate machine and
terminates TLS there), which collapsed per-IP rate limiting into a single
shared bucket for all remote visitors — the exact gap flagged in review.

Cloudflare's edge sets CF-Connecting-IP itself, overwriting any
client-supplied value, so it's safe to trust when present. Falls back to
the raw socket peer for direct LAN/local access.
2026-07-22 23:59:35 +00:00
Indiana
83575f9bb3 feat: add per-IP rate limiting to WS LLM triggers
Per-account limits alone don't stop one account script-hitting
Ollama from many source IPs. Adds matching per-IP limiters
alongside the existing per-account ones for fragment/question/
summon triggers, per spec's dual per-account-and-per-IP requirement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-21 03:45:58 +00:00
Indiana
7f0775c8c3 feat: periodically sweep expired auth sessions
auth_sessions rows were never deleted after expiry, only rejected
on read. Adds a background sweep (every 30 min) in the app lifespan,
plus a tested pure delete_expired_sessions() function.
2026-07-21 03:43:34 +00:00
Indiana
d2f4c0a993 fix: remove unused SESSION_SECRET config
Session security already comes from a cryptographically random
256-bit token (secrets.token_urlsafe) hashed before storage —
SESSION_SECRET was required config that nothing ever read.
2026-07-21 03:43:08 +00:00
Indiana
0756e677b9 Add EMF field mode, Armory shop/waitlist, and ambient haunting layer
Three self-contained features, verified complete and cross-wired
end-to-end (audited: backend 54/54 tests, frontend 110/110 tests,
tsc --noEmit clean, i18n coverage script clean):

- EMF mode: DeviceMotion/DeviceOrientation-based field-meter sensing,
  a fifth séance channel alongside Wire/EVP/Radio/Ouija, with its own
  fragment prompt persona and full frontend gauge UI.
- Armory (shop/waitlist): pre-order capture page for the future
  Ultimate Quantum Box hardware line, rate-limited public endpoint,
  explicitly no payment collection.
- Haunting layer: ambient possession effects (dread-bed audio, title
  glitching, idle-paced whispers/manifests), respects
  prefers-reduced-motion, mounted once at the app root.

Plus WebUSB robustness fixes in lib/sdr.ts (Terratec vendor ID,
explicit selectConfiguration, isSecureContext gate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-21 02:28:14 +00:00
Indiana
c372427ced feat: matrix/graph data views, wire-generated anomalies, http cookie fix
- auth: session cookie Secure only over https — plain-http LAN access was
  silently dropping the cookie, killing WS auth ('connection unstable')
- wire ghost: server-side spike detection on jitter baseline (3σ + 2.5×mean,
  20KB/s floor, 20s throttle) — wire anomalies now flood every session with
  zero hardware, pushed to clients as {type:'anomaly'} frames
- telemetry cadence 3-5s for live graphs; ambient whispers unchanged
- frontend: MATRIX view (data-rain interleaved with live utterances/anomaly/
  telemetry strings), GRAPHS view (scrolling jitter/variance/dns lines +
  anomaly markers + counters), BOARD/MATRIX/GRAPHS switcher
- connection banner: 'connecting' is now neutral 'tuning the veil…', only
  unstable/closed warns
- db: recreated quantumancy(+_test) as UTF8 (was SQL_ASCII — crashed on
  non-ASCII spirit text); README quickstart updated
- i18n: seance.views.* EN/ES
2026-07-21 00:02:41 +00:00
Indiana
b9110f45de feat: spirit engine — seance WS, entity minting/Codex, Piper TTS voices, wire telemetry
- WS /ws/session: modes, summon, anomaly fragments, streaming direct contact,
  passive wire-ghost ambient loop, per-user rate limits, event transcript
- entities: anomaly-signature fingerprinting, LLM minting + procedural
  fallback, Codex matching with contact counts and sightings
- tts: 8 local Piper voices (EN/ES), per-entity voice profiles, numpy
  effects chain (pitch/rate/bitcrush/echo/static)
- llm: streaming client, submit_stream in bounded queue, SpiritService
  with offline fallbacks for every channel
- routes: public /api/codex, /api/codex/{id}, /api/stats; /audio static mount
- models: Entity, EntitySighting, Event, ContactSession(entity_id, language)
2026-07-20 20:13:28 +00:00
Indiana
52afad1ad5 feat: add Ollama client and bounded request queue 2026-07-20 18:01:36 +00:00
Indiana
cd9fc49eed fix: prevent app crash when frontend build is missing
StaticFiles defaults to check_dir=True, which raises at import time if
frontend/dist/assets is missing on restart — taking down /healthz and
/auth/* along with the frontend. Pass check_dir=False so the mount never
crashes the app, and make the SPA fallback return a clear 503 instead of
an unhandled 500 when index.html is absent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-20 17:52:43 +00:00
Indiana
bd0c162172 feat: serve built frontend from backend with SPA fallback 2026-07-20 17:48:07 +00:00
Indiana
b8168b69a1 test: cover rate limiter eviction path with mocked time
Added test_hits_expire_after_window_elapses() which uses unittest.mock.patch
to deterministically advance time and verify that expired hits are evicted from
the rolling window. This exercises the while loop in RateLimiter.allow() that
was previously untested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-20 15:52:03 +00:00
Indiana
53828dad19 feat: add rate limiter utility 2026-07-20 15:47:56 +00:00
Indiana
10ac364a90 fix: use https base_url in test client so Secure cookies propagate automatically
httpx's cookie jar only auto-attaches Secure cookies to https:// requests.
Switching the ASGITransport client fixture's base_url from http://test to
https://test (no real socket is opened either way) makes it behave like a
browser talking to the Cloudflare-Tunnel-terminated HTTPS edge in
production, eliminating the need for manual client.cookies.set(...)
re-injection workarounds in test_auth.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-20 15:44:01 +00:00
Indiana
3758594896 fix: harden login/logout — secure cookie, server-side session revocation, timing-safe login
Addresses three Important-severity review findings inherited from Task 4's
plan reference code:

- login() now sets secure=True on the session cookie (safe behind the
  Cloudflare Tunnel, which terminates TLS at the edge).
- logout() looks up and deletes the matching AuthSession row before
  clearing the cookie, so a leaked raw token can no longer be replayed
  after logout.
- login() always performs exactly one verify_password call regardless of
  whether the username exists (against a module-level dummy hash for
  nonexistent users), removing the timing oracle that let unauthenticated
  requests distinguish registered from unregistered usernames.

Adds two tests: nonexistent-username login rejection, and logout revoking
the session server-side. Also adjusts two cookie-propagation touch points
in test_auth.py to manually re-inject the qm_session cookie, since
httpx's cookie jar won't auto-attach a Secure cookie to the test
transport's plain http://test base_url (a real browser talking to the
HTTPS tunnel edge wouldn't have this problem).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-20 15:34:12 +00:00
Indiana
a0723b5237 feat: add login, session cookies, and get_current_user 2026-07-20 15:26:58 +00:00
Indiana
fa725f1a0e fix: move NullPool test fix from db.py into a conftest-local test engine
Production code shouldn't branch on `"pytest" in sys.modules` — it's
fragile and couples db.py to test tooling. Instead, conftest.py now
builds its own dedicated NullPool engine directly from settings, used
only for the drop_all/create_all reset and the get_db override. The
production engine in app/db.py is untouched and never exercised during
tests, so this fully preserves the event-loop fix while keeping prod
code test-agnostic.
2026-07-20 15:22:41 +00:00
Indiana
cfffc7ff59 feat: add user model and registration endpoint 2026-07-20 15:14:31 +00:00
Indiana
9e69c622c9 feat: add settings and async db engine 2026-07-20 15:01:25 +00:00
Indiana
1747c355e0 fix: restore .worktrees/ entry in .gitignore and add backend/tests/__init__.py
- Restored .worktrees/ line that was lost when .gitignore was overwritten in commit 1736dd0
- Added backend/tests/__init__.py as a package marker (was untracked on disk)
- Test suite verified: backend tests pass (1/1)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 14:57:32 +00:00
Indiana
1736dd021b chore: scaffold backend venv, health check, systemd unit 2026-07-20 14:52:53 +00:00