omnixient/docs/system-map.md
2026-06-28 10:00:14 +02:00

299 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# aiolabs system map
The orientation doc for someone returning to this stack after a
break — or arriving fresh. Where everything lives, how the pieces
fit together, who holds which keys, where to start for common
tasks.
For omni-the-desktop specifically (Hyprland, themes, mksystem,
flake structure), see [`ARCHITECTURE.md`](ARCHITECTURE.md). For
fleet deploy mechanics, see [`deploy-strategy.md`](deploy-strategy.md).
This doc is the **cross-project** map.
---
## The intent
The aiolabs stack is a self-hosted, Bitcoin/Lightning- and Nostr-
native ecosystem built around the operational needs of **Château du
Faune** — a collective farm + artist residency in Ariège, France —
designed from the start to be adopted by other communities with
similar shape (co-ops, intentional communities, small-scale
agriculture, retreat centers).
The principles:
1. **Self-hostable end to end.** No SaaS dependencies in the critical
path. Every running service has a corresponding entry in `aiolabs/
server-deploy` and can be reproduced on commodity hardware.
2. **Open protocols over proprietary schemas.** Nostr (NIP-52,
NIP-72, NIP-46), iCalendar VTODO, ActivityStreams. Other
communities can adopt the same shape; their renderers and ours
interoperate by virtue of being on the same wire.
3. **Internal-network defaults.** The community-organizer
capabilities work during WAN outages — `docs.ariege.io`, the
Nostr relay, LNbits, and the maubot daemon all colocate on
`host1` so the foyer e-ink panel and the Matrix bots keep
functioning without the internet.
4. **Identity is the user's, not the operator's.** Per-user Nostr
identity, held by a sidecar nsec bunker, with per-device scoped
tokens for every client that signs on the user's behalf. See
*Trust boundaries* below.
---
## Component map
```
───── aiolabs stack ─────
┌─────────────────────────────────────────────────────────────────┐
│ DEV BOX (this machine — bohm) │
│ │
│ /etc/nixos (omni) ──── this machine's NixOS + home-manager │
│ ~/dev/ ────────────── worktree of every project repo │
│ │
└────────────────────────────────────┬─────────────────────────────┘
│ deploys via
┌─────────────────────────────────────────────────────────────────┐
│ FLEET HOSTS (via server-deploy) │
│ │
│ host1 (Château du Faune) host2 host4 │
│ • Continuwuity • dev • staging │
│ (Matrix homeserver) • lnbits • webapp demo │
│ • maubot daemon │
│ • LNbits + nsecbunkerd │
│ • Nostr relay │
│ • castle-docs (Quartz) │
│ • nginx + sops │
│ │
│ host5 host3 host6 (WG hub) │
│ production-tier hosts │
└────────────────┬──────────────────────────────┬─────────────────┘
│ │
▼ ▼
┌────────────────────────────┐ ┌──────────────────────────────┐
│ USER-FACING SURFACES │ │ PHYSICAL DEVICES │
│ │ │ │
│ webapp (PWA) │ │ inky-impression (Pi + eink) │
│ activities / wallet │ │ foyer display │
│ forum / market / chat │ │ │
│ │ │ bitSpire ATM │
│ Matrix client (Element) │ │ Nostr-native kiosk │
│ │ │ │
│ docs.ariege.io │ │ tufty-badge, InkyImpression │
│ (Quartz static site) │ │ LivestockGuardDogs │
│ │ │ standalone hardware │
└────────────────────────────┘ └──────────────────────────────┘
```
### What lives where in `~/dev/`
| Path | Role | Where to read more |
|---|---|---|
| `deploy/server-deploy/` | NixOS flake for the fleet — single source of truth for what runs on which host | `flake.nix` + per-host modules |
| `lnbits/main/`, `lnbits/dev/`, `lnbits/nostr-transport/` | Lightning + Nostr identity backend | `lnbits/main/CLAUDE.md`; per-extension READMEs |
| `webapp/main/`, `webapp/dev/` | Vue/Vite/Electron PWA — user-facing apps over Nostr | `webapp/main/CLAUDE.md` |
| `bitspire/bitspire/{main,dev}`, `bitspire/atm-tui/` | bitSpire — KYC-free, Nostr-native Bitcoin ATM frontend (+ atm-tui) | `bitspire/bitspire/CLAUDE.md` |
| `maubot-plugins/` | Matrix bot plugins: `journal`, `tracker`, `wiki` | `maubot-plugins/CLAUDE.md`; `maubot-plugins/docs/community-organizer-spec.md` |
| `inky-impression/` | E-ink renderer for the foyer panel | `inky-impression/CLAUDE.md` |
| `docs/castle-docs/` | Operational wiki (Quartz, Obsidian-flavored markdown) | `docs/castle-docs/CLAUDE.md` |
| `lnbits-extensions/` | The aiolabs LNbits extension catalog (`extensions.json`) | `~/dev/CLAUDE.md` — see "Forgejo conventions" |
| `shared/extensions/` | LNbits extension source tree (mounted into dev compose) | Per-extension READMEs |
| `local/docker/regtest/` | Local Lightning regtest stack for end-to-end testing | `local/README.md` |
| `refs/` | Curated mirrors of upstream reference codebases (Nostr NIPs, Khatru, nostr-tools, etc.) | `refs/README.md` |
| `upstream-prs/` | Branches staged for upstream contribution | per-branch context |
### Per-project CLAUDE.md (defer to these for repo-specific work)
The cross-cutting facts live in `~/dev/CLAUDE.md`. Anything more
specific belongs in the per-project `CLAUDE.md`. Start at the most
specific one for your task and walk outward only if the answer isn't
there.
---
## The end-to-end data flow
This is the picture once everything's wired up. Some pieces (per-user
signing) are in flight — see *Status snapshot* below.
```
USER (in a Matrix room)
│ !task fix the south fence #urgent
Continuwuity homeserver ─────── Matrix federation
maubot daemon (tracker plugin)
│ resolve binding: MXID → LNbits user
│ resolve signer: LNbits → bunker URL + scoped token
│ build NIP-52 event (kind 31922, community a-tag)
│ NIP-46 RPC to nsecbunkerd ──→ sign as user
Internal Nostr relay (loopback / wss://lnbits.<domain>/nostrrelay)
│ fan out to public relays (relay.ariege.io, …)
PUBLIC NOSTR
├─── inky-impression subscribes ──→ renders foyer scene
├─── webapp subscribes ──→ shows user's items in PWA
└─── any other subscriber ──→ mobile push, etc.
```
The community-organizer protocol that defines these event shapes is at
`~/dev/maubot-plugins/docs/community-organizer-spec.md`. It's
runtime-agnostic — other projects can produce the same NIP-52 events
from any source (CLI, web form, voice assistant) and the renderers
don't care.
---
## Trust boundaries (where keys live)
Knowing what's compromised when X is compromised is the first thing
to need when something breaks. The honest picture:
| Key | Where it lives | Compromise impact | Reference |
|---|---|---|---|
| **User target keys** (`X_alice`, `X_bob`, …) | `nsecbunkerd` sidecar on the LNbits host, encrypted at rest with a passphrase the operator supplies at boot | Catastrophic for user identity *if* the bunker itself is breached. Bunker has a small attack surface (no web server, no plugins). | `aiolabs/lnbits#9`, `#18` |
| **Per-device scoped tokens** (tracker, ATM, webapp, …) | The client device that holds them. Tracker holds them in its SQLite. Webapp holds them in IndexedDB/localStorage. ATM holds them in its config. | Bounded by token scope. A compromised tracker can publish NIP-52 events as the user; nothing else. Revoke the token at the bunker side; target key unaffected. | spec §7.3 |
| **Operator master `M_lnbits`** | `nsecbunkerd` admin key. Used by LNbits to authorize admin RPCs (create-account, issue-token, revoke). | Full identity-pool admin access. Operator is on the hook. | `aiolabs/lnbits#18` |
| **LNbits agent npub** | LNbits process memory, sops-managed at rest | Allows the holder to call `create_account` / `create_token` / `revoke_user` — but NOT to sign as users directly (no nsec). Bounded compared to holding the master. | `aiolabs/lnbits#18` admin client section |
| **maubot bot keypair** (`@trackerbot:ariege.io`) | maubot's sops-managed config on host1 | Can publish events under the bot's own pubkey (used as fallback when a user is unbound). No access to user signing. | `server-deploy/modules/services/maubot.nix` |
| **Server identity (`NOSTR_TRANSPORT_PRIVATE_KEY`)** | LNbits env var, sops-managed | Allows impersonation of the LNbits instance itself on the Nostr transport channel. Migrating to bunker-signed in #18. | `aiolabs/lnbits#4`, `#9` |
| **Wireguard / SSH host keys** | Per host, NixOS-generated | Lateral movement between fleet hosts. Operator concern. | `server-deploy/hosts/<host>/` |
### What's public vs community-private
| Channel | Audience |
|---|---|
| `wss://relay.ariege.io` | Public Nostr — anyone can subscribe. Community-organizer events for outward-facing rooms land here. |
| `wss://lnbits.ariege.io/nostrrelay/<channel>` | Auth-gated internal relays. `nsecbunker` channel is bunker RPC; other channels carry community-scoped events that shouldn't leave the host. |
| Matrix rooms | Federated by default. Set rooms to invite-only for community-private discussions; the bot reads room membership as trust boundary. |
| LNbits API | Per-user auth (NIP-98 or session). Bot agent has its own scoped credentials. |
| `docs.ariege.io` | Public read; published Quartz site. Castle-docs `private/` directory is git-ignored. |
---
## Cross-repo dependencies
When you change repo A, repo B may need a follow-up to actually
deploy. The chain:
```
~/dev/<project>/ changes go here first
`aiolabs/<project>` (forgejo) push to the canonical remote
`aiolabs/server-deploy` flake.lock bump the input pointing at <project>
│ (`nix flake lock --update-input <project>`)
deploy.sh <host> rolls the change to the host
```
Catalogs (LNbits extension catalog, etc.) are the exception — they
read live from `aiolabs/lnbits-extensions/extensions.json` at install
time, so no flake.lock bump is needed there. See
`~/dev/CLAUDE.md` "Forgejo conventions" for the per-repo specifics
(direct-to-main vs PR, fork versioning scheme, catalog rules).
---
## Status snapshot (2026-05-26)
Not everything in the data-flow diagram is shipped. The current state:
| Capability | Status |
|---|---|
| Matrix + Continuwuity homeserver on host1 | ✅ live |
| `journal` maubot plugin (`@journalbot:ariege.io`) | ✅ live |
| `tracker` maubot plugin (Phase 1 — Matrix + SQLite, rules classifier) | ✅ in repo, ready to deploy |
| `wiki` maubot plugin (HTTP search against `docs.ariege.io`) | ✅ in repo, ready to deploy |
| Community-organizer protocol spec | ✅ at `~/dev/maubot-plugins/docs/community-organizer-spec.md` |
| LNbits Nostr-transport (HTTP-free API) | ✅ merged (#4) |
| LNbits signer abstraction + transitional LocalSigner | ✅ Phase 1 of #9 — PR #17 (open, awaiting cascading merge) |
| **`nsecbunkerd` aiolabs fork functional** | ✅ Fork at `aiolabs/nsecbunkerd@master`; 4 upstream-rot patches applied (`#1``#4`, `#8`); native nix flake at commit `711a017`; 4 known upstream issues still open (`#5`/`#6`/`#7`/`#8` echo + multi-arch + getKeys) |
| **LNbits bunker provisioning primitive (phase 2.1)** | ✅ PR #19 (open, stacked on #17) — `NsecBunkerAdminClient` + `RemoteBunkerSigner`; live `ping` + `create_new_key` round-trip ~30ms |
| LNbits NIP-46 `sign_event` over bunker (phase 2.2-2.3) | 🚧 blocked on upstream `aiolabs/nsecbunkerd#4`/`#7` stabilizing |
| `error_code` wire vocabulary (14 codes, ATM ↔ LNbits) | ✅ agreed; lands alongside phase 2.3 — see `~/dev/coordination/log.md` |
| Extension `account.prvkey` migration to `resolve_signer` | 🚧 pre-cascade prerequisite for PR #17 — umbrella at `aiolabs/lnbits#21`; per-extension issues filed (`nostrmarket#5`, `restaurant#11`, `tasks#3`, `events#23`, spirekeeper TBD) |
| Tracker Nostr publish bridge (Phase 2a) | ⏸ waiting on bunker phase 2.3 |
| Per-user signing via bunker (Phase 2c) | ⏸ waiting on bunker phase 2.3 |
| `inky-impression` Nostr-native scene plugin | ⏸ waiting on tracker publishing |
| Webapp Nostr identity via NIP-46 | ⏸ waiting on bunker phase 2.3 |
### 2026-05-26 day's shipping
- **Cross-session coordination log** established at `~/dev/coordination/log.md` (protocol in sibling `README.md`). Replaces user-relayed messages between the parallel `lnbits`, `bitspire`, `spirekeeper`, and `alfred` sessions. Append-only, chronological.
- **LNbits PR #19** opened — operator-IdP signup primitive via `aiolabs/nsecbunkerd`. Stacked under PR #17 awaiting cascading merge.
- **`aiolabs/nsecbunkerd`** — 4 upstream-rot patches landed on `master` so the fork builds + runs from a clean clone (`06272c8`, `960b939`, `42dbbd7`, `e39eaa6`); 8 issues filed total documenting the upstream gaps.
- **`aiolabs/lnbits#20`** — outbound `_fanout_payment` defense-in-depth dedup (low priority, symmetric to `aiolabs/bitspire#50`).
- **`aiolabs/lnbits#21`** — umbrella audit catching 5 extensions still reading `account.prvkey` directly. Each affected ext now has its own tracking issue (or pre-existing for `nostrmarket#5`).
- **Canonical sat-amount vocabulary** locked across spirekeeper / bitspire / atm-tui: `wire_sats` / `principal_sats` / `fee_sats` / `fee_fraction` (unit fraction, never percentage). See `~/dev/coordination/log.md` 2026-05-26T17:10Z + 18:50Z entries.
- **`bitspire` #49 + #50** closed (Schnorr-verify on RPC replies + two-tier hash dedup on subscribe_payments callbacks). Symmetric lift on the bunker-client side landed in PR #19 commit `4ebcd959`.
---
## Start-here runbook
| I want to… | Read first |
|---|---|
| Add a new aiolabs project | `~/dev/CLAUDE.md` (workspace conventions); pick the repo's own CLAUDE.md once it exists |
| Deploy a change to a fleet host | `deploy-strategy.md` in this dir; `aiolabs/server-deploy` README |
| Add a new maubot plugin | `~/dev/maubot-plugins/CLAUDE.md`; the multi-line `@command.passive` footgun in `~/dev/CLAUDE.md` |
| Modify a verb / event shape | `~/dev/maubot-plugins/docs/community-organizer-spec.md` **first**, then the plugin code |
| Add a page to the operational wiki | `~/dev/docs/castle-docs/CLAUDE.md`; commit + push triggers Quartz rebuild |
| Render something on the foyer e-ink panel | `~/dev/inky-impression/` (currently HTTP-screenshot; Nostr scenes are planned) |
| Fork or bump an LNbits extension | `~/dev/CLAUDE.md` "Extension version-bump procedure" |
| Add a NixOS option / tool on this dev box | `/etc/nixos/home.nix` or `/etc/nixos/configuration.nix`, then `omni-rebuild` |
| Where are user nsecs? | `nsecbunkerd` on the LNbits host. See `aiolabs/lnbits#9`, `#18`. **Not in lnbits's DB.** |
| Where do connection tokens live? | The client device (maubot SQLite, webapp localStorage, ATM config). Revoke at the bunker. |
---
## Glossary
A few terms recur across the stack:
- **IdP** — identity provider. In our stack, LNbits is the IdP for end
users; it brokers token issuance against the sidecar bunker.
- **Bunker** — `nsecbunkerd` (or any NIP-46-compliant sidecar) that
holds the actual nsec material. Speaks kind-24133 (signing) and
kind-24134 (admin) over a relay.
- **Target key** — a per-user nsec held inside the bunker. Addressable
by its npub; never leaves the bunker.
- **Operator master `M_lnbits`** — the bunker's admin key. Used to
authorize admin RPCs (create-account, issue-token, revoke).
- **Connection token** — a per-device NIP-46 credential, scoped to
specific event kinds, issued by the bunker to a specific client
(tracker, webapp, ATM). Compromise leaks only the scope.
- **Signer abstraction** — the `NostrSigner` ABC in
`lnbits/core/signers/` and mirrored in tracker. Four implementations:
`BotSigner`, `RemoteBunkerSigner` (steady state), `ClientSideOnly
Signer` (sovereignty exit), `LocalSigner` (transitional migration
helper).
- **Community a-tag** — NIP-72 community reference, `34550:<pubkey>:
<d-tag>`. The way every community-organizer event scopes itself to
a community.
- **Capture / clarify** — GTD framing. `!add` is capture (frictionless,
inbox); the rules classifier (or future LLM) does the clarify step
that sorts into a kind.
- **Universal verb vs community shortcut** — the spec mandates universal
verbs (`!add`, `!task`, `!journal`, `!remind`, `!done`, `!list`).
Communities configure their own shortcuts (`!buy`, `!steward`, …)
per room.
- **Spec-first** — the working convention for cross-system features:
write the protocol doc before the implementation. See community-
organizer-spec.md as the worked example.