diff --git a/.gitignore b/.gitignore index 78a64dd4..7aacbbb6 100644 --- a/.gitignore +++ b/.gitignore @@ -93,17 +93,21 @@ scripts/resilience/reports/ Screenshot *.png uploads/ -# Operations docs — fleet runbooks, node inventories, internal trackers and -# session handoffs. Kept on disk for local use, deliberately NOT tracked: they -# describe live infrastructure (node addresses, hostnames, deploy procedures) -# and must never reach the public repo. See the ops-docs section of the -# open-source readiness plan. +# ── Local-only material ───────────────────────────────────────────────────── +# Present on disk, never tracked: everything describing Archipelago's own +# infrastructure or internal development process. The repo is source code and +# guidelines only. Inventory: .local-only/manifest.txt — wipe: .local-only/wipe.sh +/.local-only/ +/.planning/ +/loop/ /docs/operations-runbook.md /docs/hotfix-process.md /docs/PRODUCTION-MASTER-PLAN.md /docs/UNIFIED-TASK-TRACKER.md /docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md /docs/HANDOFF-2026-07-20-fips-peer-files.md +/docs/HANDOFF-2026-07-23-companion-apk-deploy.md +/docs/qr-scanner-snappiness-handover.md /docs/RETICULUM-TRANSPORT-PROGRESS.md /docs/combined-test-plan-2026-07-22.md /docs/pine-voice-release-test-plan.md @@ -113,3 +117,20 @@ uploads/ /docs/security/KEY-02-FLEET-ROTATION.md /docs/security/KEY-03-SIGNING-POSTURE.md /tests/production-quality/TRACKER.md +/scripts/deploy-config-defaults.sh +/scripts/deploy-tailscale.sh +/scripts/deploy-to-target.sh +/scripts/setup-target-dev.sh +/scripts/setup-aiui-server.sh +/scripts/setup-https-dev.sh +/scripts/debug-frontend.sh +/scripts/node-profile.sh +/scripts/fleet-fips-pair.sh +/scripts/fleet-fips-unpair.sh +/image-recipe/sync-from-live.sh +/docs/security/PHASE-10-VERIFICATION-GUIDE.md +/docs/security/KEY-01-ON-NODE-VERIFICATION.md +/docs/security/KEY-02-ROOTFS-EVIDENCE.md +/docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md +/image-recipe/INTEGRATION-GUIDE.md +/docs/multinode-testing-plan.md diff --git a/.planning/.continue-here.md b/.planning/.continue-here.md deleted file mode 100644 index 014f2a14..00000000 --- a/.planning/.continue-here.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -context: default -phase: 09-botfights-platform-upgrade (already complete — this is off-plan work) -task: n/a -total_tasks: n/a -status: paused -last_updated: 2026-08-02T10:34:47.198Z ---- - -# BLOCKING CONSTRAINTS — Read Before Anything Else - -- [ ] CONSTRAINT: Never assume pushing one repo pushed another — this session pushed `archy` repeatedly via `git push gitea-ai main`, but the `botfight` repo's last 4 commits (the entire security-fix body of work) sat **local-only** the whole time and were only discovered/pushed at the very end of this session, during this handoff step. Structural mitigation: whenever a session touches more than one git repo, explicitly run `git status -sb` (ahead/behind vs. the tracked remote) in **every** repo touched before ending the session — not just the one most recently `git push`ed. - -**Do not proceed until the box above is checked (i.e. verify both repos are still in sync with their remotes before doing anything else).** - - -This is **not** a GSD plan/task in progress. Phase 09 (BotFights Platform Upgrade) is fully complete — plans 09-01 through 09-07 all have SUMMARY.md files, the last dated 2026-07-31 05:08. Everything described below happened *after* that, as live, user-directed, reactive work preparing for a same-day BotFights demo ("two real fighters playing with cashu"). None of it was tracked against a PLAN.md task list — the original GSD task (execute 09-06-PLAN.md: bump manifest + sign catalog) completed normally and stopped cleanly at its signing checkpoint, exactly as designed. Everything after that was ad hoc. - -**As of this handoff, everything is committed and pushed in both repos, and both demo nodes are deployed and verified healthy.** There is nothing mid-flight to resume — this file exists so a future session (or this one, after compaction) has the full picture instead of re-discovering it. - - - - -**botfight repo** (`/home/archipelago/Projects/botfight`, pushed to `origin/main` @ `10d4209`): -- iframe embedding fix (X-Frame-Options was unconditional), native Archipelago signer bridge (`nostr-provider.js`), "Sign in with Archipelago" docs for app developers -- Discoverability fixes: mode-picker guide banner, AI-answer visibility, "Latest Bouts" cut off on short viewports -- Fixed a proxy-URL leak (local/Tailscale addresses leaking into AI setup prompts via client-side `window.location.origin` — switched to server-rendered `/api/docs/prompt`) -- "Let BotFights answer for me" — server-side AI bot using an operator-supplied Anthropic/OpenAI API key (poll-mode bots) -- Fixed broken profile images (CSP `img-src`) -- Cashu ecash payments made the **primary** entry-fee AND payout UX (Lightning/NWC now secondary) — Minibits mint, `BOTFIGHTS_WALLET_ENCRYPTION_KEY`, escrow-style entry fee (21 sats, 42-sat winner-take-all pot) -- Fixed anonymous poll-mode bots being locked out of staked/ranked fights (auth gap) -- **Security audit found + fixed 6 instances of the same IDOR pattern** (client-supplied `pubkey` trusted with no verification against a real JWT) — `f5f57e6`, `c162d5e`: - - `POST /api/auth/update` — could hijack any bot's webhook/customization - - `GET /api/payments/winnings/:botId` — **critical**: zero auth at all, leaked live spendable Cashu bearer tokens to anyone who knew a botId (public in every URL) - - `POST /api/payments/connect-wallet` — **critical**: zero ownership check, could redirect any victim bot's future payouts to an attacker's wallet - - `POST /api/payments/claim/:paymentId`, `DELETE /api/payments/disconnect-wallet`, `POST /api/queue/join-ranked/:botId` — same pattern, lower severity - - Fix pattern: pubkey now always derived from `extractPubkeyFromAuth(Authorization: Bearer )`, never trusted from body/query. Added `verifyBotOwner()` helper in `bot-auth.ts` for routes serving both nostr-owner and anonymous-bot-secret audiences. -- Built the two things actually requested when the audit was found: **AI-answer settings reachable for existing bots** (`/api/bots/:name/ai-config`, not just at creation) and a **claim-winnings UI** (Cashu payouts were minted server-side but had zero frontend consumer — `41f1b93`) -- `10d4209`: fixed a real `tsc` error the podman build caught that local verification initially missed (misread a wrapper's exit code instead of the actual log content — lesson: always check log *content*, not just the shell wrapper's `$?`) -- Built + pushed `146.59.87.168:3000/lfg2025/botfights:1.2.11` - -**archy repo** (pushed to `gitea-ai/main`, my commits at `aea17248`/`b0a08345` — many other agents' commits have landed on top since, this is a busy shared tree): -- `apps/botfights/manifest.yml` bumped to 1.2.11; fixed `data_uid` from `1001` to `999` (the container's real internal UID — first attempt copied fedimint-clientd/barkd's value without checking this image's actual `Dockerfile`, which does `useradd --system` with no explicit UID) -- `scripts/image-versions.sh` kept in lockstep -- Catalog regenerated, signed (user ran `sign-catalog.sh`), published — verified live on `146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/app-catalog.json` -- Deployed to both nodes via RPC (`package.update`), both verified healthy: - - **archi-dev-box** (local): `botfights` container on `1.2.11`, `/api/health` → ok - - **x250-beta** (`archy-x250-beta.tail08d8f2.ts.net`): `botfights` container on `1.2.11`, `/api/health` → ok, `/api/bots` confirmed identical to `botfights.archipelago-foundation.org` (arena-proxy forwarding correctly) - - - -Nothing blocking the demo. One loose end, likely moot: -- Framework PT (`100.65.115.109`) SSH access is still blocked — the password was rotated 2026-07-26 and the current one isn't recorded anywhere. User redirected the demo plan away from Framework PT to x250-beta earlier in the session, so this probably doesn't matter anymore unless the user brings it up again. - - - -- Cashu is now the primary UX for both paying entry fees AND receiving payouts, Lightning/NWC demoted to a secondary "or connect a Lightning wallet instead" option — explicit user instruction. -- `data_uid: 999:999` (not 1001) in the botfights manifest — verified against the running container's actual `id` output, not assumed from another app's manifest. -- ai-config routes accept EITHER a nostr JWT (new, for browser owners) OR the bot's own secret (existing, for anonymous AI-agent poll-mode bots) — additive, not a replacement, since both audiences are real and pre-existing. - - - -- Framework PT SSH: password unknown since 2026-07-26 rotation. Not currently blocking anything (user moved to x250-beta). - - -## Required Reading (in order) -1. This file, obviously. -2. `.planning/phases/09-botfights-platform-upgrade/09-06-SUMMARY.md` and `09-07-SUMMARY.md` — the actual last GSD-tracked work in this area, for anyone confused about why there's no PLAN.md for tonight's work. -3. If continuing security work: re-read the fix pattern in `botfight` repo commits `f5f57e6` and `c162d5e` before touching any other route that reads a pubkey — the same bug class may exist elsewhere in the codebase that wasn't audited (only `auth.ts`, `payments.ts`, and `queue.ts` were checked; `bots.ts`, `tournaments.ts`, `bets.ts` were not re-audited for this exact pattern). - -## Critical Anti-Patterns (do NOT repeat these) -- **ANTI-PATTERN: trusting a shell wrapper's exit code instead of the actual command output.** During this session, `tsc --noEmit ... ; echo "EXIT=$?"` was read as "passed" from the *notification summary* (which reports the wrapper's own exit code, always 0 because `echo` always succeeds) rather than the log *content*. This let a real `tsc` compile error through to a `podman build` failure. → Structural mitigation: always `cat`/`Read` the actual log file and look for the error pattern or an explicit `EXIT=N` marker line before treating a background verification command as passed. -- **ANTI-PATTERN: assuming multi-repo work is saved because one repo was pushed.** → Structural mitigation described in the BLOCKING CONSTRAINT above. -- **ANTI-PATTERN (from earlier this session, already corrected): never run `archipelago --version` on a fleet node** — it starts the full daemon rather than printing a version string (deployed binaries predate the flag). Use source-reading instead of the binary for investigation. - -## Infrastructure State -- **archi-dev-box** (local node): `archipelago` daemon healthy, RPC on `127.0.0.1:5678` (session cookie in `/tmp/archy-dev-cookies.txt`, likely stale by the time this is read — re-login with `auth.login` / password ``). `botfights` container healthy on `1.2.11`. -- **x250-beta** (`archy-x250-beta.tail08d8f2.ts.net`, tailnet IP rotates — resolve by MagicDNS name): reachable via plain `ssh archipelago@archy-x250-beta.tail08d8f2.ts.net` this session (no password prompt hit — key-based or cached). RPC session cookie in `/tmp/archy-cookies.txt` **on that remote node**, likely stale — re-login same way. `botfights` container healthy on `1.2.11`. -- Both nodes' local `/tmp` filled up mid-session (a 12G tmpfs, hit 0MB free once) — if you hit `ENOSPC` from the harness itself (not the actual command), check `df -h /tmp` and clean up stray large files (this session's culprit: two OTA release assets, ~260MB, downloaded to `/tmp` on the **local** machine as a relay step for an unrelated node update earlier in the session). -- Canonical arena: `https://botfights.archipelago-foundation.org` — both demo nodes proxy to this via `ARENA_UPSTREAM_URL`, confirmed serving identical bot/fight data on both. - - -The user is demoing BotFights live, same day, wants two real fighters paying/winning with Cashu ecash across two real node installs. All of that is now in place and verified. The security audit was NOT originally requested — it was triggered by investigating the user's question "can we confirm the fighter wins all the cashu sats into their node wallet automatically", which led to reading `payments.ts` end to end and discovering the payout claim flow had no frontend UI *and* the backend route serving it had no auth at all. That in turn led to checking every other route with a similar shape, which is how 5 more instances of the same bug were found. This is worth remembering: a seemingly simple product question ("where does the money go") uncovered a real, live, exploitable vulnerability in a publicly-deployed app — treat "let me just check how this actually works end to end" as time well spent, not scope creep. - - - -Nothing is required to "resume" — this was a complete, self-contained session of off-plan work, fully committed, pushed, deployed, and verified. If the user opens a new session and says something like "continue" or "where were we", the right first move is to summarize the state above (both nodes on `1.2.11`, security fixes live, demo-ready), not to look for a GSD plan to execute. If the user wants to resume *GSD-tracked* work specifically, `STATE.md` says Phase 10 (Key-Material Hardening, KEY-01..KEY-04, sourced from `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`) is planned and ready to execute — but that is a separate, unrelated thread from tonight's BotFights work, and STATE.md is being actively updated by other concurrent agents working other phases (01, 02, 10) in this shared tree, so re-read it fresh rather than trusting anything cached. - diff --git a/.planning/APP-PORT-AUTH-GATE.md b/.planning/APP-PORT-AUTH-GATE.md deleted file mode 100644 index 3717af7b..00000000 --- a/.planning/APP-PORT-AUTH-GATE.md +++ /dev/null @@ -1,106 +0,0 @@ -# App-port authentication gate — design - -Item 1 of `RELEASE-1.7.121-TASKS.md`. Opened 2026-08-04. - -> "if I'm logged out I can reach every app port on tailscale and LAN, this can not be -> allowed… it must present the login to access the app with an app icon of what you're -> accessing to confirm, and 2FA if present" — operator, 2026-08-03 -> -> "make sure we fix FIPS, Tor, everything … umbrel definitely shows a port when you go to -> tailscale IP or other + port but demands the node login and 2FA if activated" -> — operator, 2026-08-04 - ---- - -## What we already built, and why it did not close this - -The operator's recollection that FIPS and Tor were "done" is correct — but that work was -about **reachability**, and about restricting the **daemon's own** API. Neither one ever -authenticated an app port. Read together, each transport got a door and none got a lock: - -| Layer | What exists today | What it protects | -| --- | --- | --- | -| `server.rs:1271` `is_peer_allowed_path` | Federated peers hitting the **daemon** port may only reach `/health`, `/rpc/v1`, `/content`, `/blob/`, `/dwn/`, `/transport/inbox`, `/archipelago/*` | The daemon's API surface. **Not app ports.** | -| `fips/app_ports.rs` `APP_LAUNCH_PORTS` | 35 app ports **allowed through** the fips0 firewall | Nothing — it *opens* them | -| `server.rs:1130` `app_port_v6_relay_loop` | Daemon relays mesh v6 → v4 loopback for those same ports | Nothing — it *bridges* them | -| `api/rpc/tor/mod.rs:243` | Per-app `HiddenServicePort 80 → 127.0.0.1:` | Nothing — it *publishes* them to an onion | -| `container/quadlet.rs:261` | `PublishPort=0.0.0.0:{host}:{container}` | Nothing — it binds every interface | - -So the app ports are reachable, by construction, over LAN, Tailscale, FIPS mesh and Tor, -and nothing on any of those paths checks a session. This is the same bug class as the -v1.7.120 `/lnd-connect-info` + `/bitcoin-rpc/` leaks, but structural rather than -per-endpoint. - -## The rule this design is built on - -**You cannot gate a socket you do not own.** Every previous fix added a check *beside* the -listener, which is why each one only covered the transport it was written for. The gate -has to *be* the listener. - -## Design - -Port numbers do not change. For an app whose UI port is `P`: - -- **The app binds `127.0.0.1:P` only** (`PublishPort=127.0.0.1:P:`), so it is - no longer reachable from any interface. -- **The gate binds `P` on every external address** — LAN IP, Tailscale IP, fips0 ULA — - and on **`127.0.0.2:P`** for Tor. `127.0.0.2` is a distinct loopback address, so it does - not collide with the app on `127.0.0.1:P`, and it means **no app needs a second port - number**. `torrc` changes to `HiddenServicePort 80 127.0.0.2:P`. -- Upstream for the gate is always `127.0.0.1:P`. - -Because the gate owns the socket, LAN / Tailscale / FIPS / Tor are one code path. There is -no per-transport work, and therefore no transport to forget. - -### Request handling - -1. Read the `session` cookie. Cookies are **host-scoped and port-agnostic**, so the - session minted on the dashboard is presented to `:P` automatically — this is the - same mechanism umbrel's "proxy token" relies on. (Scheme still matters: a `Secure` - cookie will not travel to a plain-HTTP app port. See open questions.) -2. **Valid session** → proxy to `127.0.0.1:P`, passing through `Upgrade` so WebSockets work. -3. **No/invalid session** → serve the login page **on the app port itself**, naming the app - and showing its icon, POSTing back to the same origin. The gate verifies the password, - enforces TOTP when enabled, and sets the session cookie — so logging in at - `:P` also logs you into the dashboard, exactly as umbrel behaves. -4. Non-browser clients get `401` with a JSON body rather than an HTML page. - -### What must NOT be gated - -Non-HTTP ports cannot carry a cookie and must be declared, not discovered: -electrum `50002`, bitcoin p2p `8333`, LND gRPC `10009`/`9735`. These need an explicit -manifest field (`auth: none` + rationale) so the exception list is a `grep`, and they are -a firewall/allowlist question, tracked separately. - -Note `api/rpc/tor/mod.rs:238-240` already special-cases lnd's `9735`/`10009` as -`is_protocol_service` — that distinction is the seed of the manifest field. - -## Deploy traps this walks into - -- **Three copies of every container spec** — `apps//manifest.yml`, - `scripts/container-specs.sh`, `scripts/first-boot-containers.sh`. Changing `PublishPort` - in one leaves fresh installs broken while the node looks fixed. This is exactly what bit - lnd-ui (item 4). **Deduplicating these is arguably a prerequisite, not a follow-up.** -- Changing `PublishPort` drifts every app → one-time recreate fleet-wide. -- The gate must rebind when addresses change (Tailscale up/down, DHCP, fips0 re-key). - Precedent exists: `peer_late_bind_loop` in `server.rs` already does this for fips0. -- Verify **on the node**, not from source. v1.7.120's headline bug was a fix that shipped - in the binary and never reached the running container. - -## Open questions for the operator - -1. **Machine clients.** Umbrel's real-world failure mode: Home Assistant (or any API - client) hitting an app's API has no cookie and breaks. Browser-only, or do we mint - per-app long-lived tokens? -2. **TLS/scheme.** The daemon serves plain HTTP with nginx terminating TLS in front. If the - dashboard is HTTPS and app ports are HTTP, a `Secure` session cookie will not be sent — - the gate would prompt for login every time. Either the gate serves TLS on app ports too, - or app ports are HTTP-only on such nodes. - -## Sequencing - -1. Gate module + login page + proxy, behind an env opt-in. -2. Prove on **one** HTTP app on .228, across all four transports. -3. Dedupe the container-spec declarations. -4. Roll to all HTTP apps; declare the non-HTTP exceptions. -5. Repoint `torrc` at `127.0.0.2`. diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json deleted file mode 100644 index cc92f2c1..00000000 --- a/.planning/HANDOFF.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "version": "1.0", - "timestamp": "2026-08-02T10:34:47.198Z", - "phase": "09", - "phase_name": "BotFights Platform Upgrade", - "phase_dir": ".planning/phases/09-botfights-platform-upgrade", - "plan": null, - "task": null, - "total_tasks": null, - "status": "paused", - "context_type": "ad_hoc_reactive", - "note": "This handoff does NOT track a GSD plan/task. Phase 09's plans 09-01..09-07 are all already complete (SUMMARY.md exists for each, most recent 09-07-SUMMARY.md dated 2026-07-31 05:08). Everything recorded here happened AFTER 09-06/09-07 were done, as live reactive demo-day work directed by the user in conversation, not from a PLAN.md task list. There is no in-progress GSD plan to resume — this is purely a work-state save so uncommitted/unpushed work and node state are not lost.", - "completed_tasks": [ - {"id": "botfight-security-audit", "name": "Found + fixed 6 IDOR/missing-auth vulnerabilities in botfight repo", "status": "done", "commit": "f5f57e6 (auth.ts), c162d5e (payments.ts/queue.ts)"}, - {"id": "botfight-ai-config-existing-bots", "name": "AI-answer settings UI for existing bots (not just at creation)", "status": "done", "commit": "41f1b93"}, - {"id": "botfight-claim-winnings-ui", "name": "Claim-winnings UI (Cashu payouts were backend-only, no frontend consumer)", "status": "done", "commit": "41f1b93"}, - {"id": "botfight-tsc-fix", "name": "Fixed possibly-undefined route param tsc error caught by podman build", "status": "done", "commit": "10d4209"}, - {"id": "botfight-1.2.11-release", "name": "Built + pushed botfights:1.2.11 image to registry", "status": "done"}, - {"id": "archy-manifest-1.2.11", "name": "Bumped apps/botfights/manifest.yml + scripts/image-versions.sh to 1.2.11, regenerated+signed+published catalog", "status": "done", "commit": "aea17248 (manifest bump), b0a08345 (signed catalog)"}, - {"id": "deploy-archi-dev-box", "name": "Updated BotFights to 1.2.11 on archi-dev-box via package.update RPC", "status": "done"}, - {"id": "deploy-x250-beta", "name": "Updated BotFights to 1.2.11 on x250-beta via package.update RPC", "status": "done"}, - {"id": "botfight-push-to-origin", "name": "Pushed 4 local-only botfight commits to origin (were unpushed until this handoff step)", "status": "done", "commit": "d00e792..10d4209 -> origin/main"} - ], - "remaining_tasks": [ - {"id": "framework-pt-access", "name": "Framework PT (100.65.115.109) SSH access still blocked — password was rotated 2026-07-26, current password unknown. User redirected focus to x250-beta instead, so this may no longer be needed for the demo.", "status": "blocked"} - ], - "blockers": [ - {"description": "Framework PT SSH password unknown (rotated, not recorded)", "type": "human_action", "workaround": "User already redirected demo plan to use x250-beta instead of Framework PT — likely moot unless user asks for Framework PT again."} - ], - "async_jobs": [], - "human_actions_pending": [], - "decisions": [ - {"decision": "Made Cashu the primary entry-fee AND payout UX for BotFights, Lightning/NWC secondary", "rationale": "Explicit user instruction: \"please make cashu the primary UX and lightning secondary\"", "phase": "09"}, - {"decision": "Fixed data_uid in apps/botfights/manifest.yml from 1001 to 999", "rationale": "Container's actual internal UID (confirmed via `podman exec botfights id`) is 999, not 1001 — first attempt copied fedimint-clientd/barkd's value without verifying against this specific image's Dockerfile (`useradd --system` with no explicit UID lands at 999)", "phase": "09"}, - {"decision": "Extended ai-config routes to accept EITHER nostr JWT (verifyBotOwner) OR the bot's own secret, rather than replacing bot-secret auth", "rationale": "Poll-mode AI-agent bots (no nostr identity) still need the original bot-secret path; nostr-logged-in browser owners needed a new path that didn't exist before", "phase": "09"} - ], - "uncommitted_files": [], - "unrelated_uncommitted_by_other_agent": [ - "core/archipelago/src/container/prod_orchestrator.rs (archy repo) — modified by a DIFFERENT concurrent agent, not touched by this session. Do NOT stage, commit, or stash this file." - ], - "next_action": "No GSD action required to resume — Phase 09 is fully complete and this was off-plan reactive work, now fully committed and pushed in both repos (archy @ b0a08345, botfight @ 10d4209 on origin/main), deployed to both demo nodes (archi-dev-box + x250-beta, both verified healthy on botfights:1.2.11), and catalog signed+published. If resuming demo work: verify nodes are still healthy (`curl http://127.0.0.1:9100/api/health` on each) since time has passed. If resuming GSD-tracked work: STATE.md says Phase 10 (Key-Material Hardening) is planned and ready to execute — that is a SEPARATE, unrelated GSD phase from tonight's BotFights firefighting.", - "context_notes": "This was a long reactive demo-prep session, not GSD-plan-driven. Started from GSD-executing 09-06-PLAN.md (bump BotFights manifest + sign catalog), which completed normally and STOPPED at the signing checkpoint as designed. Everything after that was live user-directed firefighting for a same-day demo: iframe embedding, native signer bridge, AI-answer feature, Cashu payment integration (both entry-fee and payout sides), a security audit that surfaced a systemic IDOR pattern (client-supplied pubkey trusted without verification) repeated across 6 routes — 2 of them critical (unauthenticated Cashu-token leak, unauthenticated wallet-hijack) — and a second-node deployment to x250-beta that surfaced a real manifest bug (data_uid). The single biggest risk caught in this handoff step itself: 4 botfight-repo commits (the entire security-fix work) were sitting LOCAL-ONLY, never pushed to origin, until this pause-work step explicitly checked ahead/behind counts and pushed them. Always verify `git status -sb` / ahead-behind against the actual remote before ending a session that touched a repo other than the one being actively `git push`ed in the visible workflow — pushing archy did not imply botfight got pushed too, they are separate repos." -} diff --git a/.planning/INGEST-CONFLICTS.md b/.planning/INGEST-CONFLICTS.md deleted file mode 100644 index 703e66c4..00000000 --- a/.planning/INGEST-CONFLICTS.md +++ /dev/null @@ -1,32 +0,0 @@ -# Ingest Conflict Report - -Mode: new (fresh bootstrap — no existing .planning/ context to check against) -Precedence: ADR > SPEC > PRD > DOC (no per-doc overrides present) - -## Conflict Detection Report - -### BLOCKERS (0) - -(none) - -### WARNINGS (0) - -(none) - -### INFO (4) - -[INFO] Overlapping locked ADRs on Nostr marketplace discovery — consistent, not contradictory - Found: docs/adr/003-nostr-for-discovery.md and docs/adr/006-nostr-marketplace-discovery.md are both locked and both decide "Nostr relays (NIP-78, kind 30078) for app manifest discovery" over the same scope - Note: The decisions agree; ADR-006 refines ADR-003 with concrete trust tiers (Verified/Community/Unverified), curated built-in app list, and pre-install signature verification. Both preserved as separate entries in intel/decisions.md; no resolution needed. Consider marking one as superseding/refining the other in the docs for hygiene. - -[INFO] SPEC security validation list narrower than ADR-009 mandatory defaults - Found: docs/adr/009-manifest-container-security.md (locked) mandates non-root UID (> 1000), pinned image tags (no `latest`), and a default seccomp profile as non-negotiable defaults; docs/app-manifest-spec.md's documented SecurityPolicy schema and AppManifest::validate() list do not mention these three (SecurityPolicy has apparmor_profile but no seccomp field) - Note: This is SPEC silence, not contradiction — no auto-resolution applied. ADR-009 governs by precedence (ADR > SPEC) and lock status. The SPEC itself declares `core/container/src/manifest.rs` canonical over the doc, so the gap may be documentation drift rather than implementation drift. Flagged for downstream verification, recorded as absent in intel/constraints.md. - -[INFO] ADR numbering gap — ADR-010 absent from ingest set - Found: Classified ADRs run 001–009 and 011; no classification exists for an ADR-010 - Note: Either ADR-010 does not exist, was withdrawn, or was not included in the ingest. No action required for synthesis; noted for completeness of the decision record. - -[INFO] Cross-reference graph is acyclic - Found: cross_refs edges: ADR-007 → ADR-003; ADR-009 → docs/app-manifest-spec.md (+ code paths); SPEC → out-of-set docs and code only (app-developer-guide.md, manifest-hooks-design.md, marketplace-protocol.md, core/container/src/manifest.rs, api/rpc/package/stacks.rs) - Note: DFS cycle detection found no cycles; all 11 docs were synthesized. Several SPEC cross-refs point to documents not in the ingest set — they were not followed. diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md deleted file mode 100644 index b645fc09..00000000 --- a/.planning/PROJECT.md +++ /dev/null @@ -1,115 +0,0 @@ -# Archipelago - -## What This Is - -Archipelago is a self-hosted personal-server platform: a Rust daemon (workspace at `core/`) -plus a Vue 3 frontend (`neode-ui/`, built to `web/dist/neode-ui/`) running on Debian nodes -with rootless Podman, managing ~40 declarative, manifest-driven apps (Bitcoin, Lightning, -mesh/LoRa, federation, media, and more). It ships as OTA-updated releases to a live fleet -and is actively shipping v1.7.x alpha releases. This milestone drives it to the -**developer-ready app platform** north star. - -## Core Value - -A third-party developer can publish an app via the signed/decentralized registry and a user -can install it on their node — every app manifest-driven, manifests shipped via the signed -registry (not OTA disk files), all rootless, secure, robust, and 100%-uptime-capable. - -## Current State (brownfield baseline, 2026-07-29) - -- Single-node production gate is **GREEN** (5/5 on .228, 2026-06-23) — that exit criterion is met. -- ~40 apps are manifest-based and Quadlet-migrated; all multi-container stacks use the - orchestrator stack pattern; the legacy per-app installer anti-pattern is deleted. -- Workstream B (registry-distributed manifests) phases 1+2 are code-complete; the signing - ceremony is done (release-root pinned in `anchor.rs`); the fleet flip is not yet authorized. -- Workstream C (marketplace) is design-only (`docs/marketplace-protocol.md`); no tooling or - trust UX built. Developer CLI suite (`archy app …`) does not exist yet. -- Phase-3 Quadlet default-flip is validated opt-in on .228/.198 but not default. -- Declared next exit criteria: the multinode pass (`docs/multinode-testing-plan.md`) and the - remaining workstreams. - -## Requirements - -### Validated - -- ✓ Single-node lifecycle gate green 5× on .228 (install/UI/stop/start/restart/reinstall/ - reboot-survive/daemon-restart-survive/uninstall) — 2026-06-23 -- ✓ Manifest-driven app packaging for all ~40 apps incl. multi-container stacks (workstream A) -- ✓ Signed catalog + release-root signing ceremony (workstream B phases 1+2, code-complete) - -### Active - -See `.planning/REQUIREMENTS.md` — 20 v1 requirements across MNODE / LIFE / REG / SEC / DEV / MKT, -all mapped to phases in `.planning/ROADMAP.md`. - -### Out of Scope - -- Rootful containers, Docker, privileged containers — invariant (ADR-001/ADR-009) -- Per-app Rust installers / OS-level provisioning — the anti-pattern being deleted -- Centralized gatekept app store — decentralized Nostr marketplace instead (ADR-006) -- Web5 DWN spec compliance — deprioritized after TBD shutdown (ADR-011) -- Custom live voice-call protocol — deprioritized per user 2026-07-01; revisit later -- DHT/iroh distribution backbone (workstream D) — design-only, tracker-marked backlog; v2 - -## Context - -- Repo: `core/` Rust workspace (no root Cargo.toml), `neode-ui/` Vue frontend, `apps/` manifests, - `tests/lifecycle/` + `tests/multinode/` gates, `docs/` authoritative plans. -- Authoritative narrative: `docs/PRODUCTION-MASTER-PLAN.md`; day-to-day open list: - `docs/UNIFIED-TASK-TRACKER.md`. Codebase map: `.planning/codebase/ARCHITECTURE.md` + - `.planning/codebase/CONCERNS.md`. -- Known debt informing this milestone (from CONCERNS.md): federation tombstone-write errors - swallowed; reconciler has no flap observability and no failed-unit self-healing; generated - AppArmor profiles are never applied; multinode test harness curl calls lack timeouts; - SPEC validation is narrower than ADR-009's mandates (non-root UID, pinned tags, seccomp). -- Fleet is live and OTA-updated; all destructive verification happens on designated test - nodes per the deploy roster — never uninvited on in-use nodes. - -## Constraints - -- **Security**: Rootless Podman only; manifest-declared secrets (0600, never logged); - mandatory container security defaults enforced at manifest level (ADR-009) -- **Data safety**: Migrations never destroy data — preserve `/var/lib/archipelago/`, - secrets, credentials, ports, adoption container names; always a rollback path -- **Verification**: Real-node verification before any tag; lifecycle gate runs ON the node, - not via RPC; mesh changes need real-RF E2E tests; re-run the gate after orchestrator changes -- **Process**: Commit + push every unit of work (`git push gitea-ai main`); stage by explicit - path; deploy to the dev pair before any OTA; never commit secrets -- **Tech stack**: Rust (Tokio/Hyper, JSON-RPC 2.0) backend; Vue 3 + Pinia frontend; - Quadlet/systemd-user container units; Ed25519-signed release artifacts - -## Key Decisions - - - -All ten ADRs below are **locked** (Status: Accepted; ingest source `docs/adr/*.md`). They are -non-negotiable inputs to planning and cannot be overridden without a new ADR. - -| ID | Decision | Scope | -|----|----------|-------| -| ADR-001 | Podman over Docker — rootless, daemonless, systemd-native; `archy-net` for inter-container DNS | Container runtime | -| ADR-002 | `did:key` (Ed25519) node identity — self-contained, offline-capable; gaps mitigated via federation trust lists | Identity | -| ADR-003 | Nostr relays (NIP-78, kind 30078) for node + app discovery — multi-relay query, 15-min cache, trust scoring, Tor-compatible | Discovery | -| ADR-004 | Tor hidden services for inter-node RPC/control plane — bulk data via registries, not Tor | Federation transport | -| ADR-005 | ChaCha20-Poly1305 + Argon2id (64MB, 3 iter) for backup encryption | Backups | -| ADR-006 | Nostr relays for marketplace discovery — DID-signed manifests, trust tiers (Verified/Community/Unverified), signature verification before install | Marketplace | -| ADR-007 | Bilateral DID federation trust via single-use invite codes; Trusted/Observer/Untrusted levels | Federation trust | -| ADR-008 | Dual keys from one master seed — Ed25519 canonical identity, secp256k1 for Nostr/Bitcoin/Lightning, linked via NIP-05 | Keys | -| ADR-009 | Manifest-level container security enforcement — readonly_root, no_new_privileges, non-root UID, drop-ALL caps, pinned tags, seccomp; overrides explicit + audited | Container security | -| ADR-011 | DWN deprioritized — keep custom `dwn_store.rs`, stop branding as Web5, invest in Nostr + Tor federation instead | Peer data sync | - -(ADR-010 does not exist in the repo — numbering gap, noted in `.planning/INGEST-CONFLICTS.md`.) - - - -Milestone-level decisions: - -| Decision | Rationale | Outcome | -|----------|-----------|---------| -| Milestone version = 1.8.0-alpha | Decided 2026-07-08 per tracker | — Pending ship | -| Workstream D (DHT) deferred to v2 | Design-only, tracker-marked backlog; not needed for north-star metric | — Pending | -| App manifest canonical schema = `core/container/src/manifest.rs` | SPEC self-declares code wins over doc | ✓ Good | -| Phase-3 Quadlet flip gated on multinode gate reporting clean | Prior uncommitted-flip confusion; flip fresh as a 2-line change when gate is clean | — Pending | - ---- -*Last updated: 2026-07-29 after intel ingest (10 ADRs + 1 SPEC) + codebase mapping* diff --git a/.planning/RELEASE-1.7.121-TASKS.md b/.planning/RELEASE-1.7.121-TASKS.md deleted file mode 100644 index 7542bda1..00000000 --- a/.planning/RELEASE-1.7.121-TASKS.md +++ /dev/null @@ -1,554 +0,0 @@ -# Release 1.7.121 — task list - -Opened 2026-08-03, immediately after v1.7.120-alpha shipped. Everything the operator has -asked for since, plus the items v1.7.120 deliberately left open. Ordered by severity. - -Status key: **DONE** (committed) · **READY** (written, not yet committed/tested) · -**OPEN** (not started) · **BLOCKED** (needs an operator decision) - ---- - -## P0 — Security - -### 1. App ports are reachable with no login, on every transport — **OPEN** -> "if I'm logged out I can reach every app port on tailscale and LAN, this can not be -> allowed… it must present the login to access the app with an app icon of what you're -> accessing to confirm, and 2FA if present" — operator, 2026-08-03 - -- Applies to **Tailscale, LAN, Tor, FIPS** alike, and to "ssh access to that port or whatever". -- Required behaviour: an unauthenticated request to any app port serves a **login page - naming and showing the icon of the app being accessed**, then honours **2FA when set**. -- **Research first:** how umbrelOS and StartOS gate app access (operator asked explicitly). - Both are open source — `getumbrel/umbrel` and `Start9Labs/start-os`. Do not guess at - their model; read it. -- This is the same class as the v1.7.120 `/lnd-connect-info` + `/bitcoin-rpc/` leaks, but - **fleet-wide across every app port** rather than two endpoints. Those two were closed by - moving authorisation to the resource; this needs a general gate. -- Scope note: `fips/app_ports.rs` holds the mesh allowlist; `is_peer_allowed_path` in - `server.rs` holds the peer HTTP allowlist. Neither currently authenticates app ports. - -#### Research — umbrelOS (verified from their docs/source, 2026-08-03) - -umbrelOS solves this **architecturally, not per-app**: the app's own port is never -published. Each app gets a sidecar `app_proxy` container that owns the published port and -forwards to the app on the internal network. - -- `containers/app-proxy` is described as *"a transparent HTTP proxy to add authentication - to Umbrel apps"* — **every** HTTP request and WebSocket upgrade passes through it and - has its session token checked. -- Tokens come from a separate `app-auth` service; the proxy talks to it over a local port - (default 2000) with a shared secret (`UMBREL_AUTH_SECRET`). Two JWTs exist: an **API - token** in localStorage (`{loggedIn: true}`) for the dashboard's own API, and a - **proxy token** in an **HttpOnly cookie** (`{proxyToken: true}`) for app access. Both - HS256, 7-day expiry. -- Unauthenticated requests are redirected to the login screen. -- Per-app escape hatches, all env vars on the proxy: `PROXY_AUTH_ADD` (bool, **default - true** — so apps are protected unless opted out), `PROXY_AUTH_WHITELIST` (paths exempt, - e.g. `/public/*`), `PROXY_AUTH_BLACKLIST` (paths that must be authed, e.g. `/admin/*`). -- Known friction worth designing around: apps with their own login (Frigate, and the - `PROXY_AUTH_ADD=false` tracker issue) end up double-authenticating, and non-browser API - clients (Home Assistant hitting an app's API) break because they have no cookie. Any - gate we build needs a story for machine clients, not just browsers. - -**The lesson for us:** the reason umbrel doesn't have this bug class is that there is no -unauthenticated path to bind to in the first place. Our apps publish their own ports -directly, so a gate bolted onto one transport leaves the others open — which is exactly -the shape of the `/lnd-connect-info` + `/bitcoin-rpc/` leaks. The fix likely has to move -the port binding, not just add a check. - -#### Reproduced ON archi-dev-box, 2026-08-03 — baseline before the fix - -No session cookie, over the Tailscale IP `100.69.68.39`: - -``` -port 18083 HTTP 200 LND - Archipelago -port 8334 HTTP 200 -port 8175 HTTP 200 Fedimint Guardian - Archipelago -port 8336 HTTP 200 FIPS Mesh -port 8090 HTTP 200 -port 7777 HTTP 200 -``` - -`ss -tlnp` confirms these are bound `0.0.0.0`, so the same responses are served on the LAN -IP and every other host address. Re-run this exact loop after the fix: every one must -become the login page, and the ports listed as protocol exemptions (item 1b) must be the -*only* ones still answering. - -#### Exposure map — how app ports are reachable TODAY (verified in source, 2026-08-03) - -All four transports converge on `127.0.0.1:`. This is the whole reason the fix -is tractable: it is **one gate, not four**. - -| Transport | Path to the app | Code | -|---|---|---| -| LAN / Tailscale | container publishes the port on the host (`--network host`, so `0.0.0.0:`) — reachable on *every* host IP | `scripts/container-specs.sh`, `first-boot-containers.sh` | -| FIPS mesh | daemon binds `[fips0-ULA]:` and raw-TCP-forwards to `127.0.0.1:` | `server.rs:1130` `app_port_v6_relay_loop` | -| FIPS firewall | `tcp dport { …APP_LAUNCH_PORTS… } accept` drop-in opens them all | `fips/config.rs:274`, `fips/app_ports.rs` | -| Tor | `HiddenServicePort 80 127.0.0.1:` per service | `api/rpc/tor/mod.rs:243` | - -#### Design decision (operator, 2026-08-03) - -**Gate app UIs + bearer tokens; protocol ports exempt.** HTTP app UIs get the login gate -(app name + icon, 2FA honoured). Protocol ports (LND gRPC 10009 + REST, electrum 50002, -bitcoin p2p 8333) stay open but MUST be declared `auth: none` with a rationale in the -manifest, so the exceptions are a grep rather than a discovery — see item 1b. Per-app -long-lived bearer tokens cover machine clients that speak HTTP (Home Assistant). **Zeus -and electrum wallets keep working untouched** — that was the deciding constraint. - -The gate lives in the **daemon**, not a per-app sidecar container (umbrel's `app_proxy` -model): rootless, no extra containers per app, one place to update, and it can reuse the -existing `app_port_v6_relay_loop` rather than fight it. - -#### ⚠️ Trap found while designing — an nft-only gate FAILS OPEN - -The obvious implementation is an nft redirect of inbound app-port traffic to the gate. -But `/etc/fips/fips.nft` is **provisioned out-of-band** and `fips/config.rs:290` treats its -absence as a no-op (`if try_exists("/etc/fips/fips.nft")`). A gate shipped as a `fips.d` -drop-in would therefore be **silently absent on every node without the hardening -baseline** — i.e. it fails open, which is exactly the failure class this item exists to -close. - -Two viable shapes, both fail-closed: -- **(a) Apps bind loopback only**, daemon owns every external bind. Airtight, the true - umbrel model, but requires touching each app's own listen config (nginx.conf etc.). - Note you *cannot* half-do this: while an app holds `0.0.0.0:`, the daemon cannot - bind `:` at all. -- **(b) Daemon owns a dedicated `archipelago-appgate` nft table** with its own - default-deny + redirect, independent of whether `fips.nft` exists, and refuses to start - / alarms loudly if it cannot install it. Non-invasive to apps. - -#### Enabler found — `PortMapping.bind` already does half of (a) - -`core/container/src/manifest.rs:518` — `PortMapping` has a `bind` field, documented as -*"Host address to bind the publish to. Empty = all interfaces (0.0.0.0). Set `127.0.0.1` -to keep a port host-local"*. So for **bridge apps that declare `ports:`**, going -loopback-only is a **manifest edit, not app surgery**, and the daemon can then own the -external bind. That is most of the catalog. - -The exception is **host-networked apps** (`security.network_policy: host` — `lnd-ui`, -`bitcoin-ui`, `electrs-ui`): host networking bypasses port mapping entirely, so `bind` has -no effect and `ports:` is deliberately empty. Those bind whatever their internal nginx -binds. We build those images ourselves, so the fix is a `listen 127.0.0.1:;` change -in each `docker/*-ui/nginx.conf` — still no third-party surgery. - -Watch the rootless trap documented at `manifest.rs:532`: a publish bound to an address the -host cannot actually bind crash-loops the whole unit (took bitcoin down fleet-wide on .228, -2026-07-09). Loopback binds are explicitly always accepted without probing, so this -direction is safe. - -**Tor needs separate handling either way**: the onion connects *from* localhost, so a -redirect that exempts loopback will not catch it. `HiddenServicePort` must be repointed at -the gate, and since that mapping loses the original destination port, each app needs its -own gate port (or an HTTP-level Host mapping). - -#### Primitives that already exist — do NOT build these from scratch - -The gate is mostly assembly, not invention: - -| Need | Existing API | -|---|---| -| Read the session cookie off a request | `session::extract_session_cookie(&HeaderMap) -> Option` (`session.rs:479`) | -| Validate a session | `SessionStore::validate(&token) -> bool` (`session.rs:194`) | -| **Honour 2FA** | Already modelled: `create_pending(totp_secret)` (`:176`) + `upgrade_to_full` (`:247`). A session still pending 2FA **fails `validate()`**, so the gate gets 2FA for free by calling `validate` — no TOTP code in the gate itself | -| **Machine-client bearer tokens** | `device_tokens::create/verify` (`device_tokens.rs:63/:90`) — long-lived, minted from an authenticated session, only the SHA-256 persisted, plaintext returned once. Built for the companion pairing QR; needs **per-app scoping** added for this use | -| Rate limiting | `device_tokens` verification already rides `auth.login`'s limiter | - -So the new code is: the listener/redirect, the app-identification step (which app is this port?), -the login page render (app name + icon), and per-app scoping on `device_tokens`. - -#### Research — StartOS: **DROPPED** (operator, 2026-08-03) - -"don't need the startOS research we decided on a approach already." The umbrelOS read -plus the design decision above settled it; no further prior-art work. - -### 1b. Manifest declaration of unauthenticated ports — **DONE** (`0c4826f8`, pushed) - -`PortMapping` grew `auth` (`session` | `none`, defaulting to **`session`**) and -`auth_rationale`. The default is the protected one, so exposure is now something a -manifest has to ask for rather than something it gets by saying nothing. - -Validation is two-sided: `auth: none` without a rationale is rejected, **and** a -rationale without `auth: none` is rejected — that combination means the author wrote an -exemption and did not get one, and shipping it silently would leave them believing -otherwise. - -**17 ports across 12 apps are exempt**, each with its reason: Lightning p2p (BOLT-8 -noise), LND gRPC 10009 / REST 18080 and CLN gRPC 9835 (macaroon / mutual TLS — this is -what keeps Zeus and remote wallets working), Bitcoin p2p 8333, electrum 50001, the three -Wyoming voice ports, git-over-SSH 2222, and the UDP discovery protocols (mDNS 5353, -SSDP 1900, STUN 3478). **The other 39 published ports now default to gated.** - -Bitcoin RPC 8332 is deliberately *not* exempted: it is already `bind: 127.0.0.1`, so the -gate never sees it, and claiming an exemption it does not need would put a meaningless -line in the audit list. If that bind is ever dropped it fails closed. - -Two corpus tests pin this: every shipped manifest must parse, and the exempt set is -frozen at 17 so the node's unauthenticated surface cannot grow by accident. - -### 1c. The gate itself — **IN PROGRESS** - -`core/archipelago/src/appgate/` — `identity.rs` (port → app id/name/icon, gated vs -exempt, re-read from manifests so a catalog refresh applies without a restart), -`mod.rs` (authorize + login page + TOTP step + reverse proxy), `listener.rs` (binds the -external addresses, sweeps every 60s). - -Design points worth not re-deriving: - -- **It invents no auth policy.** `verify_password`, `totp::decrypt_secret`, - `verify_code` + used-step replay protection, `SessionStore::create/create_pending/ - upgrade_to_full`, and the *same* `LoginRateLimiter` instance as the JSON-RPC path. - Only the transport differs (HTML form vs JSON-RPC), because a browser being redirected - to an app cannot speak JSON-RPC. Sharing the limiter matters: otherwise an attacker - gets a fresh budget of password guesses by moving to an app port. -- **2FA is free.** A session still pending its TOTP step fails `validate()`, so the gate - rejects it without knowing anything about second factors. -- **Cookies ignore port.** The session cookie is host-only with no `Domain`, so one - sign-in covers the dashboard and every app port on the same host. The corollary is - that an app reached on a *different* host — its own onion — is a separate sign-in. -- **401, not a redirect.** A redirect to a login page is indistinguishable from the app - itself redirecting, and machine clients would follow it and parse HTML as their API - response. -- **The gate strips `Cookie` and `Authorization` before proxying.** The app has no use - for the node session and must never be in a position to log or forward it. -- **Machine clients**: `device_tokens` grew `apps: Option>` and - `verify_for_app`. `None` = node-wide (what every existing companion token is — - migrating them by guessing a scope would silently revoke access nobody asked to - revoke); `Some(list)` restricts to those apps. An empty list is rejected rather than - minted, since it would read as "unrestricted" while authorising nothing. - -#### ⚠️ The ordering constraint that shapes the rollout - -A published container port is bound `0.0.0.0:`, which claims **every** host -address. While the app holds that, the gate **cannot** bind `:` at all. -So the gate can only stand in front of an app whose publish has been pinned to loopback -(`bind: 127.0.0.1`) and whose container has been recreated. Gate-first is not possible; -all-apps-at-once would recreate every container on the node simultaneously. - -Therefore the rollout is **per app**, and the gate is built to be honest about being -partially deployed: a port it cannot claim is logged at **warn** every sweep and recorded -in `GateStatus::unprotected`. The failure mode this exists to prevent is a gate that -binds nothing, logs at debug, and reports success while every app stays exactly as open -as before — worse than no gate, because it stops anyone looking. (Same reasoning that -killed the nft drop-in: `/etc/fips/fips.nft` is provisioned out-of-band and its absence -is a silent no-op.) - -**Still open on this item:** pin the 39 gated ports to loopback app-by-app, repoint -`HiddenServicePort` at the gate (Tor connects *from* loopback, so a loopback-exempt -redirect will not catch it, and the mapping loses the original destination port), gate -the FIPS relay path, surface `GateStatus` in the UI, and verify on a real node. - -### 2. Filebrowser ships an insecure default login — **OPEN** -- Change the default credential **without breaking the dashboard's Cloud view**, which - authenticates to filebrowser on the user's behalf. -- Related prior art: FED-07 rotated the shipped Fedimint gateway credential and had to - recreate the running container for it to take effect (`06e0e695`) — the same trap - applies here. - -### 3. Federation trust escalation — **DONE** (`c0cfc72a`, pushed) -Two independent fail-open paths granted `Trusted` without any operator decision: - -- `federation.peer-joined` is **unauthenticated** (middleware no-session list) and - peer-reachable on `/rpc/v1`. Its ed25519 check verifies the caller against **the pubkey - the caller supplied**, so it proves key possession, never authorisation. A join with no - `invite_token` fell through to `TrustLevel::Trusted.min(claimed_trust)`, and - `claimed_trust` defaults to `Trusted` — so anyone able to reach the node could - self-grant Trusted. **Now capped at `Observer`.** -- `merge_transitive_peers` added every peer advertised by a Trusted source as `Trusted`, - making trust viral across the whole federation graph. **Now `Observer`** — which is what - `NodeStateSnapshot.federated_peers`' own doc comment always said it should be - ("adds them as Observers on her side… doesn't auto-promote to Trusted"). The code - contradicted its own spec. -- Added `FederatedNode.trust_source` (`invite` | `uninvited-join` | `transitive-merge` | - `manual`, `None` = pre-existing/unknown) so existing grants are **auditable**. Per - operator decision: existing peers are **left alone, not auto-demoted**. -- `trust_source` is now **surfaced** in `federation.list-nodes` (as an explicit `null` - when unknown, not omitted — "recorded before this was tracked" is the population that - needs review, so the UI must be able to tell it apart from a field it didn't read) and - rendered under the trust dropdown in the node detail modal as "Granted via:". - -### 3b. Granting Trusted must require the node password — **DONE** (uncommitted at time of writing) -> "to make someone trusted must require the node password to generate the code or change -> in the modal dropdown when you click a node" — operator, 2026-08-03 - -Re-authentication on privilege escalation. Both entry points are covered: - -- **Minting a Trusted invite** (`federation.invite`) — gated on the **resolved** level, - which matters because "Link Your Nodes" sends no `trust_level` at all and falls through - to the `Trusted` default. The invite is a bearer grant of Trusted to whoever redeems - it, so minting it *is* the escalation. Observer invites are untouched. -- **Changing a node's level in the UI dropdown** (`federation.set-trust`) — gated only - when the peer is **not already** Trusted, so the dropdown re-emitting its own value - doesn't demand a password for a no-op. - -Demotion is NOT gated: making something less privileged must never be harder than leaving -it, or the safe action becomes the inconvenient one. The operator path stamps -`TrustSource::Manual`; `set_trust_level` grew an `Option` so automatic -adjustments (the discovery-handshake demotion safety net) pass `None` and leave the -recorded provenance alone rather than laundering an `uninvited-join` peer into looking -operator-approved. - -Wiring: the backend is the sole authority on what counts as an escalation — it returns a -`PASSWORD_REQUIRED:` prefixed error, and the UI prompts and retries only on that. The -frontend never pre-judges, so the rule lives in exactly one place. -`TrustPasswordModal.vue` (modelled on `RotateDidModal.vue`) serves both flows. -`NodeDetailModal`'s select now snaps back to the node's real level on change, because a -cancelled or failed promotion would otherwise leave the dropdown displaying a level the -node never accepted. - -**Follow-up, deliberately not done here:** `federation.join` also grants Trusted (when -redeeming someone else's Trusted invite) with no re-auth. It is an explicit operator -paste rather than a UI toggle, and was outside the two entry points specified — but it is -the third way a node reaches Trusted and should be reviewed. - ---- - -## P1 — Correctness the operator hit directly - -### 4. LND UI never updates over OTA — **DONE** (`5088aef5`, pushed) -- `LND_UI_IMAGE` was `lnd-ui:latest` while `BITCOIN_UI_IMAGE` was pinned to - `1.7.119-alpha`. Podman will not re-pull a tag it already holds locally, so nodes kept a - stale lnd-ui forever. **Now pinned to `1.7.119-alpha`.** -- `scripts/first-boot-containers.sh` declared lnd-ui as bridge `-p 18083:80`. That is the - **third copy** of the declaration the UI agent already corrected in - `scripts/container-specs.sh` and `apps/lnd-ui/manifest.yml` — so **fresh installs** still - produced the reproduced `HTTP 000`. **Now `--network host`, ports empty.** -- Root cause worth fixing separately: the same container spec is declared in three places. - -### 5. Federated/peered nodes must message without a LoRa hop first — **OPEN** -> "make it so federated/peered nodes can message without needing to connect on Lora first -> once connected" - -- Investigate the split contact model (radio contact vs federation peer) — there is prior - art in memory: `project_archy_lora_e2e_rootcause` ("split contact model; don't touch - federation") and `mesh::seed_federation_peers_into_mesh` / - `upsert_federation_peer`, which already mirror federation peers into the mesh table. -- Likely the gap is addressing/route selection rather than transport availability. - -### 6. In-app app updates, independent of OTA — **OPEN** -> "we need app update to see updates in the registry, whether UI or not… show the update -> mechanism in the app… a modal and update now / cancel… same in the detail page… the -> update button should show 'see update' and a different graphic for just ui, app, or both -> together. All pushed through the signed-catalog flow." … "This has to show independent of -> OTA updates as a separate pipeline, I think we've done a lot of work on it." - -- **Operator says much of this already exists — research the codebase before building.** - Known groundwork: the signed catalog (`releases/app-catalog.json`, `sign-catalog.sh`), - catalog→manifest runtime reload, `package.update` RPC, `check-app-catalog-drift.py`, - and `scripts/image-versions.sh` pinning. - -#### What already exists (verified in source, 2026-08-03) — the operator was right - -The whole update *pipeline* is built and is already independent of OTA: - -- `package.check-updates` (`api/rpc/package/update.rs:180`) refreshes the signed catalog - and hot-reloads manifests when it changed — no daemon restart, no OTA involved. -- `package.update` (`spawn_package_update`), `package.versions`, `package.set-config` - version pinning, and `execute_update` (stop → pull → remove → recreate → verify). -- Version awareness: `app_catalog::catalog_versions(app_id)` vs `installed_version()` - (`api/rpc/package/set_config.rs:46`). -- Frontend: `AppCard.vue` already renders an Update button off `pkg['available-update']` - (`:48`, `:128`) and emits `update`. - -#### What is actually MISSING (this is the real scope of item 6) - -1. **The UI-vs-app-vs-both distinction does not exist.** `available-update` is a single - version string — nothing classifies whether the change is the app image, its `*-ui` - image, or both. This is the core of the operator's ask ("a different graphic for just - ui, app, or both together") and needs a backend change, not just an icon. - ⚠️ Compounding factor: per `reference_app_ui_delivery_model`, `*-ui` apps are **not in - the signed catalog** at all — so "is there a UI update" cannot be answered from the - catalog today. That gap has to be closed first or the UI half is unanswerable. -2. **The modal** (Update now / Cancel) — the card currently updates on click, no confirm. -3. **The detail-page affordance** — same treatment as the card. -4. **Button copy**: "See update" rather than "Update". - -### 6b. Multiversion for ALL apps + upstream release discovery — **OPEN** (operator, 2026-08-03) -> "we also need a way to provide multiversion support for all apps and it automatically -> pulls the latest versions from the source app repository, safely, and the user can -> choose to update so we aren't always updating manually" - -#### Verified 2026-08-03: the schema and runtime already exist - -This is much less work than it sounds, because the multiversion machinery built for -Bitcoin generalises as data rather than code: - -- `releases/app-catalog.json` entries already support a `versions[]` array of - `{version, image, default?, deprecated?}`. -- Runtime is complete: `catalog_versions(app_id)`, `catalog_default_version`, - `catalog_image_for_version`, `package.versions`, version pinning through - `package.set-config`, and `available_update_for_app` falling back to the - `image-versions.sh` baseline pin. - -**It is populated for 2 of 66 apps** — `bitcoin-core` (9 versions) and `bitcoin-knots` -(5). Every other app carries a single `version`. So "multiversion for all apps" is -primarily a **catalog-generation and image-mirroring job**, not new runtime plumbing. - -#### What has to be built - -1. **Populate `versions[]` fleet-wide.** Extend `scripts/generate-app-catalog.py` to emit - a version list per app instead of a single pin. Needs a per-app policy for how many - historical versions to carry and which is `default` (Bitcoin's list shows the shape, - including `deprecated: true` for old-but-installable). -2. **Mirror the images.** A version in the catalog that is not in our registry is a - broken promise — `package.update` would pull and fail. Use the existing skopeo path - (`feedback_skopeo_source_selection`: prefer `.160`, concurrency ≤ 6). -3. **An upstream release-watcher.** 48 manifests already carry a `repo:` URL under - `metadata`, so there is something to poll (GitHub releases / registry tags). It runs - **off-node**, as part of catalog generation. -4. **Keep the signed catalog as the trust boundary.** This is the whole of "safely": - the watcher **proposes** versions, the offline signing ceremony **admits** them, and - nodes only ever install what the signed catalog carries. A node must never pull - straight from an upstream repo — that would put an unsigned third party inside the - supply chain, which is exactly what the signed-registry model exists to prevent. -5. **The user chooses.** Discovery must never auto-apply. `package.check-updates` already - refreshes and hot-reloads without touching the running containers, so "a new version - exists" and "install it" stay separate — which is also what item 6's modal is for. - -**Sequencing note:** 6b's step 1 and item 6's UI-vs-app classification want the same -thing — `*-ui` images represented in the catalog. Doing that once unblocks both. - ---- - -## P2 — Carried over from v1.7.120 - -### 7. `create-release.sh` commits the manifest BEFORE signing — **OPEN** -Release commit always carries an **unsigned** manifest; nodes fetch it from branch `main` -and refuse to auto-apply. Caught manually this cycle. Fix the ordering so it cannot ship. - -### 8. `gitea-vps2` remote is dead, and is the same server as `gitea-ai` — **OPEN** -Stored token fails auth. `source.archipelago-foundation.org` == `146.59.87.168`, so -`git push gitea-ai` already publishes to the "primary" OTA host. Ties into the existing -"migrate VPS2 IP to domain" todo. - -### 9. Fleet SSH host-key rotation — **BLOCKED** (operator decision) -`archipelago-1`, `archy-x250-beta`, `archipelago` share all three SSH host keys; two also -share a TLS private key. Detection shipped; rotation deliberately not performed. - -### 10. 5× lifecycle gate — **OPEN** -Not run for v1.7.120 (disclosed in its changelog). Needs repeated reboots of a live node. - -### 11. `prod_orchestrator.rs:3181` unreachable code — **OPEN** -`bitcoin_host()` returns unconditionally at :3171, so the podman container-name lookup -below is dead on every path. Pre-existing; spotted in the v1.7.120 build warnings. - ---- - -## Notes for whoever picks this up - -- A separate agent is doing **AIUI planning with GSD** — do not touch AIUI. -- AIUI must always be built `VITE_BASE_PATH=/aiui/` (see the memory note); a hand-built - bundle renders a black page. -- Verify security claims on the node, not from the source. v1.7.120's headline bug was a - fix that shipped in the binary and silently never reached the running container. - ---- - -## STATUS 2026-08-04 — what shipped in 1.7.121 and what did not - -### Shipped (committed + pushed) - -| Item | Commit | Verified | -|---|---|---| -| 3. Federation trust escalation | `c0cfc72a` | 42/42 federation tests | -| 3b. Trusted requires node password | `24ce8b39` | 44/44 + 79/79 + vue-tsc | -| 4. lnd-ui OTA pin + host networking | `5088aef5` | — | -| 1b. Manifest `auth:` declarations | `0c4826f8` | 73/73, all 56 manifests parse | -| 1c. App gate (engine + audit) | `0de67ca6` | 23/23 appgate | -| Dashboard backdrop-filter seam | `63d0183d` | 3/3, **live on archi-dev-box** | -| 7. Release refuses unsigned manifest | `cc9e1958` | dry-run: signed/stripped/wrong-signer | -| Gate safety model (`Option`) | `ab2c8b6e` | 75/75 incl. LND wallet-port case | -| Companion rebuild-loop | `719446c0` | podman behaviour proven first | -| 5. Federated peers messageable | `edc9a172` | predicate pinned across device types | - -### The two gate incidents — read before touching the gate again - -Both were ONE mistake: a safety decision read an ABSENT manifest field as a -value. A node's installed manifests always lag the binary, so "absent" is the -normal state, and the daemon acted on instructions no manifest ever gave. - -1. Gating any `session` port regardless of `bind` **published Bitcoin's - loopback-only RPC 8332 on the LAN/Tailscale/IPv6** within seconds of deploy. -2. The `bind`-keyed replacement looked safe (it protected `bind: 127.0.0.1`) - but LND's gRPC 10009 / REST 18080 carry an EMPTY bind — one container - recreate from pinning them to loopback and **breaking Zeus and every remote - wallet**. - -Now structural: `auth_policy()` classifies (undeclared → reported as -unprotected, always safe), `auth_is_declared()` gates action (undeclared → -never acted on). **Silence is not consent.** - -### Proven on the node, empirically, not by reasoning - -- Gate challenge → login → proxy works end to end over LAN and Tailscale. -- **Daemon-side publish rewriting was removed.** Publishes are built in several - places (`podman_client`, `package::install`, `stacks`); patching one covered - one — the strfry recreate went through another and the pin never fired. -- **Disk manifest edits do not apply to catalog-covered apps.** Even - `bind: 127.0.0.1` written into the node's strfry manifest was overridden by - the signed catalog. The catalog re-sign is REQUIRED; there is no shortcut. -- A loopback-bound host port is **unreachable** from a pasta container, so - loopback-pinning the Wyoming ports would break Home Assistant voice. - -### Open for 1.7.122 - -1. **Catalog re-sign** — `bind: 127.0.0.1` + `auth: session` on the ~39 gated - UI ports. This is what turns the gate from auditing into enforcing. Nothing - in code can substitute for it. -2. **Release-root rotation** — branch `rotate-release-root`, key - `did:key:z6Mkfu5LT…DLWT` / `1578adcc…4418`, validated as a real curve point. - **Sign the rotation release with the OLD key**; only the release after it - uses the new one. Re-sign the catalog too. -3. **Wyoming voice ports** (10200/10300/10400) — unauthenticated, and by the - operator's policy they should not be. Correct fix is co-locating Home - Assistant with the pine services on one container network so nothing is - published; needs a node running both. -4. **Item 2** filebrowser default login. **Items 6/6b** app updates + - multiversion (`versions[]` already exists, populated for 2 of 66 apps). -5. **`cargo-test-weekly` times out** at its 1500s cap on a loaded box — raise - the cap or split the stage; it is not a code failure. - -## RESUME HERE — next session - -**Landed this session (both pushed):** -- `c0cfc72a` federation trust escalation (items 3) — 42/42 federation tests green -- `5088aef5` lnd-ui OTA pin + host networking (item 4), and this task file - -**v1.7.120-alpha is SHIPPED** — signed, published, assets verified live. Do not re-cut it. - -### Start with item 3b (password gate) — groundwork already located - -Everything needed to implement it, so the next session does not re-search: - -- **The helper to use:** `self.auth_manager.verify_password(password).await?` — returns - `bool`. Existing callers to copy the shape from: `api/rpc/node.rs:176`, - `api/rpc/totp.rs:18` / `:66` / `:121`. -- **Entry point A — minting a Trusted invite:** `handle_federation_invite`, - `api/rpc/federation/handlers.rs:58`. It reads `trust_level` from params and - **defaults to `TrustLevel::Trusted` at :72**. Gate only when the resolved level is - `Trusted`; leave Observer invites unchanged. -- **Entry point B — the UI dropdown:** `handle_federation_set_trust`, - `api/rpc/federation/handlers.rs:326`, dispatched as `"federation.set-trust"` - (`api/rpc/dispatcher.rs:353`). Its parse is at `:342`. -- **Rule:** gate PROMOTION to Trusted only. Demotion must stay ungated — making something - less privileged must never be harder than leaving it. -- Set `TrustSource::Manual` on the operator path so the audit trail distinguishes a - deliberate grant from the capped automatic ones. -- Frontend will need the password prompt in both places (invite modal, node dropdown). - -### Then item 1 (app ports unauthenticated) — the big one - -Start with the research the operator explicitly asked for: how **umbrelOS** -(`getumbrel/umbrel`) and **StartOS** (`Start9Labs/start-os`) gate app access. Read their -model rather than inventing one. Only then design the gate. - -Give this a fresh session with real context — it is the largest item here and is the same -bug class as the `/lnd-connect-info` + `/bitcoin-rpc/` leaks fixed in v1.7.120, but across -every app port and every transport. - -### Working notes -- A separate agent is doing **AIUI planning with GSD** — do not touch AIUI. -- The shared tree has concurrent agents: stage by explicit path, never `git add -A`. -- Verify security claims **on the node**, not from source. v1.7.120's headline bug was a - fix that shipped in the binary and silently never reached the running container. -- A piped command's exit code is the pipe's, not the script's — redirect to a log file and - read the content. diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md deleted file mode 100644 index cd48075e..00000000 --- a/.planning/REQUIREMENTS.md +++ /dev/null @@ -1,171 +0,0 @@ -# Requirements: Archipelago (v1.8.0 — Developer-Ready App Platform) - -**Defined:** 2026-07-29 -**Core Value:** A third-party developer can publish an app via the signed/decentralized registry and a user can install it on their node — manifest-driven, rootless, secure, robust. - -No PRDs existed in the ingest set; these requirements are derived from the master plan's -declared exit criteria (multinode pass + workstreams B/C/F), `.planning/codebase/CONCERNS.md`, -`docs/UNIFIED-TASK-TRACKER.md`, and the user-chosen success metric. Constraints from -`docs/app-manifest-spec.md` and the locked ADRs (see PROJECT.md) bound how each is built. - -## v1 Requirements - -### Federation & Mesh Hardening (FED) - -- [ ] **FED-01**: Removing a federation node sticks — it disappears from every UI surface, tombstones propagate, it never reappears via later sync cycles, and a failed removal surfaces an error (never a silent no-op) -- [ ] **FED-02**: Federation sync converges and is observable — after sync settles, fleet nodes agree on the node list with fresh status; stale entries, duplicates, and silent sync failures are eliminated and sync errors are operator-visible -- [ ] **FED-03**: A structured code review of the federation/fleet area (`core/archipelago/src/federation`, node sync, FIPS/transport dial layer) and mesh area (`core/archipelago/src/mesh`, mesh RPC surface) is completed, with every finding fixed or explicitly deferred with a reason -- [x] **FED-04**: Mesh messaging parity — attachment send (and the rest of the mesh chat surface) behaves identically on the demo and on real nodes: the demo backend implements the same RPC surface the UI calls, transport decisions mirror the real size-based tier logic, and no demo-only modals exist -- [ ] **FED-05**: Inter-node Lightning channel opening UX — the UI shows the node's shareable Lightning URI; lists trusted (federated) nodes by hostname for one-click channel opening; and lets the user browse/request channels with public nodes — using the existing design system and components, verified on the :8100 dev preview against archi-dev before deploy -- [x] **FED-06**: On-brand payment success animation — the invoice "paid" tick's circle uses the screensaver-style ring with outer EQ-segment lines (reuse `ScreensaverRing.vue`'s compact size) in place of the current success burst, applied consistently everywhere the paid tick shows -- [x] **FED-08**: Lightning invoices created by the wallet embed route hints (LND `private` flag) so nodes whose channels are unannounced can actually receive payments — diagnosed on archy-x250-mad2 2026-07-31, where every wallet-UI invoice had `route_hints: []` and was unroutable; the bug is unconditional and affects any node without a public channel -- [x] **FED-09**: The container doctor does not restart Tor on every run — it recognises Tor's own setgid `2700` hidden-service directory mode as correct rather than "fixing" it to `700` and restarting, a loop that reset Tor every ~5 minutes, starved it of its consensus/HSDir cache (`No more HSDir available to query`), and broke the mesh's Tor fallback entirely; genuinely permissive modes are still corrected, and a restart backoff makes the failure class non-recurring -- [x] **FED-07**: Fedimint gateway never installs with a pre-set password — gateway credentials are generated per-install via manifest-declared `generated_secrets` (or explicitly set by the user), never baked into the image/manifest; existing installs with the default password get a migration path (BLOCKER — default credentials are a security hole) - -### UI Fixes (UIFIX) — user-reported blockers, added 2026-07-30 - -- [ ] **UIFIX-01**: The FIPS/Tor pills on cloud files are kept (never removed by cleanups) and render at mobile widths — on mobile, users can see each file's security/transport state (BLOCKER) -- [x] **UIFIX-02**: The connected-nodes list scrolls at row-matched height — its height tracks the taller right-hand sibling in the row and the inner list scrolls within it, never growing to fit all rows scroll-free (BLOCKER) -- [x] **UIFIX-03**: On short viewports the onboarding confirmation tickbox is discoverably visible — an on-brand affordance (scroll cue, sticky footer, or equivalent) makes it obvious without altering tall-screen appearance (BLOCKER) -- [x] **UIFIX-04**: Paid Files pictures open in the app's lightbox, not a browser tab — consistent with the rest of the app's media UX -- [x] **UIFIX-05**: Picture-in-picture is robust — entering PiP closes the lightbox with a fluid on-brand animation, and an active PiP session survives main-tab changes and video buffering pauses (only an explicit user stop ends it) -- [x] **UIFIX-06**: Surfaces with genuinely slow opens show house-style loader states — no dead-feeling clicks (cached revisits stay spinner-free per PERF-02) - -### UI Performance (PERF) - -- [x] **PERF-01**: The slowest tab switches and secondary-screen opens are profiled with causes named (remount storms, serial RPC waterfalls, uncached fetches) — fixes are targeted, not guessed -- [x] **PERF-02**: Main-tab switches render immediately from cached state with background refresh — no blank screens or long spinners on tabs already visited this session -- [x] **PERF-03**: Secondary screens (screens reached from a tab's main page) open without a blocking full reload and are instant on repeat visits — verified on real node hardware, not just the dev box - -### Multinode Verification (MNODE) - -- [ ] **MNODE-01**: The 5× destructive lifecycle gate passes on a second fleet node (archy-x250-beta) with 0 failures, run on-node per gate policy -- [ ] **MNODE-02**: Cross-node federation/mesh/transport suites (`tests/multinode/smoke.sh`, `meshtastic.sh`) pass between fleet nodes, with all harness RPC calls time-bounded (no indefinite curl hangs) -- [ ] **MNODE-03**: Removing a federation peer sticks — tombstone-write failures are surfaced (not swallowed) and a removed peer never silently reappears after subsequent sync cycles - -### Lifecycle Perfection (LIFE) - -- [ ] **LIFE-01**: Quadlet backends are the default — restarting `archipelago.service` leaves every app container running (no SIGKILL-the-world, no multi-minute rebuild storm) -- [ ] **LIFE-02**: The reconciler self-heals failed Quadlet units — a `.service` in `failed` state (and not user-stopped) is reset-failed + started automatically, with backoff against busy-looping -- [ ] **LIFE-03**: Per-app restart/flap observability — restart counters, a threshold log line when an app restarts >N times in M minutes, and restart counts surfaced in health/status RPC output -- [ ] **LIFE-04**: Cascade uninstall→reinstall is gate-verified for multi-container stacks and installed apps — no ghost entries, no orphan containers, data preserved per policy, reinstall returns healthy -- [ ] **LIFE-05**: Install and uninstall report real, monotonic progress driven by backend progress events, always reaching a terminal success/failure state — asserted in the gate, never a fake or stuck bar - -### Registry-Distributed Manifests (REG) - -- [ ] **REG-01**: The published signed catalog embeds full app manifests; nodes install/update from signature-verified catalog manifests (disk manifests remain the fallback for build-source apps); tampered catalogs are rejected with safe fallback -- [ ] **REG-02**: The fleet is flipped to registry-distributed manifests — adding or bumping an image-only app requires only a re-signed catalog publish, no binary OTA or disk rsync - -### Security Enforcement (SEC) - -- [ ] **SEC-01**: `AppManifest::validate()` enforces the full ADR-009 mandate set — non-root UID, pinned image tags (no `latest`), capability allow-list, seccomp — with explicit, documented, auditable overrides -- [ ] **SEC-02**: Generated AppArmor/seccomp security profiles are actually applied at container creation (`--security-opt`) and verified effective on running apps - -### Developer Tooling (DEV) - -- [ ] **DEV-01**: `archy app validate` checks a manifest locally and returns the same pass/fail verdict the node enforces (schema + security rules) -- [ ] **DEV-02**: `archy app render` previews the exact Quadlet/podman configuration a manifest produces -- [ ] **DEV-03**: A developer can local-install and lifecycle-test an app against a dev node from the CLI (`archy app local-install` / `lifecycle-test`) -- [ ] **DEV-04**: The developer guide walks a new third-party developer from an empty directory to an installed, running app using only the CLI and docs - -### Decentralized Marketplace (MKT) - -- [ ] **MKT-01**: A third-party developer can publish a DID-signed app manifest to public Nostr relays (NIP-78, kind 30078) via the tooling -- [ ] **MKT-02**: A node discovers marketplace apps from multiple relays and displays each app's trust tier (Verified / Community / Unverified) per ADR-006 trust scoring -- [ ] **MKT-03**: Manifest signatures are verified before installation; tampered or invalid marketplace manifests cannot be installed -- [ ] **MKT-04**: End-to-end north star: a user installs a third-party marketplace-published app on their node and it runs healthy under the standard lifecycle guarantees - -### AIUI — Conversational Node Control & Content Surfaces (AIUI) — added 2026-08-03 - -- [ ] **AIUI-01**: Human-language node control — a typed request in AIUI chat ("restart bitcoin", "how much space is left", "who's connected") reaches a real node action and returns a real result, over a permissioned tool-calling bridge rather than raw RPC -- [ ] **AIUI-02**: Conversational settings — the system settings surfaced across neode-ui become reachable by conversation, scoped to what the user has granted -- [ ] **AIUI-03**: Content surfaces made real — AIUI's designed-but-empty content views render live node data (peer files, music, IndeeHub movies, owned/paid content); audio belongs to the global bottom-bar player and media streams via Range requests, never base64 blobs -- [ ] **AIUI-04**: Sandboxed by construction, permissioned by the user — secrets never reach the browser or the model context; the chat gets an explicit, user-granted, default-closed, revocable capability scope; destructive and identity-touching operations are human-confirmed; tool authority never derives from peer-controlled content (BLOCKER) -- [ ] **AIUI-05**: Delivery and build — AIUI reaches nodes on a delivery path an operator can actually receive updates through, with `VITE_BASE_PATH=/aiui/` enforced by the build script so a hand-built bundle cannot ship a black page -- [ ] **AIUI-06**: Verified on device — in the real embedded iframe on archi-dev-box, mobile included, not only in the local `dev:mock` loop - -## v2 Requirements - -Deferred to a future milestone. Tracked but not in the current roadmap. - -### Distribution Backbone (DIST) - -- **DIST-01**: BLAKE3 content-addressed catalog distribution via iroh swarm, origin-always-wins (workstream D — design-only today, tracker-marked backlog) - -### Fleet & Hardening (FLEET) - -- **FLEET-01**: Bitcoin multi-version fleet-wide OTA rollout (user-gated on timing per `docs/bitcoin-version-bulletproof-rollout.md`) -- **FLEET-02**: App-specific health assertions for the ~34 apps with only baseline lifecycle coverage -- **FLEET-03**: LUKS2 full-partition encryption for `/var/lib/archipelago/` -- **FLEET-04**: Dynamic per-app resource rebalancing (cgroup-stats feedback loop) - -## Out of Scope - -| Feature | Reason | -|---------|--------| -| Rootful/privileged containers, Docker | Invariant — ADR-001/ADR-009 | -| Per-app Rust installers / host provisioning | The anti-pattern workstream A deleted | -| Centralized gatekept app store | ADR-006 chose decentralized Nostr marketplace | -| Web5 DWN spec compliance | ADR-011 — deprioritized after TBD shutdown | -| Custom live voice-call protocol | Deprioritized 2026-07-01 per user; no scope decided | - -## Traceability - -Which phases cover which requirements. Updated during roadmap creation. - -| Requirement | Phase | Status | -|-------------|-------|--------| -| FED-01 | Phase 1 | Pending | -| FED-02 | Phase 1 | Pending | -| FED-03 | Phase 1 | Pending | -| FED-04 | Phase 1 | Complete | -| FED-05 | Phase 1 | Pending | -| FED-06 | Phase 1 | Complete | -| FED-07 | Phase 1 | Complete — rotation + recreate verified on archi-dev-box 2026-08-02 | -| FED-08 | Phase 1 | Code complete + unit-pinned; post-OTA check on the user device pending | -| FED-09 | Phase 1 | Complete — 15h Tor uptime / 0 permission-fixes on archi-dev-box; onion-resolution check post-OTA | -| UIFIX-01 | Phase 1 | Pending | -| UIFIX-02 | Phase 1 | Complete | -| UIFIX-03 | Phase 1 | Complete | -| UIFIX-04 | Phase 1 | Complete | -| UIFIX-05 | Phase 1 | Complete | -| UIFIX-06 | Phase 1 | Complete | -| PERF-01 | Phase 2 | Complete | -| PERF-02 | Phase 2 | Complete. 02-11 (`02-FINDINGS.md` § Client-Side Render Cost Root Cause + § Task 3) named and fixed the real cause of Web5/Server's revisit-ms regressions — three leaked background pollers (`useFleetData.ts`, `FipsNetworkCard.vue`, `Web5Monitoring.vue`) armed in `onMounted` and never disarmed once their owning views joined `KEEP_ALIVE_PATHS`, gated to activate/deactivate. Web5 now fixed (275ms, below both its 566ms pre-phase-2 baseline and the 300ms pass bar); Server's regression is closed (574ms, below its 738ms baseline) though not yet under the 300ms stretch target — residual named as real, un-eliminated per-resource reactivation cost, not a new defect | -| PERF-03 | Phase 2 | Complete. 02-11 fixed Fleet's leaked `useFleetData.ts` poll (790ms, down from a 2631ms regression, substantially closing the gap to its 330ms baseline). AppDetails restored to at/near its own baseline (1231ms vs. 1204ms) — residual is the already-documented `useCachedResource` per-mount setup cost, not fixed further. Discover (1389ms) has a SECOND, distinct, evidenced cause found this session (CSS entrance-animation replay on KeepAlive reactivation, `card-stagger`/`showStagger` never removed from the DOM) — named with full profiling/diagnostic evidence but NOT fixed (blast radius spans 5+ files outside this plan's scope, needs its own real-device verification budget) — recommended as a dedicated follow-up. OpenWrtGateway: not measurable this pass (Chromium crash cascading from an unrelated surface); prior numbers stand, confirmed to reflect a real (not empty) disconnected-device UI render, not retracted | -| MNODE-01 | Phase 3 | Pending | -| MNODE-02 | Phase 3 | Pending | -| MNODE-03 | Phase 3 | Pending | -| LIFE-01 | Phase 4 | Pending | -| LIFE-02 | Phase 4 | Pending | -| LIFE-03 | Phase 4 | Pending | -| LIFE-04 | Phase 4 | Pending | -| LIFE-05 | Phase 4 | Pending | -| REG-01 | Phase 5 | Pending | -| REG-02 | Phase 5 | Pending | -| SEC-01 | Phase 6 | Pending | -| SEC-02 | Phase 6 | Pending | -| DEV-01 | Phase 7 | Pending | -| DEV-02 | Phase 7 | Pending | -| DEV-03 | Phase 7 | Pending | -| DEV-04 | Phase 7 | Pending | -| MKT-01 | Phase 8 | Pending | -| MKT-02 | Phase 8 | Pending | -| MKT-03 | Phase 8 | Pending | -| MKT-04 | Phase 8 | Pending | -| AIUI-01 | Phase 13 | Pending | -| AIUI-02 | Phase 13 | Pending | -| AIUI-03 | Phase 13 | Pending | -| AIUI-04 | Phase 13 | Pending | -| AIUI-05 | Phase 13 | Pending | -| AIUI-06 | Phase 13 | Pending | - -**Coverage:** - -- v1 requirements: 35 total -- Mapped to phases: 35 -- Unmapped: 0 - ---- -*Requirements defined: 2026-07-29* -*Last updated: 2026-07-29 — added FED (federation/mesh hardening) and PERF (UI performance) requirement groups; phases renumbered after inserting them as Phases 1–2* diff --git a/.planning/RESUME-2026-08-05-appgate-fixes.md b/.planning/RESUME-2026-08-05-appgate-fixes.md deleted file mode 100644 index 743227c4..00000000 --- a/.planning/RESUME-2026-08-05-appgate-fixes.md +++ /dev/null @@ -1,113 +0,0 @@ -# Resume — 2026-08-05 (app gate, releases .122–.125) - -Paste the block at the bottom into a new session. - -## Where things stand - -- **v1.7.124-alpha is SHIPPED** (signed with the NEW root, published, verified). -- **Signed catalog is LIVE** carrying two hotfixes made after .124: - the repaired bitcoin start script and the fedimint 8175 removal. - Last commit: `4ace62fa`. -- **Release-root rotation is COMPLETE.** .122 was the last release signed with - the old key; .123/.124 and all catalogs use the new one. No override needed. - -## Two bugs I introduced in .124 (both fixed, both instructive) - -1. **Bitcoin vanished from every node.** I put a `#` comment INSIDE the - manifest's folded YAML scalar (`>-`), where `#` is not a comment — it - reaches the shell, and folding joins lines with spaces so it commented out - the `if ... then` while the more-indented `echo` survived, leaving an orphan - `fi`. Container exited instantly; app detection is container-based so the - app disappeared. **Guard added:** `scripts/check-manifest-shell.py` runs - `sh -n` over every embedded manifest script and rejects `#` in these - scalars; wired into `tests/release/run.sh`. -2. **Fedimint crash-looped.** I declared port 8175 on the `fedimint` app so the - gate could name it — but 8175 is served by the separate `archy-fedimint-ui` - companion. The orchestrator then tried to publish 8175 from fedimintd, - collided, and `start_container` failed forever. Removed. **Rule: never - declare a port on an app whose container does not actually serve it.** - -Also: I published an UNSIGNED catalog at one point, which nodes correctly -reject — they silently keep their old cached copy. **Always verify -`'signature' in catalog` on the live URL after publishing.** - -## OPEN TASKS - -1. **indeedhub crash-loop — NOT mine, needs a real fix.** `indeedhub-minio` is - **absent** on `.38` and `.88`, so nginx fails with - `host not found in upstream "minio"` and both `indeedhub` and - `indeedhub-api` exit(1). The stack member never gets created. Look at - `api/rpc/package/stacks.rs` + `dependencies.rs`. -2. **Verify `.38` refetched the signed catalog** and bitcoin-knots starts. - `.88` already did (signed: True, script fixed). -3. **Deploy the .125 build to archi-dev-box for operator confirmation.** - Binary is built at `core/target/release/archipelago` with: app-login page - using the sidebar **A mark** (`favico-black-v2.svg`) not the wordmark; - page pinned to `100svh` + `position:fixed` so mobile stays centred and the - keyboard overlays instead of scrolling; install-version modal icon uses - `object-contain` so non-square icons are not cropped. **Operator has not - seen these yet.** -4. **Cut v1.7.125-alpha** once confirmed. Sign with the **NEW** mnemonic. - -## Traps that cost time today - -- `create-release.sh` says "sign, then re-run" — **re-running regenerates the - manifest and DESTROYS the signature**, and its clean-tree check blocks - anyway. Do steps 7/8 by hand: `git add` version+changelog+manifest → - commit `chore: release vX` → `git tag -a vX` → push main → **push the tag - explicitly** → `git ls-remote --tags` to prove it → `publish-release-assets.sh`. -- The release gate's `cargo-test-weekly` times out on the **compile** after any - version bump. Pre-warm: `CARGO_INCREMENTAL=0 cargo test --manifest-path - core/Cargo.toml -p archipelago --no-run`. -- The frontend version check fails until the in-app **What's New** block for - that version exists (`neode-ui/src/views/settings/AccountInfoSection.vue`) — - that string is what it greps for. -- `generate-app-catalog.py` writes `APP_LAUNCH_PORTS` one-per-line; rustfmt - packs it, so run `cargo fmt` after any catalog sync or the gate fails. -- **Manifest changes reach nodes via the SIGNED CATALOG, not the binary.** A - manifest hotfix needs only a catalog re-sign — no release. - -## Fleet - -SSH: `sshpass -p '' ssh archipelago@` (note the `!`; `@` -is older and still works on some). RPC/node password differs per node — the -`!` one failed RPC login on `.38`. - -- `100.69.68.39` archi-dev-box — dev target -- `100.82.34.38` archipelago-1 -- `100.70.96.88` austin-sapien -- `100.64.204.114` .228 shorty-s — **in real use, treat carefully** - -**Force a catalog refresh on a node:** Settings → App Updates → Check for -updates, or `sudo rm -f /var/lib/archipelago/app-catalog.json && sudo -systemctl restart archipelago`. - -**All fleet nodes were repaired** from `Restart=on-failure` → -`Restart=always`; a node with the old value stays DEAD after an in-process -update (the updater exits cleanly and systemd reads that as success). -`bootstrap::ensure_restart_policy()` now self-heals it. - ---- - -## PASTE THIS INTO THE NEW SESSION - -Resume the archy work from 2026-08-05. Read -`.planning/RESUME-2026-08-05-appgate-fixes.md` and the memory notes -`project_fleet_ota_restart_policy_incident` and -`project_v1_7_121_shipped_appgate` first. - -v1.7.124-alpha is shipped and the signed catalog is live with two hotfixes -(bitcoin start script, fedimint 8175). Four things are open, in order: - -1. Fix the indeedhub crash-loop: `indeedhub-minio` is absent on .38 and .88 so - nginx fails on upstream "minio" and indeedhub + indeedhub-api exit(1). This - one is pre-existing, not from the port work. -2. Verify .38 refetched the signed catalog and bitcoin-knots starts (.88 - already did). -3. Deploy the built .125 binary + frontend to archi-dev-box (100.69.68.39) so - I can confirm the app-login page (A mark, mobile centring, keyboard - behaviour) and the install-modal icon. -4. Then cut v1.7.125-alpha — I sign with the new mnemonic. - -Do not re-run create-release.sh after signing; it destroys the signature — -do the commit/tag/publish steps by hand as the resume doc describes. diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md deleted file mode 100644 index 5b42df9f..00000000 --- a/.planning/ROADMAP.md +++ /dev/null @@ -1,395 +0,0 @@ -# Roadmap: Archipelago — v1.8.0 Developer-Ready App Platform - -## Overview - -Brownfield milestone starting from a green single-node production gate (5/5 on .228, -2026-06-23). The journey: make federation and mesh rock-solid (node removal, sync, -messaging parity), fix the UI slowness users feel on every tab switch, prove the platform -across the fleet (multinode pass), make the container lifecycle bulletproof (Quadlet -default, self-healing, honest progress, no ghosts), flip manifest distribution from OTA -disk files to the signed registry, harden manifest security enforcement to the full -ADR-009 bar, ship the `archy app` developer CLI, and land the decentralized Nostr -marketplace — ending at the north star: a third-party developer publishes an app via the -signed/decentralized registry and a user installs it on their node. - -## Phases - -**Phase Numbering:** - -- Integer phases (1, 2, 3): Planned milestone work -- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) - -- [ ] **Phase 1: Federation & Mesh Hardening** - Deep review of federation/fleet + mesh code; node removal sticks, sync converges, mesh messaging behaves identically on demo and real nodes -- [x] **Phase 2: UI Performance** - Tab switches and secondary screens render fast; worst transitions measured and fixed (completed 2026-07-31) -- [ ] **Phase 3: Multinode Verification Pass** - Lifecycle gate green on a second node; cross-node federation/mesh/transport suites pass; federation removal sticks -- [ ] **Phase 4: Lifecycle Perfection & Quadlet Default** - Quadlet backends default, failed-unit self-healing, flap observability, cascade gate, truthful progress -- [ ] **Phase 5: Registry-Distributed Manifests** - Signed catalog carries full manifests; fleet flipped off OTA disk-file distribution -- [ ] **Phase 6: Manifest Security Enforcement** - Validation matches ADR-009 mandates; generated security profiles actually applied -- [ ] **Phase 7: Developer Tooling CLI** - `archy app validate/render/local-install/lifecycle-test` + developer guide -- [ ] **Phase 8: Decentralized Marketplace** - DID-signed publish to Nostr relays, trust-tier discovery, verified third-party install end-to-end -- [ ] **Phase 9: BotFights Platform Upgrade** - Native nostr signer login, one self-contained AI bot-setup prompt, shared public VPS2 match endpoint so all nodes see all fighters, registry updated -- [ ] **Phase 12: Bitcoin Node Settings & Core/Knots Parity** - Every bitcoind option reachable in the UI, Knots-only options gated to Knots, network mode a setting defaulting to Tor -- [ ] **Phase 13: AIUI — Conversational Node Control & Content Surfaces** - Human-language node control and settings in AIUI chat, its designed content surfaces wired to real peer/music/movie data, all inside a user-granted capability sandbox that keeps keys and secrets away from the browser and the model - -## Phase Details - -### Phase 1: Federation & Mesh Hardening - -**Goal**: Federation and mesh are tight — a structured review of the fleet/federation and mesh code feeds fixes so node removal sticks, sync converges, and mesh messaging (including attachments) behaves identically everywhere it runs -**Depends on**: Nothing (first phase) -**Requirements**: FED-01, FED-02, FED-03, FED-04, FED-05, FED-06, FED-07, UIFIX-01, UIFIX-02, UIFIX-03, UIFIX-04, UIFIX-05, UIFIX-06, FED-08, FED-09 -**Success Criteria** (what must be TRUE): - - 1. A structured code review of the federation/fleet area (`core/archipelago/src/federation`, node sync, FIPS/transport dial layer) and the mesh area (`core/archipelago/src/mesh`, mesh RPC surface) produces a findings list, and every finding is fixed or explicitly deferred with a reason - 2. Removing a federation node removes it everywhere — it disappears from all UI surfaces, tombstones propagate, and it never reappears after later sync cycles; a failed removal surfaces an error instead of silently no-opping - 3. Federation sync converges: after sync settles, fleet nodes agree on the node list and node status is fresh — stale entries, duplicates, and silent sync failures are gone, and sync errors are visible to the operator - 4. Mesh attachment send works identically on the demo and on real nodes — same modals, same transport decisions, same success — with the demo backend implementing the same RPC surface the UI calls (no "Method not found", no demo-only chooser modal) - 5. Channel-opening between nodes is first-class UI: a user can copy/share their node's Lightning URI; sees a list of trusted (federated) nodes by hostname to open a channel with in one flow; and can browse/request channels with public nodes — built with the existing design system (Teleport-to-body modals, house style), tested live on the :8100 dev preview against archi-dev, and fixed there before any deploy - 6. The invoice/payment "paid" success animation is on-brand: the tick's circle is the screensaver-style ring with the outer EQ-segment lines (reuse `neode-ui/src/components/ScreensaverRing.vue`, which already ships a `compact` overlay size), replacing the current burst in the payment success pane (`neode-ui/src/components/SendBitcoinModal.vue`) and matching wherever else the paid tick appears - 7. Fedimint gateway installs have no pre-set password (BLOCKER, added 2026-07-30): a fresh install generates its gateway credentials per-install via manifest-declared `generated_secrets` (per the repo secrets invariant) or requires the user to set one — never a baked-in default; existing installs carrying the default password are migrated or flagged. NOTE: phase 1's 10 plans predate this criterion — an additional gap plan is required before phase 1 execution completes - 8. The FIPS/Tor pills on cloud files are kept and visible at mobile widths (UIFIX-01, BLOCKER, added 2026-07-30 — see `.planning/todos/pending/2026-07-30-keep-fips-tor-pills-on-cloud-files-and-show-them-on-mobile.md`) - 9. The connected-nodes list scrolls at row-matched height instead of growing to fit (UIFIX-02, BLOCKER, added 2026-07-30 — see `.planning/todos/pending/2026-07-30-connected-nodes-list-must-scroll-at-row-matched-height.md`) - 10. The onboarding tickbox is discoverably visible on short viewports via an on-brand affordance (UIFIX-03, BLOCKER, added 2026-07-30 — see `.planning/todos/pending/2026-07-30-onboarding-tickbox-hidden-below-fold-on-short-screens.md`) - 11. Paid Files pictures open in the app lightbox, not a browser tab (UIFIX-04, added 2026-07-30 — see `.planning/todos/pending/2026-07-30-peer-files-pictures-open-in-tab-not-lightbox.md`) - 12. Picture-in-picture closes the lightbox with a fluid on-brand animation (UIFIX-05, added 2026-07-30 — see `.planning/todos/pending/2026-07-30-pip-should-close-lightbox-with-fluid-animation.md`) - 13. Genuinely slow opens show loader states (UIFIX-06, added 2026-07-30 — see `.planning/todos/pending/2026-07-30-missing-loader-states-on-slow-opens.md`; 02-08's flagged timing regressions are the starting inventory) - NOTE for criteria 7–13: all were added after phase 1's 10 plans were written — before phase 1 execution completes, create gap plan(s) covering FED-07 + UIFIX-01..06 (existing desktop visuals must remain untouched per the standing visual-invisibility rule; UIFIX items themselves are user-approved visual changes) - -**Plans**: 11/20 plans executed - -Plans: - -- [x] 01-20-PLAN.md — URGENT wave 1: doctor stops restarting Tor every 5min (mesh Tor fallback) (FED-09) -- [x] 01-19-PLAN.md — URGENT wave 1: wallet invoices embed route hints so private-channel nodes can receive (FED-08) -- [x] 01-01-PLAN.md — Serialize the federation node store and make removal stick (FED-01) -- [x] 01-02-PLAN.md — Demo mesh/federation RPC parity + automated parity harness (FED-04) -- [x] 01-03-PLAN.md — On-brand paid tick: ScreensaverRing badge variant on both success surfaces (FED-06) -- [ ] 01-04-PLAN.md — Lightning identity: own-node URI + meshed Lightning peer discovery (FED-05) -- [ ] 01-05-PLAN.md — Federation sync convergence and operator-visible sync errors (FED-02) -- [ ] 01-06-PLAN.md — Lightning URI on the federation sync payload, sharing default decided (FED-05) -- [ ] 01-07-PLAN.md — Channel-open request messaging over the mesh (FED-05) -- [ ] 01-08-PLAN.md — Channel-open UX: own URI, trusted-node picker, meshed-peer requests (FED-05) -- [ ] 01-09-PLAN.md — Structured federation/mesh review + dev-pair deploy (FED-03) -- [ ] 01-10-PLAN.md — Consolidated phase verification on the dev pair (FED-01/02/05/06) - -**Wave 7** *(gap closure — criteria 7–13, added 2026-07-30 after the original 10 plans were written)* - -- [x] 01-11-PLAN.md — No baked-in Fedimint gateway credential: per-install secret on every path (FED-07) -- [x] 01-12-PLAN.md — Connected-nodes list scrolls at row-matched height instead of growing (UIFIX-02) -- [x] 01-13-PLAN.md — On-brand scroll cue makes the onboarding tickbox findable on short screens (UIFIX-03) -- [x] 01-14-PLAN.md — Paid Files open in the app lightbox, with a visible wait and a real error path (UIFIX-04/06) -- [x] 01-15-PLAN.md — PiP hands off from the lightbox and survives tab changes and buffering (UIFIX-05) - -**Wave 8** *(blocked on Wave 7 completion)* - -- [x] 01-16-PLAN.md — Migrate existing installs off the default gateway credential, data preserved (FED-07) -- [ ] 01-17-PLAN.md — FIPS/Tor pills pinned against removal and readable at phone widths (UIFIX-01) - -**Wave 9** *(blocked on Wave 8 completion)* - -- [ ] 01-18-PLAN.md — Six-fix sign-off on archi-dev-box (UIFIX-01/02/03/04/05/06) - -**UI hint**: yes - -### Phase 2: UI Performance - -**Goal**: The UI feels fast — switching tabs and opening secondary screens (screens reached from a tab's main page) renders promptly instead of stalling on refetches and remounts -**Depends on**: Nothing (frontend-focused; parallelizable with Phase 1) -**Requirements**: PERF-01, PERF-02, PERF-03 -**Success Criteria** (what must be TRUE): - - 1. The slowest tab switches and secondary-screen opens are profiled and the causes named (remount storms, serial RPC waterfalls, uncached fetches) before fixes land - 2. Switching between main tabs renders the target view immediately from cached state, refreshing data in the background — no blank screens or long spinners on tabs already visited this session - 3. Secondary screens open without a blocking full reload; repeat visits are instant - 4. The fixes are verified on real node hardware (not just the dev box) — the sluggishness the user reported is gone on-device - -**Plans**: 11/11 plans executed - -Plans: -**Wave 1** - -- [x] 02-01-PLAN.md — Profile every D-09 surface on archi-dev-box and commit the findings doc (PERF-01) - -**Wave 2** *(blocked on Wave 1 completion)* - -- [x] 02-02-PLAN.md — TRACER: KeepAlive host, hook reactivation, app-store tab, refresh indicator (PERF-02) -- [x] 02-03-PLAN.md — Secondary screens: per-item cache, parallel loads, purge on logout (PERF-03) - -**Wave 3** *(blocked on Wave 2 completion)* - -- [x] 02-04-PLAN.md — Keep every main tab alive safely: lifecycle audit + full registration (PERF-02) - -**Wave 4** *(blocked on Wave 3 completion)* - -- [x] 02-05-PLAN.md — Mesh: cache the six fetch groups, bound the D3 graph and Leaflet map (PERF-02) -- [x] 02-06-PLAN.md — Server and Home: cache the uncached fan-out, guarantee wallet freshness (PERF-02) -- [x] 02-07-PLAN.md — Chat/AIUI: stable embed URL + the two D-14 UX defaults (PERF-02) - -**Wave 5** *(blocked on Wave 4 completion)* - -- [x] 02-08-PLAN.md — Dev-pair deploy, on-device re-measure, D-11 pass bar (PERF-01/02/03) - -**Wave 6** *(gap closure — blocked on Wave 5 completion)* - -- [x] 02-09-PLAN.md — Server.vue KeepAlive remount: name the cause, fix it, pin it (PERF-02) - -**Wave 7** *(gap closure — blocked on Wave 6 completion)* - -- [x] 02-10-PLAN.md — Timing-regression verdict: three-way re-measure, clear or name each surface (PERF-02/03) - -**Wave 8** *(gap closure — blocked on Wave 7 completion)* - -- [x] 02-11-PLAN.md — Profile the real cause of the six confirmed regressions, fix what's fixable, re-measure (PERF-02/03) - -**UI hint**: yes - -### Phase 3: Multinode Verification Pass - -**Goal**: The platform's lifecycle and federation guarantees are proven across the fleet, not just on .228 — the declared next exit criterion -**Depends on**: Phase 1 (proves the federation/mesh fixes hold fleet-wide) -**Requirements**: MNODE-01, MNODE-02, MNODE-03 -**Success Criteria** (what must be TRUE): - - 1. The 5× destructive lifecycle gate reports 0 failures on a second fleet node (archy-x250-beta), run on-node - 2. The cross-node smoke suite (federation pairing both directions, FIPS anchors, peer content browse) passes between two fleet nodes with every harness RPC time-bounded — a slow node produces a test failure, never an indefinite hang - 3. An operator who removes a federation peer never sees it reappear in the peer list after later sync cycles; a tombstone-write failure is surfaced as an error instead of silently swallowed - 4. The on-air mesh suite passes between two radio-equipped nodes over real RF - -**Plans**: TBD - -### Phase 4: Lifecycle Perfection & Quadlet Default - -**Goal**: An insanely-reliable container environment — every app installs, runs, restarts, uninstalls, and reinstalls cleanly with honest progress, no ghosts, and automatic recovery -**Depends on**: Phase 3 (Quadlet default-flip is gated on the second-node gate reporting clean) -**Requirements**: LIFE-01, LIFE-02, LIFE-03, LIFE-04, LIFE-05 -**Success Criteria** (what must be TRUE): - - 1. Restarting `archipelago.service` on a fleet node leaves every app container running — no SIGKILL-the-world, no multi-minute reconciler rebuild - 2. An app whose Quadlet unit enters `failed` state (and was not user-stopped) comes back automatically within a bounded window, with backoff on persistent failure — no operator intervention - 3. An operator can see per-app restart counts in status output, and a flapping app (>N restarts in M minutes) is flagged in logs instead of being invisible - 4. Uninstalling then reinstalling any gated app — including multi-container stacks like immich/btcpay — leaves no ghost My-Apps entries or orphan containers, preserves data per policy, and returns the app healthy, verified by the cascade gate tier - 5. Install and uninstall progress bars move monotonically from real backend progress events and always land on a terminal success/failure state — asserted in the gate, and the single-node gate stays green after all orchestrator changes - -**Plans**: TBD -**UI hint**: yes - -### Phase 5: Registry-Distributed Manifests - -**Goal**: Manifests ship via the signed registry, not OTA disk files — bumping or adding an app becomes a signed catalog change -**Depends on**: Phase 4 (fleet lifecycle stable under Quadlet default before changing the distribution channel) -**Requirements**: REG-01, REG-02 -**Success Criteria** (what must be TRUE): - - 1. A fleet node installs and updates an image-only app from the full manifest embedded in the signed catalog, verified against the pinned release-root key, with no corresponding OTA disk file present (disk remains the fallback for build-source apps) - 2. A tampered or unsigned catalog manifest is rejected and the node falls back safely — it never installs from an unverified manifest - 3. Bumping an app version fleet-wide requires only regenerating, re-signing, and publishing the catalog — no binary OTA, no disk rsync — proven live on the fleet - -**Plans**: TBD - -### Phase 6: Manifest Security Enforcement - -**Goal**: A third-party manifest cannot weaken node security — declared security policy is fully validated and actually enforced at runtime -**Depends on**: Phase 5 (enforcement guards the registry channel third-party manifests will arrive through) -**Requirements**: SEC-01, SEC-02 -**Success Criteria** (what must be TRUE): - - 1. A manifest violating ADR-009 mandates (root user, unpinned `latest` tag, capability outside the allow-list, disabled seccomp) is rejected at validation with a clear error naming the violation - 2. Security overrides (`readonly_root: false`, extra capabilities) work only when explicitly listed in the manifest and leave an audit trail - 3. Generated AppArmor/seccomp profiles are applied to containers at creation and verifiably effective on a running app — not just generated and ignored - 4. The single-node lifecycle gate stays green with enforcement on — existing catalog apps all pass the strengthened validation (or carry documented overrides) - -**Plans**: TBD - -### Phase 7: Developer Tooling CLI - -**Goal**: A third-party developer can build, validate, and test an Archipelago app locally without reading platform internals -**Depends on**: Phase 6 (CLI validation must mirror the final enforced rule set) -**Requirements**: DEV-01, DEV-02, DEV-03, DEV-04 -**Success Criteria** (what must be TRUE): - - 1. A developer runs `archy app validate` on a manifest directory and gets the same pass/fail verdict — including security rules — that a node would enforce at install - 2. A developer runs `archy app render` and sees the exact Quadlet/podman configuration their manifest produces before ever touching a node - 3. A developer can install their app onto a dev node and run its lifecycle test (install/UI/stop/start/restart/uninstall) from the CLI - 4. A new developer following only the developer guide goes from an empty directory to a running app on a node — no tribal knowledge required - -**Plans**: TBD - -### Phase 8: Decentralized Marketplace - -**Goal**: The north star — third-party developers publish apps via the decentralized registry and users install them on their nodes -**Depends on**: Phase 7 (publish rides the CLI; installs ride registry distribution from Phase 5 and enforcement from Phase 6) -**Requirements**: MKT-01, MKT-02, MKT-03, MKT-04 -**Success Criteria** (what must be TRUE): - - 1. A third-party developer publishes a DID-signed app manifest to public Nostr relays (NIP-78, kind 30078) using the tooling - 2. A node discovers the published app from multiple relays and the app store UI shows its trust tier (Verified / Community / Unverified) per ADR-006 scoring - 3. The node verifies the manifest signature before installation; a tampered or invalid marketplace manifest cannot be installed - 4. A user installs the third-party marketplace-published app on their node and it runs healthy under the standard lifecycle guarantees — the user-chosen success metric, demonstrated end-to-end - -**Plans**: TBD -**UI hint**: yes - -## Progress - -**Execution Order:** -Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 -(Phases 1 and 2 are independent and may be worked in parallel.) - -| Phase | Plans Complete | Status | Completed | -|-------|----------------|--------|-----------| -| 1. Federation & Mesh Hardening | 11/20 | In Progress| | -| 2. UI Performance | 11/12 | Complete | 2026-07-31 | -| 3. Multinode Verification Pass | 0/TBD | Not started | - | -| 4. Lifecycle Perfection & Quadlet Default | 0/TBD | Not started | - | -| 5. Registry-Distributed Manifests | 0/TBD | Not started | - | -| 6. Manifest Security Enforcement | 0/TBD | Not started | - | -| 7. Developer Tooling CLI | 0/TBD | Not started | - | -| 8. Decentralized Marketplace | 0/TBD | Not started | - | -| 9. BotFights Platform Upgrade | 7/7 | Executed — awaiting human demo verification | 2026-07-31 | -| 10. Key-Material Hardening | 0/5 | Planned — **priority override, see phase note** | - | -| 11. Wallet Experience & LND UI Parity | 0/TBD | Not started — gated on 10-05's watch-only verdict | - | - -### Phase 9: BotFights Platform Upgrade - -**Goal:** BotFights (app + registry) works great on every node: users sign in with the native nostr signer, a single self-contained AI prompt sets up their bot (replacing the confusing docs page), and every node's instance talks to a shared public match endpoint on VPS2 so all fighters are visible and battle across all nodes. -**Requirements**: BOT-01 native nostr signer login; BOT-02 unified AI bot-setup prompt (one copy-paste prompt, no doc-hopping); BOT-03 public shared match/fighter endpoint hosted on VPS2, node instances federate to it by default; BOT-04 registry/manifest + signed catalog updated and republished for the new version -**Depends on:** Nothing (independent app work; parallelizable with Phases 1–8) -**Plans:** 7 plans - -Plans: - -- [x] 09-01-PLAN.md — Arena reverse-proxy tracer: node instances become thin clients of one shared arena (BOT-03) -- [x] 09-02-PLAN.md — Finish native nostr signer login: JWT-only GET /api/auth/me, bare-pubkey path retired (BOT-01) -- [x] 09-03-PLAN.md — One self-contained AI bot-setup prompt served at /api/docs/prompt (BOT-02) -- [x] 09-04-PLAN.md — Canonical public arena on VPS2 + DNS/TLS via nginx-proxy-manager (BOT-03) -- [x] 09-05-PLAN.md — Build+push botfights:1.2.0, roll the arena, prove cross-instance visibility (BOT-03/BOT-04) -- [x] 09-06-PLAN.md — Manifest 1.2.0 with generated JWT secret + signed catalog republished (BOT-04) -- [x] 09-07-PLAN.md — archi-dev-box deploy + demo rehearsal: real signer login, cloud bot from the prompt (BOT-01/02/03/04) - -### Phase 10: Key-Material Hardening - -**Goal:** Every path that creates, restores, or persists node key material proves the caller is authorized and the material is per-node — closing the three exploitable findings from `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`. A node that is already onboarded must refuse to have its identity replaced; a node flashed from the shared rootfs must never share another node's host keys; and the wallet spending key must not exist in cleartext outside the encrypted envelope. -**Requirements**: KEY-01 (F-01, **Critical**) `seed.generate`/`seed.restore` are unauthenticated (`api/rpc/middleware.rs:25`) and `NodeIdentity::from_seed` (`identity.rs:79`) overwrites `node_key`/`nostr_secret`/FIPS key unconditionally — one unauthenticated POST with an attacker-chosen mnemonic hijacks a live node; gate on onboarding-incomplete (the unused `identity.rs:117` `key_exists` guard) + rate-limit; KEY-02 (F-03, **High**) first-boot per-device secret regeneration is fail-open and its completion marker is set even on failure (`image-recipe/_archived/build-auto-installer-iso.sh:1647,:1659,:1663`), over a fleet-shared cached rootfs that bakes SSH host keys + the TLS key — make it fail-closed and retried; KEY-03 (F-13, **High**) the BIP-84 account **private** key is imported into Bitcoin Core's wallet (`api/rpc/bitcoin.rs:203,:229-231`), duplicating the spending key outside the encrypted envelope — move to watch-only descriptors per `docs/security/PSBT-SIGNING-ARCHITECTURE.md`; KEY-04 on-node verification of C-3/C-4/C-6 from the audit's UNVERIFIED checklist (host-key uniqueness across two real nodes, rootfs tar contents on the build host, unauthenticated LAN reachability of the RPC endpoint); KEY-05 (F-10a, **Medium**, added 2026-08-02) **a defaulted RNG cannot be inherited anywhere in the crate**. The audit's F-10 recorded this as 2 call sites; it is **41 raw matches across 15 files** (`session.rs` 16 → 4 prod + 12 test, `pine_ha.rs` 6, `wallet/bdhke.rs` 4 → 2 prod — *Cashu proof secret + blinding factor, genuine key material*, `storage_crypto.rs` 1 — *AEAD nonce*, `mesh/x3dh.rs` 2 — *prekey identifiers, **not** key material, corrected 2026-08-02*, +10 more; full table in the audit's §F-10a. Per-site prod/test classification is KEY-05's Task 1, not an assumption). Nothing is broken today — `rand::random()`/`thread_rng()` are ChaCha12 seeded from `getrandom(2)` — but this is the exact T1 structural shape that produced the 2026-07-30 COLDCARD defect, now with key material in its blast radius. Five layers, all required: (a) **sealed allowlist trait** at key-generation seams (private supertrait, so no other module *or crate* can implement it; exactly one production impl, `OsRng`) — this also retires the `impl rand::CryptoRng for CountingRng` false promise at `seed.rs:656`; (b) **`clippy.toml` `disallowed-methods`** banning `rand::thread_rng`/`rand::random` crate-wide, so enforcement is a compile failure in CI rather than a review convention (no `clippy.toml` exists today; CI already runs clippy); (c) **`cargo-deny`** failing on duplicate `rand` majors — two coexist today, which is the mechanism by which a bump could silently rebind (absorbs R-05); (d) **degenerate-entropy runtime check** before key generation (rejects all-zero / counter-like draws — the one layer that would catch the Coldcard failure *on the device* rather than in review); (e) **persist the CSPRNG-readiness verdict** that `seed.rs:59` already computes and discards, so a node can answer after the fact "was the pool seeded when this key was born?" (absorbs R-09). Supersedes R-13 -**Depends on:** Nothing (independent security work; parallelizable with Phases 1–8). **Priority override: F-01 is Critical and live on every fleet node — this phase should be planned and executed ahead of its numeric position, which reflects append order in a shared roadmap, not sequencing.** -**Plans:** 6 plans - -> **EXECUTION GATE (user instruction, 2026-08-02):** do **not** begin executing this phase until -> (a) the concurrent agent working Phase 1 has finished, and (b) their changes are synced and -> accounted for. Rationale: Phase 10 edits `middleware.rs`, `identity.rs`, `seed_rpc.rs`, -> `bitcoin.rs` and — under KEY-05 — ~15 further files across the same crate that agent is -> actively committing to. Verify a clean tree and a fetched `gitea-ai/main` before starting. -> -> **KEY-05 is planned** as `10-06` (added 2026-08-02). The other 5 plans predate KEY-05 and -> are unchanged by it. `10-06` is wave 2 because it shares `seed.rs` with `10-05` and -> `api/rpc/auth.rs` with `10-01`; see its ``. - -Plans: - -**Wave 1** *(parallel — no shared files)* - -- [ ] 10-01-PLAN.md — Identity-mutating unauthenticated RPCs hard-refuse on a provisioned node, with the byte-identity regression suite (KEY-01) -- [ ] 10-03-PLAN.md — First-boot secret regeneration retries then fails closed, and the rootfs tar ships identity-free (KEY-02/KEY-04 C-4) -- [ ] 10-05-PLAN.md — Delete the Bitcoin Core xprv-import path; make LND's PSBT round trip first-class, tested and honestly documented (KEY-03) - -**Wave 2** *(each blocked on its wave-1 sibling)* - -- [ ] 10-02-PLAN.md — On-node C-6 exposure measurement, live refusal proof, and the fresh-node onboarding non-regression (KEY-01/KEY-04) — depends on 10-01 -- [ ] 10-04-PLAN.md — Fleet detection of image-baked host secrets, guarded one-time rotation, and C-3 two-node verification (KEY-02/KEY-04) — depends on 10-03 -- [ ] 10-06-PLAN.md — A defaulted RNG cannot be inherited anywhere in the crate: sealed allowlist, clippy ban, cargo-deny, degenerate-entropy check, persisted CSPRNG verdict (KEY-05) — depends on 10-01 and 10-05 - -### Phase 11: Wallet Experience & LND UI Parity - -**Goal:** The wallet is something a user chooses and understands, not something that just appears. A first-run wallet screen lets them pick a wallet type and route accordingly; seed handling reuses the SeedQR + seed-words patterns already shipped; and the day-to-day Lightning interface offers what umbrelOS's LND UI offers, so nothing is missing for someone arriving from Umbrel. -**Requirements**: WALLET-01 first-run wallet-type chooser (an intro/initial screen presenting the available wallet types with plain-language trade-offs, routing into the matching setup flow) — the available types depend on Phase 10's `10-05` watch-only verdict, so this requirement is **gated on that evidence**, not on assumption; WALLET-02 seed handling in the wallet flow reuses the existing SeedQR + seed-words components rather than reimplementing them (`neode-ui/src/utils/seedqr.ts`, `OnboardingSeedGenerate.vue`, `SeedRevealPanel.vue`, `WalletScanModal.vue`) — including the standing constraint that the LND aezeed is text-only by design and has no SeedQR; WALLET-03 evidence-based umbrelOS LND UI parity — produce a feature-by-feature comparison matrix from the actual Umbrel interface (researched, not assumed), classify each row as already-shipped / gap / deliberately-not-wanted, and close the gaps worth closing; WALLET-04 the resulting interface is house-style (Teleport-to-body modals, existing design system) and verified on the :8100 dev preview against archi-dev before any deploy; WALLET-05 **the PSBT air-gap round trip is a real, usable flow** — the standard two-scan dance (node displays the unsigned PSBT as an animated QR → offline signer scans and signs → signer displays the signed PSBT → node scans it back with the camera → finalize + broadcast). Three sub-gaps, all verified 2026-08-01: (a) **no UI exists** — `lnd.create-psbt`/`lnd.finalize-psbt` and their `rpc-client.ts:417` wrappers are called by nothing but unit tests; (b) **no animated-QR encoder** — `qrcode`/`qrloop` are dependencies and `useAnimatedQRDecoder.ts` + `WalletScanModal.vue` already handle the *inbound* scan, but nothing encodes a PSBT for display; (c) **format interop is wrong for real signers** — the animated format in use is `qrloop` (Ledger's), while Passport/SeedSigner speak **BC-UR** (`ur:crypto-psbt`) and Coldcard Q speaks **BBQr**; BC-UR is the priority given the existing Passport-Prime-compatible SeedQR work. **WALLET-05 is meaningless until 10-05's watch-only verdict lands** — `lnd.create-psbt` funds from LND's own wallet whose keys LND holds, so until LND is watch-only against the external signer the offline device would produce a signature the node does not need -**Depends on:** **Phase 10** — specifically `10-05`, which produces the evidence-backed verdict on whether LND can be provisioned watch-only against an external signer. WALLET-01's list of offerable wallet types is a direct consequence of that verdict; building the chooser first would mean guessing at what it can offer. `10-05` also deletes the dead Core wallet path, so this phase never has to represent it in the UI. -**Plans:** 0 plans - -**Already shipped — do not rebuild (verified 2026-08-01):** `LightningChannelsPanel.vue`, `SendBitcoinModal.vue`, `ReceiveBitcoinModal.vue`, `WalletScanModal.vue`, `WalletSettingsModal.vue`, `SeedRevealPanel.vue`, `LndSeedBackupPrompt.vue`, `utils/seedqr.ts`, and the channels All/Active/Pending/Closed tabs. The parity matrix (WALLET-03) must start from this inventory so the phase closes real gaps instead of re-implementing existing surfaces. - -Plans: - -- [ ] TBD (run /gsd-plan-phase 11 to break down) - -### Phase 12: Bitcoin Node Settings & Core/Knots Parity - -**Goal:** The Bitcoin node's configuration is something the operator chooses in the UI, not something baked into three shell scripts. Every option umbrelOS surfaces for its Bitcoin app is reachable, the options that exist **only** on Knots are surfaced separately from the ones Core shares, and the node's network mode is a first-class setting whose **default is Tor, not clearnet**. - -**Requirements**: BTCSET-01 **a single source of truth for bitcoind arguments** — today they are hardcoded and duplicated across `scripts/first-boot-containers.sh:666`, `scripts/container-specs.sh:193-202` and `apps/bitcoin-knots/manifest.yml:43`, which is the exact triplication that produced the lnd-ui bridge/host defect (`HTTP 000`, found 2026-08-02); a persisted settings model must replace it, with those three call sites rendering FROM it rather than restating it; BTCSET-02 **network mode is a setting, defaulting to Tor** — Tor / clearnet / both, wired to the archy-net SOCKS listener shipped in `f0494193` via `-onion=:9050` (onion-only) or `-proxy=` (everything), with the operator's 2026-08-02 choice of onion-only as the shipped default for the "both" mode; **inbound onion is out of scope and must be stated as such in the UI** — it needs Tor's ControlPort, deliberately disabled for security, so the node can reach .onion peers but stays unlisted; BTCSET-03 **Core options surfaced** (prune, dbcache, txindex, maxconnections, maxmempool, mempoolexpiry, persistmempool, blocksonly, peerbloomfilters, blockfilterindex, and the rest of the umbrelOS set, researched from `getumbrel/umbrel-bitcoin` rather than assumed); BTCSET-04 **Knots-only options surfaced separately and gated to Knots** (`datacarrier`, `datacarriersize`, `permitbaremultisig`, `rejectparasites`, `maxscriptsize`, the spam-filter family) — offering a Knots-only flag on Core would produce a node that refuses to start, so the gate is a correctness requirement, not a cosmetic one; BTCSET-05 house-style UI verified on the `:8100` dev preview against archi-dev before any deploy, mobile included. - -**The hazard this phase must not get wrong:** several of these options are **not freely reversible**. Turning `txindex` on forces a full reindex; turning `prune` on is destructive to block data and cannot be undone without a full resync; lowering `prune` below what is already pruned is meaningless. Any setting in that class must be labelled, confirmed, and — where it implies hours of resync on a node that is somebody's wallet backend — refused or gated rather than silently applied. Changing any option at all requires a bitcoind restart, which interrupts LND, electrs and the fedimint gateways that depend on it. - -**Depends on:** `f0494193` (the archy-net SOCKS listener) for BTCSET-02's Tor path to exist at all. Independent of Phases 1–11 otherwise. - -**Plans:** 0 plans - -Plans: - -- [ ] TBD (run /gsd-plan-phase 12 to break down) - -### Phase 13: AIUI — Conversational Node Control & Content Surfaces - -**Goal:** AIUI stops being a beautiful shell and becomes the node's conversational front door. Today it is embedded in `neode-ui/src/views/Chat.vue` as an iframe, its D-14 embed defaults are honoured, and its surfaces are designed — but the chat cannot *do* anything to the node, and the content views are not wired to real data. This phase makes it functional in three directions at once: (1) **ask the node in human language and have it act** — the capability Pine already demonstrates through voice becomes reachable from typed chat; (2) **talk to the system's settings** conversationally instead of hunting through screens; (3) **surface the node's content beautifully** — peer files, music, IndeeHub movies, owned/paid content — in the design AIUI already has but does not yet fill. - -**Requirements**: AIUI-01, AIUI-02, AIUI-03, AIUI-04, AIUI-05, AIUI-06 - -**Requirement detail**: -- **AIUI-01 — human-language node control.** A typed request in AIUI chat ("restart bitcoin", "how much space is left", "who's connected") reaches a real node action and returns a real result. The Pine stack (`core/archipelago/src/api/rpc/pine_status.rs`, `.../package/pine_ha.rs`, the wyoming/Home-Assistant voice pipeline) already proves the intent→action path exists for voice; this requirement is about exposing that capability over a **permissioned tool-calling bridge** the browser can reach — not about handing the chat raw RPC. Whether a text entry point exists today or must be built is the first thing the phase research must settle. -- **AIUI-02 — conversational settings.** The system settings surfaced across neode-ui become reachable by conversation, scoped to what the user has granted. -- **AIUI-03 — content surfaces made real.** AIUI's designed-but-empty content views render live node data: **peer files** (the `/content`, `/content/`, `/api/peer-content//` subsystem and the `content.*` RPCs), **music** (today only a MIME branch and a hardcoded `Music` folder — there is no library domain, so scope must be honest about what "music" means here), **IndeeHub movies**, and owned/paid content. Playback must respect the existing rules: audio belongs to the global bottom-bar player, never the lightbox; media streams via Range requests, never base64 blobs. -- **AIUI-04 — sandboxed by construction, permissioned by the user.** *(see hazard below — this is the gating requirement, not a nice-to-have)* -- **AIUI-05 — delivery and build.** AIUI is a `*-ui` app outside the signed catalog; it reaches nodes on the frontend rsync, which is how the `/assets` 404 happened (fixed in `fbec7006`). A functional AIUI needs a delivery path an operator can actually receive updates through, and the `VITE_BASE_PATH=/aiui/` build requirement pinned so a hand-built bundle cannot ship a black page. -- **AIUI-06 — verified on device**, in the real embedded iframe on archi-dev-box, mobile included — not only in the local `dev:mock` loop. - -**The hazard this phase must not get wrong — an LLM is now touching a node that holds keys.** AIUI runs in the browser and talks to a model. The node holds wallet keys, LND macaroons, Fedimint credentials, node identity and per-app secrets, and Phase 10 is currently hardening exactly that material. So: **secrets never reach the browser or the model context** — the existing pattern where credentials stay server-side and the client gets a scoped token (`app.filebrowser-token`) is the model to follow, not an exception to it. The chat gets an **explicit, user-granted capability scope** — it can reach only what the user has allowed, defaults closed, and the grant is visible and revocable. **Destructive and identity-touching operations are confirmed by the human**, never executed on model say-so alone; the Phase-10 hard-refuse gates and the loopback/auth boundaries must hold with AIUI on the other side of them, not be widened to accommodate it. Prompt injection is in the threat model: peer-supplied content (filenames, descriptions, chat) will enter the model's context, so tool authority must not be derivable from anything a peer controls. Note also the known leak to resolve rather than propagate: `filebrowser-client.ts` puts a JWT in the media URL query string. - -**Depends on:** Independent of Phases 1–12 for its UI and content work. Its security model must not contradict Phase 10 (Key-Material Hardening) — coordinate rather than widen. AIUI's own source lives in a **separate repository** (`git.tx1138.com/lfg2025/AIUI`, branch `development`, cloned at `~/Projects/AIUI`), so this phase spans two repos and needs push access to both. - -**Plans:** 15 plans in 8 waves - -Plans: - -**Wave 1** *(tracer + the two independent security/spike tracks)* - -- [ ] 13-01-PLAN.md — TRACER: typed AIUI chat reaches a real node tool and returns a real result (AIUI-01) -- [ ] 13-02-PLAN.md — Close the live unauthenticated model proxies: session-gated Rust forwarder, port-3142 sidecar deleted (AIUI-04) -- [ ] 13-03-PLAN.md — Routstr protocol spike + capability coverage matrix (AIUI-01) - -**Wave 2** - -- [ ] 13-04-PLAN.md — Music library: one-way entity-model decision + lofty legitimacy gate + tag extraction (AIUI-03) -- [ ] 13-05-PLAN.md — Curated tool registry, D-09 authority ceiling, D-16 default-closed grants, conversational settings (AIUI-01/02) -- [ ] 13-06-PLAN.md — Content surfaces: ContentItem → Film/Song/Podcast adapter, AIUI grids fed from Archy (AIUI-03) - -**Wave 3** - -- [ ] 13-07-PLAN.md — Music index + music.* RPCs + freshness (AIUI-03) -- [ ] 13-08-PLAN.md — D-11 confirm gate: node-authored, nonce-bound, rendered in trusted chrome (AIUI-01/04) - -**Wave 4** - -- [ ] 13-09-PLAN.md — AIUI delivery: enforced build, pinned commit, live-asset verify, iframe sandbox mechanism (AIUI-04/05) -- [ ] 13-10-PLAN.md — D-04 chain: Ollama tool-calling + D-08 node-side history (AIUI-01) -- [ ] 13-11-PLAN.md — SongGrid lit from the real library + the m4a/aac/opus/wma share-mime fix (AIUI-03) - -**Wave 5** - -- [ ] 13-12-PLAN.md — D-10 untrusted-content boundary + cloud-egress guardrails + rate limiting (AIUI-04) - -**Wave 6** - -- [ ] 13-13-PLAN.md — Routstr backend + D-05 hard budget ceiling (AIUI-01) - -**Wave 7** - -- [ ] 13-14-PLAN.md — Eval harness: ScriptedBackend suite, EV-01..EV-18, cross-backend parity (AIUI-01/04) - -**Wave 8** - -- [ ] 13-15-PLAN.md — On-device sign-off: archi-dev-box, embedded iframe, desktop + mobile (AIUI-06) - -**Track note (D-13):** the music-library track (13-04 → 13-07 → 13-11) is independent — no plan -on the control or content track depends on any music plan, **and neither does the phase-closing -gate**. 13-15 depends on 13-06, 13-09 and 13-14 only, so there is no path from it to 13-04, -13-07 or 13-11: if the music track slips or is deferred, 13-15 records that at its step 7b and -the control and content work still closes and ships. 13-11 is therefore a terminal plan of the -phase rather than a gate on it. diff --git a/.planning/STATE.md b/.planning/STATE.md deleted file mode 100644 index 60c41e5f..00000000 --- a/.planning/STATE.md +++ /dev/null @@ -1,225 +0,0 @@ ---- -gsd_state_version: 1.0 -milestone: v1.8.0 -milestone_name: milestone -current_phase: 09 -current_phase_name: BotFights Platform Upgrade -status: executing -stopped_at: v1.7.120-alpha SHIPPED; 1.7.121 queue open — see .planning/RELEASE-1.7.121-TASKS.md (12 items, RESUME HERE section at the end) -last_updated: "2026-08-03T15:15:58.798Z" -last_activity: 2026-07-31 -last_activity_desc: Phase 02 complete, transitioned to Phase 09 -progress: - total_phases: 13 - completed_phases: 2 - total_plans: 60 - completed_plans: 38 - percent: 15 ---- - -# Project State - -## Project Reference - -See: .planning/PROJECT.md (updated 2026-07-29) - -**Core value:** A third-party developer can publish an app via the signed/decentralized registry and a user can install it on their node — manifest-driven, rootless, secure, robust. -**Current focus:** Phase 02 — ui-performance - -## Current Position - -Phase: 09 — BotFights Platform Upgrade -Plan: Not started -Status: Ready to execute -Last activity: 2026-07-31 — Phase 02 complete, transitioned to Phase 09 - -Progress: [█████░░░░░] 54% - -## Performance Metrics - -**Velocity:** - -- Total plans completed: 11 -- Average duration: — -- Total execution time: — - -**By Phase:** - -| Phase | Plans | Total | Avg/Plan | -|-------|-------|-------|----------| -| 02 | 11 | - | - | -**Per-Plan Metrics:** - -| Plan | Duration | Tasks | Files | -|------|----------|-------|-------| -| Phase 02 P01 | 100min | 3 tasks | 5 files | -| Phase 02 P03 | 45min | 3 tasks | 5 files | -| Phase 02 P02 | 105min | 3 tasks | 11 files | -| Phase 02 P04 | 150min | 3 tasks | 12 files | -| Phase 02 P05 | 50min | 2 tasks | 8 files | -| Phase 02 P06 | 73min | 2 tasks | 7 files | -| Phase 02 P07 | 75min | 3 tasks | 5 files | -| Phase 02 P08 | ~190min | 3 tasks | 4 files | -| Phase 02 P09 | 130min | 3 tasks | 3 files | -| Phase 02 P10 | 55min | 2 tasks | 3 files | -| Phase 02 P11 | ~150min | 3 tasks | 8 files | -| Phase 01 P01 | n/a-continuation | 2 tasks | 1 files | - -## Accumulated Context - -### Roadmap Evolution - -- Phase 1 added (2026-07-29): Federation & Mesh Hardening — user-directed top priority (node removal/sync issues, mesh attachment parity incl. demo); prior phases shifted down -- Phase 2 added (2026-07-29): UI Performance — slow tab switches and secondary screens; prior phases shifted down -- FED-05 added to Phase 1 (2026-07-29): inter-node Lightning channel-opening UX (share node URI, pick trusted/federated nodes by hostname, request channels with public nodes); UI tested on :8100 dev preview against archi-dev before deploy -- FED-06 added to Phase 1 (2026-07-29): on-brand paid-tick animation — screensaver ring + EQ segments (reuse ScreensaverRing.vue compact) replacing the success burst in SendBitcoinModal.vue -- Phase 9 added (2026-07-30): BotFights Platform Upgrade — native nostr signer login, unified AI bot-setup prompt replacing docs page, shared public match endpoint on VPS2 (all nodes see all fighters), registry/manifest update. Independent of Phases 1–8. -- Phase 13 added (2026-08-03): AIUI — Conversational Node Control & Content Surfaces. User-directed: AIUI is embedded and styled but non-functional — chat cannot act on the node, content surfaces are unwired. Scope is (a) Pine's human-language intent→action capability reachable from typed chat, (b) conversational settings, (c) peer files / music / IndeeHub movies / node content rendered live, (d) **a user-granted capability sandbox** keeping keys, secrets and identity material away from the browser and the model — the user called this out explicitly as non-negotiable. Spans two repos: this one and `git.tx1138.com/lfg2025/AIUI` (branch `development`, clone at `~/Projects/AIUI`). Appended, not inserted — numeric position is append order, not priority. -- Phase 10 added (2026-08-01): Key-Material Hardening — KEY-01/F-01 (Critical: unauthenticated `seed.generate`/`seed.restore` overwrite a live node's identity keys), KEY-02/F-03 (fail-open first-boot secret regeneration over a fleet-shared rootfs), KEY-03/F-13 (BIP-84 private key imported into Bitcoin Core), KEY-04 (on-node verification of the audit's UNVERIFIED checklist). Sourced from `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` (quick task 260731-upz). Appended rather than inserted to avoid renumbering a roadmap with concurrent uncommitted edits — **numeric position is append order, not priority; F-01 is Critical and live on the fleet.** - -### Decisions - -Decisions are logged in PROJECT.md (10 locked ADRs in the `` block + milestone decisions table). Recent decisions affecting current work: - -- Milestone version = 1.8.0-alpha (decided 2026-07-08) -- Phase-3 Quadlet default-flip is gated on the second-node gate reporting clean (do fresh, never stage uncommitted) -- Workstream D (DHT distribution) deferred to v2 — design-only backlog -- Canonical manifest schema = `core/container/src/manifest.rs` (code wins over spec doc) -- [Phase ?]: Marketplace is the 02-02 tracer tab (worst-measured main tab, 2033ms revisit) matching the user's own 'often app store' complaint -- [Phase ?]: ContainerAppDetails.vue confirmed fully unreachable dead code (no importer, no route) — no serial-RPC-waterfall target exists in the measured D-09 surface set -- [Phase ?]: archi-dev-box UI password was unknown/undiscoverable from this environment — paused at a checkpoint:human-action rather than guessing or falling back to a mock baseline silently -- [Phase ?]: Purged the resource cache on logout via clearAll() + a generation guard, so no in-flight fetch from an ending session can repopulate memory or sessionStorage (T-02-02) -- [Phase ?]: AppDetails/MarketplaceAppDetails/OpenWrtGateway converted to per-item (or single-key) keyed useCachedResource; CloudFolder.vue's existing store-level cache left as-is (cloud.ts TTL gate is a follow-up, out of this plan's file scope) -- [Phase ?]: Wallet/send flow (SendBitcoinModal.vue) reported as an unplanned-item gap — named by findings as owned by 02-03 but not in files_modified; its cost is pure client-side remount, not a caching problem -- [Phase ?]: PERF-03 reverted to Pending in REQUIREMENTS.md after an initial mark-complete was premature — its own text requires real-node-hardware verification, which is 02-08's job (also declares PERF-03); 02-03 delivers the code-level portion only -- [Phase ?]: 02-02: DashboardRouterView final shape uses statically-named per-route KeepAlive wrapper components (dashboardViewWrappers.ts) with :include name-matching, restoring pre-restructure view-wrapper DOM/animations byte-for-byte after a checkpoint-caught regression -- [Phase ?]: 02-02: HARD RULE for rest of Phase 02 — perf work must be visually invisible; verify against the real dev preview before considering a checkpoint satisfied -- [Phase ?]: 02-02: app-catalog persist:true ttl 300000ms; bitcoin.prune-status persist:true ttl 30000ms — both explicit per T-02-01, no default relied on -- [Phase ?]: 02-02: PERF-02 reverted to Pending/In-Progress in REQUIREMENTS.md after an automated mark-complete run — PERF-02 also spans 02-04..02-07 (extending KeepAlive caching to every remaining main tab); this plan proves the architecture on the tracer tab only -- [Phase ?]: 02-04: KEEP_ALIVE_PATHS widened to every audited main tab (10 paths) derived from TAB_ORDER + /dashboard/discover; /dashboard/settings deliberately withheld — its child sections (SystemDangerZone reboot poll, several onMounted-only fetches) were never audited by this plan -- [Phase ?]: 02-04: onActivated is a documented no-op outside a KeepAlive boundary — every arm function now runs from both onMounted and onActivated (fresh-mount guards on Home/Web5/Mesh/Server avoid doubling first-load RPC cost); caught by CloudPeersRefresh.test.ts -- [Phase ?]: 02-04: useCachedResource.ts's onActivated no longer eagerly force-loads a never-fetched immediate:false resource, so tab-gated lazy data (Cloud.vue Paid Files/My Files) isn't force-loaded merely by its view entering the KeepAlive cache -- [Phase ?]: 02-04: AIUI blank-screen-and-loading symptom reported at Task 3 checkpoint diagnosed as pre-existing (local mock-backend dev mode sets VITE_AIUI_URL=http://localhost:5173 unconditionally with no AIUI repo checked out) — not a regression, left for 02-07 (Chat/AIUI) to address -- [Phase ?]: 02-05: mesh.refreshAll()/transport.fetchStatus() stay uncached at the store level (other callers need guaranteed-fresh reads); the useCachedResource wrapper around each lives in Mesh.vue instead, since Pinia's defineStore(id,setup) runs in a bare effectScope where onActivated() silently no-ops -- [Phase ?]: 02-05: FLAGGED - RESEARCH.md's premise that Mesh.vue owns a D3 force simulation is incorrect for this codebase (verified via grep); only NetworkMap.vue/Federation.vue has one. Task 2's D3 truths are vacuously satisfied; only the real Leaflet map lifecycle (MeshMap.vue, added to scope) was implemented -- [Phase ?]: 02-05: per-group TTL/persist table - mesh.refresh-all/federation-nodes/self-onion/self-did/contacts all persist:false (identity payload); transport-status persists (aggregate, non-identity); reachability groups get 10s TTL, identity groups 300s -- [Phase ?]: 02-06: RESEARCH A3 settled — none of Server's seven load-group loaders consumes another's result; concurrent fan-out is correct as-is -- [Phase ?]: 02-06: Five of Server's seven groups were already on useCachedResource from a pre-phase legacy commit (ea254f63) with only composable defaults (30s TTL, persist:true) — this plan's work was explicit TTL/persist/dedup, not initial conversion; only loadDiskStatus was a genuinely uncached plain fetch -- [Phase ?]: 02-06: Home's wallet composite does NOT share a cache key with Web5.vue's web5.lnd-info — sharing would either corrupt Web5's typed entry.data or fail to close the sessionStorage gap since Web5.vue's own hook (out of scope) defaults persist:true -- [Phase ?]: 02-06: homeStatus.refresh() wrapped by useCachedResource at Home.vue (the view), not inside the homeStatus Pinia store — defineStore(id,setup) runs in a bare effectScope where onActivated() silently no-ops, same finding as 02-05's Mesh.vue -- [Phase ?]: 02-07: AIUI source located mid-plan at git.tx1138.com/lfg2025/AIUI (base branch development, not stale main); AIUI-side D-14 commit 900c0b9 initially local-only (anonymous push 403) then pushed/merged upstream onto development by the orchestrator using a user-supplied write token -- [Phase ?]: 02-07: D-14a fixed via new ?chatExpanded param overriding chat.ts's chatCollapsed default (never persisted to localStorage); D-14b fixed via new ?mobileChat param re-asserting ChatPage.vue's mobileTab='chat' once on mount, guarding against module-singleton content-panel state surviving an internal AIUI remount -- [Phase ?]: 02-07: PERF-02 marked Complete in REQUIREMENTS.md — 02-02 through 02-07 extended KeepAlive/useCachedResource to every main tab, each dev-preview-verified against archi-dev-box per D-11 -- [Phase ?]: 02-08: KEEP_ALIVE_MAX left at 6, now backed by an on-device memory reading (4 cycles, 11 tabs, JS heap fluctuating 10-21MB, no monotonic growth) rather than the FA-D estimate -- [Phase ?]: 02-08: archy-x250-dev offline for the entire plan (checked 3x); archi-dev-box (D-11's named target) is the only dev-pair node this phase reached -- [Phase ?]: 02-08: the harness's remount-probe field is confounded for main tabs once real KeepAlive keeps multiple instances alive simultaneously; corrected via an independent, reproduced-twice verification rather than editing the frozen 02-01 harness — revealed Server.vue genuinely does not survive a round-trip (open gap, not hidden) -- [Phase ?]: 02-08: a user-reported Cloud first-visit navigation regression was treated as release-blocking, not known-open, per explicit direction — root-caused to content.browse-peer's unbounded, untimed-enough per-peer RPC fan-out starving Chromium's connection pool; fixed via a concurrency cap + shortened timeout, verified 5/5, user-approved on-node -- [Phase ?]: 02-08: 4 other user-reported UX issues (Paid Files window.open, PiP not closing lightbox, missing loader on Paid Files item-open, PiP not surviving tab changes) classified as pre-existing (predate phase 2 via git history) and captured into UIFIX-04/05/06, not fixed -- [Phase ?]: 02-09: /dashboard/server's (and Web5's) 'genuinely remounts' reading was a proven probe-measurement artifact (generic .view-container selector can't disambiguate the foreground tab from other still-connected cached tabs) — confirmed via document.elementFromPoint() hit-test contradicting the naive verdict across device runs; no source change needed, pinned with vm.$.uid-based regression tests instead -- [Phase ?]: 02-09: committed neode-ui/e2e/perf/keepalive-remount-probe.spec.ts as a re-runnable, instrumented probe covering every KEEP_ALIVE_PATHS tab, replacing the ad-hoc 02-08 probe so this class of false positive cannot recur -- [Phase ?]: [Phase 2, gap closure 02-10]: Wallet/send-flow's timing regression cleared as environmental noise (re-measure at/below baseline); Discover/Server/Web5/AppDetails/OpenWrtGateway confirmed as real, phase-2-caused client-side render/reactivation regressions via 3-run dispersion + git bisection, recorded as accepted deviations (not fixed — deploy blocked mid-session by a shared-tree hazard with concurrent security-follow-up and BotFights sessions) -- [Phase 2, gap closure 02-11]: Real cause of the six regressions was NOT compute-bound render cost (CPU profile: 86-99% idle/program, <10% JS self-time everywhere) — it was three background pollers (useFleetData.ts 60s, FipsNetworkCard.vue 15s, Web5Monitoring.vue 30s) armed in onMounted and never disarmed once their owning views joined KEEP_ALIVE_PATHS in 02-04, invisible to that audit because it grepped the top-level view files, not the child composables they delegate to. Gated to onActivated/onDeactivated, mirroring 02-04's own established pattern. Fixed: web5 275ms (was 566ms baseline/1329ms regressed), server 574ms (was 738/1239), fleet 790ms (was 330/2631) -- [Phase 2, gap closure 02-11]: Discover (1389ms, worst remaining) has a SECOND, distinct cause: card-stagger/showStagger entrance-animation classes are baked into the DOM at first mount and never programmatically removed, so every KeepAlive detach/reattach cycle restarts the CSS animation on reactivation — replaying the full entrance cascade on every revisit. Confirmed via a diagnostic (DOM card count doubling transiently on every revisit) and an extended animation-event log. NOT fixed — blast radius spans 5+ files outside 02-11's scope (Apps.vue, Marketplace.vue, Home.vue, several Web5 sub-cards), needs its own real-device verification budget; recommended as a dedicated follow-up -- [Phase 2, gap closure 02-11]: openwrt-gateway unmeasurable in the final re-measure (Chromium "Target crashed" cascading from an unrelated surface, cloud-folder, earlier in the same harness run) — recorded as not-measurable, not written in as data. Separately confirmed the prior baseline/after/remeasure numbers were measuring a real, substantive disconnected-state UI (OpenWrtGateway.vue's h1 is unconditional; a "No router configured" RPC error deterministically renders a real Connect-to-Router form, not a blank/error page) — the six-surface regression count is not retracted, but the numbers reflect one specific code branch (no OpenWrt device has ever been connected to archi-dev-box) -- [Phase ?]: 01-01: record_peer_transport and update_node routed through FEDERATION_STORE_LOCK via *_inner; tombstone-write-failure test added; full-suite verify blocked by a concurrent agent's uncommitted install.rs edit (unrelated file, not fixed per scope boundary) -- [Phase ?]: UIFIX-02: connected-nodes card height tracks row sibling via xl:flex-1 xl:basis-0 (zero-basis flex-grow) instead of flex-auto, with an xl:min-h-[40rem] floor for a short sibling (discovery disabled), tuned from an initial 20rem guess per Dorian's live feedback - -### Pending Todos - -- [blocker/ui] Keep FIPS/Tor pills on cloud files and show them on mobile (`.planning/todos/pending/2026-07-30-keep-fips-tor-pills-on-cloud-files-and-show-them-on-mobile.md`) -- [blocker/security] Fedimint gateway must not install with a pre-set password — tracked as FED-07 / Phase 1 gap plan (`.planning/todos/pending/2026-07-30-fedimint-gateway-must-not-install-with-preset-password.md`) -- [blocker/ui] Connected-nodes list must scroll at row-matched height, not grow to fit (`.planning/todos/pending/2026-07-30-connected-nodes-list-must-scroll-at-row-matched-height.md`) -- [blocker/ui] Onboarding tickbox hidden below fold on short screens — make it beautifully obvious (`.planning/todos/pending/2026-07-30-onboarding-tickbox-hidden-below-fold-on-short-screens.md`) -- [major/ui] Paid Files pictures open in browser tab, not the app lightbox — UIFIX-04 (`.planning/todos/pending/2026-07-30-peer-files-pictures-open-in-tab-not-lightbox.md`) -- [major/ui] PiP should close the lightbox with a fluid animation — UIFIX-05 (`.planning/todos/pending/2026-07-30-pip-should-close-lightbox-with-fluid-animation.md`) -- [major/ui] Missing loader states on slow opens — UIFIX-06 (`.planning/todos/pending/2026-07-30-missing-loader-states-on-slow-opens.md`) - -### Blockers/Concerns - -- [Phase 1] Federation tombstone fix touches trust code — fix carefully, re-verify with `tests/multinode/smoke.sh`, don't patch blind -- [Phase 3] Multinode gate on archy-x250-beta was launched 2026-07-01 (log on-node); verify outcome before re-running -- [Phase 5] Fleet registry flip awaits explicit user authorization + timing call -- [Phase 6] Strengthened ADR-009 validation may reject existing catalog apps — audit manifests before enforcement lands -- [Global] Live OTA fleet: deploy to the dev pair before any OTA; gate re-runs required after orchestrator changes; some verification is user/hardware-gated (radios, on-device tests) -- cloud.ts's navigate() needs a TTL gate to fully satisfy 'no new RPC within TTL' for CloudFolder.vue — currently always re-fetches on revisit (just doesn't block paint) -- [Phase 2, RESOLVED by 02-09] ~~Server.vue does not survive a tab round-trip despite KEEP_ALIVE_PATHS registration~~ — retracted: proven a probe-measurement artifact (shared generic `.view-container` selector couldn't disambiguate the foreground tab from other cached tabs), not a real defect. Server.vue's (and Web5.vue's) instance genuinely survives; pinned with `vm.$.uid`-based regression tests immune to the same ambiguity. Checkpoint approved on real hardware. -- [Phase 2, RESOLVED by 02-11] ~~Timing regressions on Discover/Web5/Fleet/AppDetails/OpenWrtGateway~~ — root cause found (three leaked background pollers, not compute-bound render cost) and fixed for web5/server/fleet, each proven with a real before/after number on archi-dev-box. AppDetails restored to at/near its own pre-phase-2 baseline (pre-existing per-mount cost, not a new defect). OpenWrtGateway not measurable this pass (browser crash); prior numbers stand with a data-integrity note (measuring a real disconnected-device UI, not an empty page). -- [Phase 2, follow-up needed] Discover (1389ms, worst remaining named surface) has a second, evidenced, phase-2-caused defect: KeepAlive'd entrance-stagger animations (`card-stagger`/`showStagger`) never get their class removed from the DOM after first play, so every reactivation replays the full CSS animation cascade. Fix requires touching 5+ files outside 02-11's scope (Apps.vue, Marketplace.vue, Home.vue, Web5Wallet.vue/Web5Identities.vue/Web5NodeVisibility.vue/Web5NostrRelays.vue) with its own real-device visual-regression verification budget (the same class of risk 02-02's original KeepAlive rollout hit on its first checkpoint attempt) — needs a dedicated follow-up plan, not squeezed into a gap-closure pass. - -### Quick Tasks Completed - -| # | Description | Date | Commit | Directory | -|---|-------------|------|--------|-----------| -| 260729-fw7 | improve mesh message hop graphic/animation: balanced desktop sizing, vertical mobile layout, archipelago branding | 2026-07-29 | ac09fc5d | [260729-fw7-improve-mesh-message-hop-graphic-animati](./quick/260729-fw7-improve-mesh-message-hop-graphic-animati/) | -| 260729-gjd | demo: indee.tx1138.com in app iframe (:2101 whole-origin proxy), auto nostr signer sign-in, IndeeHub pre-installed on fresh session | 2026-07-29 | d00ca624 | [260729-gjd-demo-make-indee-tx1138-com-work-in-the-a](./quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/) | -| 260729-hj1 | peer-files media batch: Wavlake paid tracks + purchases + dedupe + real photos (demo); lightbox/player open routing + free-image lightbox fix (both builds) | 2026-07-29 | f52c5407 | [260729-hj1-peer-files-media-batch-wavlake-paid-trac](./quick/260729-hj1-peer-files-media-batch-wavlake-paid-trac/) | -| 260729-je5 | connected-nodes list fills card height (constant footer gap); companion app skips demo intro | 2026-07-29 | d54517cf | [260729-je5-ui-fixes-connected-nodes-scrollable-list](./quick/260729-je5-ui-fixes-connected-nodes-scrollable-list/) | - -## Deferred Items - -| Category | Item | Status | Deferred At | -|----------|------|--------|-------------| -| Distribution | DIST-01 DHT/iroh backbone (workstream D) | v2 | 2026-07-29 | -| Fleet | FLEET-01 Bitcoin multi-version fleet OTA (user-gated) | v2 | 2026-07-29 | -| Fleet | FLEET-02 per-app deep health assertions (~34 apps) | v2 | 2026-07-29 | -| Fleet | FLEET-03 LUKS2 data-partition encryption | v2 | 2026-07-29 | - -## Release SHIPPED — v1.7.120-alpha (2026-08-03) - -**LIVE.** signature PRESENT (did:key:z6Mkkid…q7ur), both assets HTTP 200 at exactly their -manifest byte counts, tag pushed. Two release-process traps hit and documented in memory: -create-release.sh commits the manifest BEFORE signing (fleet refuses unsigned), and -gitea-vps2 is the SAME server as gitea-ai (vps2 token is dead). - -### Staging record (kept for the evidence trail) - -Built from `4d67f56b` (release profile, 15m15s, exit 0), deployed to archi-dev-box, -`.bak` rollback at /opt/archipelago/rollback/archipelago.bak. - -Verified on the node: both security gates 401 unauthenticated from a non-loopback -address; CORS origin-scoped; AIUI assets 200 AFTER the frontend rsync (the deploy that -would have wiped a copied-file fix); mesh.lightning-peers/send-lightning-info answer -correctly; system.stats host_secrets = per-node; served bundle sha256-matches the build -on all three chunks; 31 containers up, none down, no restart loop. - -NOT verified, deliberately: the new torrc SocksPort/SocksPolicy block. regenerate_torrc -only fires on a Tor services change, so the live torrc still reads only `SocksPort 9050`. -Gateway detection was proven in isolation (10.89.0.1 10.89.0.0/24; missing network exits -non-zero -> stays loopback-only). The change is INERT this release since bitcoind has no --onion flag yet (Phase 12), so forcing a torrc regeneration would risk bouncing every -onion service for zero benefit. - -Frontend is a proven no-op this cycle — built chunks are byte-identical to those already -served — so a fleet node only changes binary + the two app-UI images + nginx. - -Remaining to ship: operator go/no-go, then `scripts/create-release.sh 1.7.120-alpha` -(stops at the signing prompt — reads the master mnemonic interactively, operator-only), -then publish-release-assets.sh to gitea-vps2, then push tags. CHANGELOG.md already -carries curated v1.7.120-alpha notes (create-release.sh hard-fails without them). -The 5x lifecycle gate was NOT run. - -## Session Continuity - -Last session: 2026-08-03T12:57:50.980Z -Stopped at: Phase 13 context gathered -Resume file: .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-CONTEXT.md - -Open on this thread (all recorded as broken windows, none blocking): - -- Window 15 CLOSED 2026-08-02 20:02 — f6b5245b's reconcile path proven on archi-dev-box by - a controlled test: stale conf installed + container restarted (probe 200, genuinely - re-exposed), daemon started, reconcile repaired it unaided at 20:02:19 with the expected - warn line, probe 401, conf byte-identical to the known-good. Both halves now proven on - hardware. - -- Windows 11/12: host-secret rotation on three fleet nodes sharing SSH host keys — - detect-only so far; rotation is USER-GATED and deliberately not actioned. - -- Credential rotation DECIDED AGAINST 2026-08-02 (operator): no LND macaroon rotation, no - Bitcoin RPC password rotation — no evidence of exploitation and the vulnerability is - being closed rather than lived with. rotate-lnd-macaroon.sh stays as a tool, exercised in - detect mode only, never run against a node. Do not re-litigate; see - docs/security/BITCOIN-RPC-PROXY-EXPOSURE.md. - -- Dev-pair verification is archi-dev-box ONLY, by operator instruction 2026-08-02. Do not - raise archy-x250-dev as a blocker again. diff --git a/.planning/WINDOWS.md b/.planning/WINDOWS.md deleted file mode 100644 index 553df6af..00000000 --- a/.planning/WINDOWS.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -schema_version: 1 -open_count: 11 -waived_count: 0 -fixed_count: 4 -total_count: 15 -last_updated: 2026-08-03T00:06:03.112Z ---- - -# Broken Windows Ledger - -> Cross-phase defect register. `/gsd-ship` blocks while `open_count > 0`. -> Waive with `gsd-tools windows waive ""` (reason required). -> Mark fixed with `gsd-tools windows fixed `. - -| id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at | -|----|-------|------|------|------|-------------|--------|--------|-------------|-------------| -| 1 | 02 | deviation | neode-ui/src/stores/cloud.ts | | CloudFolder.vue's file listing cache lacks a TTL gate in cloudStore.navigate() — always re-issues the RPC on revisit (paints from cache instantly first, but still refetches unconditionally). Needs a TTL check added to navigate() to fully satisfy 'no new RPC within TTL'. | open | | 2026-07-30T12:25:22.301Z | | -| 2 | 02 | deviation | neode-ui/src/views/Home.vue | | Wallet/send flow (SendBitcoinModal.vue via Home.vue) named by 02-FINDINGS.md as owned by 02-03 (worst-ranked revisit, 2607ms) but not in 02-03-PLAN.md's files_modified — reported as an unplanned-item gap, not converted. Cause is pure client-side remount cost (0 RPC), not a caching problem. | open | | 2026-07-30T12:25:22.450Z | | -| 3 | 02 | deviation | neode-ui/src/views/PeerFiles.vue | | 02-03-PLAN.md assumed PeerFiles.vue already used useCachedResource; it actually uses the raw resources store directly (correctly per-item-keyed) with no TTL gate and the same loading/refreshing conflation bug fixed in OpenWrtGateway.vue this plan. Left untouched (out of files_modified scope) — candidate for the same fix in a future plan. | open | | 2026-07-30T12:25:22.605Z | | -| 4 | 02 | deviation | neode-ui/src/views/Chat.vue | | AIUI-side D-14 commit (900c0b9, branch feat/d14-embed-defaults in local clone /home/archipelago/Projects/AIUI, based on development) is NOT pushed upstream to git.tx1138.com/lfg2025/AIUI — anonymous push returned 403 Forbidden. neode-ui's two new query params (chatExpanded, mobileChat) are inert no-ops against any currently-deployed AIUI build until a maintainer with push rights merges and it is rebuilt/redeployed. 02-08 (deploy) or the user must resolve push access. | fixed | | 2026-07-30T22:37:25.565Z | 2026-07-30T22:37:44.642Z | -| 5 | 09 | unrun-verify | botfight/e2e/signup-bot.spec.ts | | pnpm test:e2e -- e2e/signup-bot.spec.ts not run: local backend dev port 9100 is occupied by the live archi-dev-box botfights container (podman, 42h uptime) needed for tomorrow's demo — could not free it to run a local dev server. Task-level automated verify (vue-tsc + grep sweep) passed; vitest server suite passed with only pre-existing unrelated flaky failures. | open | | 2026-07-31T02:35:00.391Z | | -| 6 | 02 | deviation | neode-ui/src/views/Discover.vue | | Discover revisit-ms regression (1083->1257->1453ms across 3 runs), confirmed phase-2-caused split-signal client-side render cost, not fixed (deploy blocked this session) | open | | 2026-07-31T10:56:26.089Z | | -| 7 | 02 | deviation | neode-ui/src/views/Server.vue | | Server revisit-ms regression (738->849->1239ms across 3 runs) despite confirmed instance survival and improved RPC count; confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session) | open | | 2026-07-31T10:56:26.305Z | | -| 8 | 02 | deviation | neode-ui/src/views/web5/Web5.vue | | Web5 revisit-ms regression (566->709->1329ms, zero overlap across 3 runs) despite confirmed instance survival; confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session) | open | | 2026-07-31T10:56:26.570Z | | -| 9 | 02 | deviation | neode-ui/src/views/AppDetails.vue | | AppDetails revisit-ms regression (1204->1510->2668ms across 3 runs); confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session) | open | | 2026-07-31T10:56:26.751Z | | -| 10 | 02 | deviation | neode-ui/src/views/server/OpenWrtGateway.vue | | OpenWrtGateway revisit-ms regression (663.5->1148->1460ms across 3 runs); confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session) | open | | 2026-07-31T10:56:26.933Z | | -| 11 | 10 | unrun-verify | docs/security/KEY-02-FLEET-ROTATION.md | | C-3 FAILED: archipelago-1, archy-x250-beta and archipelago share all three SSH host keys; the first two also share their TLS private key. Not rotated — needs an operator-driven --apply --yes per node. | open | | 2026-08-02T19:07:39.861Z | | -| 12 | 10 | unrun-verify | scripts/security/host-secrets-audit.sh | | Rotation never exercised on real hardware: that 'systemctl reload ssh' keeps the operator's own forked session alive is proven only by design, not by observation. Needs --apply --yes on one disposable node from a session the operator is willing to lose. | open | | 2026-08-02T19:07:40.217Z | | -| 13 | 10 | unrun-verify | core/archipelago/src/api/rpc/system/handlers.rs | | system.stats host_secrets never observed on a real node — proven against the file contract in unit tests only. Needs a build carrying 10-04 deployed to the dev pair, then a system.stats call. | fixed | | 2026-08-02T19:07:40.522Z | 2026-08-02T23:00:30.894Z | -| 14 | 10 | unrun-verify | core/archipelago/src/container/prod_orchestrator.rs | | LIVE EXPOSURE on archi-dev-box: archy-bitcoin-ui (systemd/Quadlet-owned, user-uninstalled marker set) still serves unauthenticated POST /bitcoin-rpc/ on 0.0.0.0:8334 with Access-Control-Allow-Origin *, reaching Bitcoin Core RPC through a credential-injecting proxy. Verified live 2026-08-02 (returned a real block height with no cookies). Code fix committed f6b5245b but NOT deployed: closing it needs the new binary on the node plus an archy-bitcoin-ui restart. archy-electrs-ui is in the same uninstalled-but-running state (static UI only, no credential proxy). Operator-gated; no node touched. | fixed | | 2026-08-02T22:44:15.215Z | 2026-08-02T23:16:04.071Z | -| 15 | 10 | unrun-verify | core/archipelago/src/container/prod_orchestrator.rs | | The f6b5245b reconcile fix is DEPLOYED on archi-dev-box (binary installed 19:06, running) but NEVER EXERCISED on hardware: the state it repairs (uninstall marker + Quadlet-running + stale config) stopped existing here at 18:36, when a separate rebuild of bitcoin-ui rendered the fixed conf and restarted the container. So :8334 returning 401 proves a05956c4's template, NOT the reconcile path that is supposed to deliver it. archy-electrs-ui still carries the marker+running shape and could exercise it, but has no rendered config to rewrite. Needs a node that still has a stale bitcoin-ui conf, or a deliberately re-staled one. | fixed | | 2026-08-02T23:16:04.510Z | 2026-08-03T00:06:03.112Z | - -````json -[ - { - "id": 1, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/stores/cloud.ts", - "line": null, - "description": "CloudFolder.vue's file listing cache lacks a TTL gate in cloudStore.navigate() — always re-issues the RPC on revisit (paints from cache instantly first, but still refetches unconditionally). Needs a TTL check added to navigate() to fully satisfy 'no new RPC within TTL'.", - "status": "open", - "reason": "", - "recorded_at": "2026-07-30T12:25:22.301Z", - "resolved_at": null - }, - { - "id": 2, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/Home.vue", - "line": null, - "description": "Wallet/send flow (SendBitcoinModal.vue via Home.vue) named by 02-FINDINGS.md as owned by 02-03 (worst-ranked revisit, 2607ms) but not in 02-03-PLAN.md's files_modified — reported as an unplanned-item gap, not converted. Cause is pure client-side remount cost (0 RPC), not a caching problem.", - "status": "open", - "reason": "", - "recorded_at": "2026-07-30T12:25:22.450Z", - "resolved_at": null - }, - { - "id": 3, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/PeerFiles.vue", - "line": null, - "description": "02-03-PLAN.md assumed PeerFiles.vue already used useCachedResource; it actually uses the raw resources store directly (correctly per-item-keyed) with no TTL gate and the same loading/refreshing conflation bug fixed in OpenWrtGateway.vue this plan. Left untouched (out of files_modified scope) — candidate for the same fix in a future plan.", - "status": "open", - "reason": "", - "recorded_at": "2026-07-30T12:25:22.605Z", - "resolved_at": null - }, - { - "id": 4, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/Chat.vue", - "line": null, - "description": "AIUI-side D-14 commit (900c0b9, branch feat/d14-embed-defaults in local clone /home/archipelago/Projects/AIUI, based on development) is NOT pushed upstream to git.tx1138.com/lfg2025/AIUI — anonymous push returned 403 Forbidden. neode-ui's two new query params (chatExpanded, mobileChat) are inert no-ops against any currently-deployed AIUI build until a maintainer with push rights merges and it is rebuilt/redeployed. 02-08 (deploy) or the user must resolve push access.", - "status": "fixed", - "reason": "", - "recorded_at": "2026-07-30T22:37:25.565Z", - "resolved_at": "2026-07-30T22:37:44.642Z" - }, - { - "id": 5, - "kind": "unrun-verify", - "phase": "09", - "file": "botfight/e2e/signup-bot.spec.ts", - "line": null, - "description": "pnpm test:e2e -- e2e/signup-bot.spec.ts not run: local backend dev port 9100 is occupied by the live archi-dev-box botfights container (podman, 42h uptime) needed for tomorrow's demo — could not free it to run a local dev server. Task-level automated verify (vue-tsc + grep sweep) passed; vitest server suite passed with only pre-existing unrelated flaky failures.", - "status": "open", - "reason": "", - "recorded_at": "2026-07-31T02:35:00.391Z", - "resolved_at": null - }, - { - "id": 6, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/Discover.vue", - "line": null, - "description": "Discover revisit-ms regression (1083->1257->1453ms across 3 runs), confirmed phase-2-caused split-signal client-side render cost, not fixed (deploy blocked this session)", - "status": "open", - "reason": "", - "recorded_at": "2026-07-31T10:56:26.089Z", - "resolved_at": null - }, - { - "id": 7, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/Server.vue", - "line": null, - "description": "Server revisit-ms regression (738->849->1239ms across 3 runs) despite confirmed instance survival and improved RPC count; confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session)", - "status": "open", - "reason": "", - "recorded_at": "2026-07-31T10:56:26.305Z", - "resolved_at": null - }, - { - "id": 8, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/web5/Web5.vue", - "line": null, - "description": "Web5 revisit-ms regression (566->709->1329ms, zero overlap across 3 runs) despite confirmed instance survival; confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session)", - "status": "open", - "reason": "", - "recorded_at": "2026-07-31T10:56:26.570Z", - "resolved_at": null - }, - { - "id": 9, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/AppDetails.vue", - "line": null, - "description": "AppDetails revisit-ms regression (1204->1510->2668ms across 3 runs); confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session)", - "status": "open", - "reason": "", - "recorded_at": "2026-07-31T10:56:26.751Z", - "resolved_at": null - }, - { - "id": 10, - "kind": "deviation", - "phase": "02", - "file": "neode-ui/src/views/server/OpenWrtGateway.vue", - "line": null, - "description": "OpenWrtGateway revisit-ms regression (663.5->1148->1460ms across 3 runs); confirmed phase-2-caused split-signal cost, not fixed (deploy blocked this session)", - "status": "open", - "reason": "", - "recorded_at": "2026-07-31T10:56:26.933Z", - "resolved_at": null - }, - { - "id": 11, - "kind": "unrun-verify", - "phase": "10", - "file": "docs/security/KEY-02-FLEET-ROTATION.md", - "line": null, - "description": "C-3 FAILED: archipelago-1, archy-x250-beta and archipelago share all three SSH host keys; the first two also share their TLS private key. Not rotated — needs an operator-driven --apply --yes per node.", - "status": "open", - "reason": "", - "recorded_at": "2026-08-02T19:07:39.861Z", - "resolved_at": null - }, - { - "id": 12, - "kind": "unrun-verify", - "phase": "10", - "file": "scripts/security/host-secrets-audit.sh", - "line": null, - "description": "Rotation never exercised on real hardware: that 'systemctl reload ssh' keeps the operator's own forked session alive is proven only by design, not by observation. Needs --apply --yes on one disposable node from a session the operator is willing to lose.", - "status": "open", - "reason": "", - "recorded_at": "2026-08-02T19:07:40.217Z", - "resolved_at": null - }, - { - "id": 13, - "kind": "unrun-verify", - "phase": "10", - "file": "core/archipelago/src/api/rpc/system/handlers.rs", - "line": null, - "description": "system.stats host_secrets never observed on a real node — proven against the file contract in unit tests only. Needs a build carrying 10-04 deployed to the dev pair, then a system.stats call.", - "status": "fixed", - "reason": "", - "recorded_at": "2026-08-02T19:07:40.522Z", - "resolved_at": "2026-08-02T23:00:30.894Z" - }, - { - "id": 14, - "kind": "unrun-verify", - "phase": "10", - "file": "core/archipelago/src/container/prod_orchestrator.rs", - "line": null, - "description": "LIVE EXPOSURE on archi-dev-box: archy-bitcoin-ui (systemd/Quadlet-owned, user-uninstalled marker set) still serves unauthenticated POST /bitcoin-rpc/ on 0.0.0.0:8334 with Access-Control-Allow-Origin *, reaching Bitcoin Core RPC through a credential-injecting proxy. Verified live 2026-08-02 (returned a real block height with no cookies). Code fix committed f6b5245b but NOT deployed: closing it needs the new binary on the node plus an archy-bitcoin-ui restart. archy-electrs-ui is in the same uninstalled-but-running state (static UI only, no credential proxy). Operator-gated; no node touched.", - "status": "fixed", - "reason": "", - "recorded_at": "2026-08-02T22:44:15.215Z", - "resolved_at": "2026-08-02T23:16:04.071Z" - }, - { - "id": 15, - "kind": "unrun-verify", - "phase": "10", - "file": "core/archipelago/src/container/prod_orchestrator.rs", - "line": null, - "description": "The f6b5245b reconcile fix is DEPLOYED on archi-dev-box (binary installed 19:06, running) but NEVER EXERCISED on hardware: the state it repairs (uninstall marker + Quadlet-running + stale config) stopped existing here at 18:36, when a separate rebuild of bitcoin-ui rendered the fixed conf and restarted the container. So :8334 returning 401 proves a05956c4's template, NOT the reconcile path that is supposed to deliver it. archy-electrs-ui still carries the marker+running shape and could exercise it, but has no rendered config to rewrite. Needs a node that still has a stale bitcoin-ui conf, or a deliberately re-staled one.", - "status": "fixed", - "reason": "", - "recorded_at": "2026-08-02T23:16:04.510Z", - "resolved_at": "2026-08-03T00:06:03.112Z" - } -] -```` diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md deleted file mode 100644 index 904c84b6..00000000 --- a/.planning/codebase/ARCHITECTURE.md +++ /dev/null @@ -1,333 +0,0 @@ - -# Architecture - -**Analysis Date:** 2026-07-29 - -## System Overview - -```text -┌────────────────────────────────────────────────────────────────┐ -│ Frontend Layer (Vue 3) │ -│ `neode-ui/src` (TypeScript + SPA) │ -│ Routes → Views → Components → Composables → RPC Client │ -└────────────────┬─────────────────────────────────────────────┘ - │ WebSocket + HTTP(S) - │ JSON-RPC 2.0 protocol - ▼ -┌────────────────────────────────────────────────────────────────┐ -│ HTTP Server Layer (Hyper) │ -│ `core/archipelago/src/server.rs` │ -│ TCP Listener → Hyper → Router → ApiHandler/RpcHandler │ -└────────────────┬─────────────────────────────────────────────┘ - │ - ┌──────────┴──────────┬──────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌─────────┐ ┌──────────┐ ┌────────────┐ - │ WebSocket │ RPC │ │ Content │ - │ Handler │ Handler │ │ Proxy │ - │ (state sync) │ (methods)│ │ (app URIs) │ - └─────────┘ └──────────┘ └────────────┘ - │ │ - └──────────┬───────┘ - │ - ▼ - ┌─────────────────────────────────────┐ - │ Service Layer (Async Tasks) │ - │ `core/archipelago/src/api/rpc/*` │ - │ │ - │ • auth, identity, secrets │ - │ • container orchestration │ - │ • bitcoin, lightning, wallet │ - │ • mesh, federation, FIPS │ - │ • content, backup, settings │ - └─────────────┬───────────────────────┘ - │ - ┌───────────┼───────────┬──────────────┐ - │ │ │ │ - ▼ ▼ ▼ ▼ - ┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐ - │Container│ │ State │ │BlobStore - │Orch. │ │Manager │ │ │ │Identity │ - │(Podman) │ │(Broadcast - │ │ │ channels)│ │ ContentClient Manager │ - └─────────┘ └──────────┘ └────────┘ └──────────┘ - │ │ │ │ - └───────────┼───────────┼─────────┘ - │ - ▼ - ┌─────────────────────────────────────┐ - │ Persistent Storage Layer │ - │ │ - │ • Data directory files (YAML/JSON) │ - │ • SQLite (session store) │ - │ • Blob store (content-addressed) │ - │ • Podman container state │ - │ • Secret vaults (encrypted) │ - └─────────────────────────────────────┘ -``` - -## Component Responsibilities - -| Component | Responsibility | File | -|-----------|----------------|------| -| **Server** | HTTP listener, connection multiplexing, TLS/encryption | `core/archipelago/src/server.rs` | -| **ApiHandler** | HTTP request routing, authentication, response formatting | `core/archipelago/src/api/handler/mod.rs` | -| **RpcHandler** | JSON-RPC 2.0 dispatch, method registration, rate limiting | `core/archipelago/src/api/rpc/mod.rs` | -| **ContainerOrchestrator** | Podman lifecycle, manifest reconciliation, adoption | `core/archipelago/src/container/prod_orchestrator.rs` | -| **StateManager** | Central state broadcast channel, revision tracking | `core/archipelago/src/state.rs` | -| **AuthManager** | User credentials, session validation, password hashing | `core/archipelago/src/auth.rs` | -| **Identity Manager** | Node Ed25519 keys, seed derivation, Tor address | `core/archipelago/src/identity_manager.rs` | -| **BootReconciler** | Periodic manifest sync loop, adoption, remediation | `core/archipelago/src/container/boot_reconciler.rs` | -| **Frontend Router** | Vue Router, page navigation, deep linking | `neode-ui/src/router/index.ts` | -| **Frontend Stores** | Pinia state (apps, settings, user, mesh) | `neode-ui/src/stores/` | -| **Frontend Components** | UI elements, modals, cards, layout primitives | `neode-ui/src/components/` | - -## Pattern Overview - -**Overall:** Multi-tier async architecture with centralized request dispatch and broadcast state synchronization. - -**Key Characteristics:** -- **Async-first (Tokio)** - All I/O operations are non-blocking; task spawning for background work -- **RPC-driven API** - Frontend communicates via JSON-RPC 2.0 (not REST); single `/api/v0` WebSocket + HTTP endpoint -- **State as broadcast** - Global state changes flow through Tokio broadcast channels to all connected WebSocket clients -- **Manifest-driven containers** - App lifecycle controlled by declarative YAML manifests (Archipelago-specific extensions) -- **Plugin architecture** - Apps are isolated Podman containers with declarative interfaces (web UI, ports, secrets) - -## Layers - -**HTTP/Transport Layer:** -- Purpose: Accept inbound connections, handle TLS termination, demultiplex HTTP/WebSocket -- Location: `core/archipelago/src/server.rs` -- Contains: Hyper listener, TCP accept loop, connection state tracking -- Depends on: Tokio, Hyper, TLS/mTLS libraries (rustls/openssl) -- Used by: All external clients (web UI, companion app, API consumers) - -**Request Routing & Auth Layer:** -- Purpose: Dispatch HTTP requests to handlers, validate sessions, enforce CSRF, rate-limit login -- Location: `core/archipelago/src/api/` (handler + rpc submodules) -- Contains: Route matching, middleware chain, cookie extraction, error formatting -- Depends on: Server, StateManager, SessionStore -- Used by: All request paths; gates API access - -**RPC Dispatch Layer:** -- Purpose: Deserialize JSON-RPC 2.0 requests, call appropriate service method, serialize responses -- Location: `core/archipelago/src/api/rpc/mod.rs` + subdirectories (auth.rs, container.rs, bitcoin.rs, etc.) -- Contains: Method table, parameter validation, response formatting, rate limit checks -- Depends on: All service modules -- Used by: Frontend (WebSocket + HTTP POST to /api/v0), internal tools - -**Service Layer:** -- Purpose: Implement business logic — container lifecycle, identity, auth, content sync, mesh discovery -- Location: `core/archipelago/src/api/rpc/*` (one RPC module per domain), plus `core/archipelago/src/` (background tasks) -- Contains: ~40 RPC method modules + 50+ core service modules (bootstrap.rs, health_monitor.rs, crash_recovery.rs, etc.) -- Depends on: StateManager, ContainerOrchestrator, config/secrets, external services (Bitcoin, Lightning, FIPS) -- Used by: RPC layer; other services for cross-cutting concerns (mesh, federation, webhooks) - -**State Management Layer:** -- Purpose: Hold canonical application state, broadcast changes to all connected clients, persist snapshots -- Location: `core/archipelago/src/state.rs` (StateManager + data_model.rs) -- Contains: RwLock, broadcast channel, revision counter -- Depends on: DataModel (serde-serializable struct tree) -- Used by: All services that mutate state (container ops, auth, settings) - -**Container Orchestration Layer:** -- Purpose: Podman lifecycle management, image verification, secret injection, crash recovery, adoption -- Location: `core/archipelago/src/container/prod_orchestrator.rs` (1M+ lines; split across boot_reconciler.rs, quadlet.rs, docker_packages.rs, etc.) -- Contains: Manifest parsing, image pull/verify, container create/start/stop, volume mounts, networking -- Depends on: Podman CLI + socket, config parser, image registries, local filesystem -- Used by: RPC container.* methods, BootReconciler loop, crash recovery - -**Frontend Layer (Vue 3):** -- Purpose: Render UI, dispatch RPC calls, maintain local UI state, handle user input -- Location: `neode-ui/src/` -- Contains: Views (pages), Components (reusable UI), Composables (logic hooks), Stores (Pinia), Router -- Depends on: Vue 3, Vue Router, Pinia, RPC client library (custom), D3/Leaflet (charts/maps) -- Used by: Browser clients (desktop, mobile, companion app via WebView) - -## Data Flow - -### Primary Request Path (User Action → Backend → State Sync) - -1. **Frontend user interaction** (click button, type input) → Vue component event handler - - Location: `neode-ui/src/views/*.vue` or `neode-ui/src/components/*.vue` - -2. **Composable dispatches RPC** (e.g., `useContainerInstall()` calls `rpc.container.install()`) - - Location: `neode-ui/src/composables/` (custom or imported from `api/rpc-client.ts`) - -3. **RPC client serializes → HTTP/WebSocket POST to /api/v0** - - Location: `neode-ui/src/api/rpc-client.ts` - - Payload: `{ jsonrpc: "2.0", method: "container.install", params: {...}, id: ... }` - -4. **HTTP Server receives, routes to ApiHandler** - - Location: `core/archipelago/src/server.rs` (listener) → `core/archipelago/src/api/handler/mod.rs` (dispatch) - -5. **ApiHandler checks auth**, extracts body, calls RpcHandler - - Location: `core/archipelago/src/api/handler/mod.rs:handle_request()` - -6. **RpcHandler dispatches by method name** to specific RPC module - - Location: `core/archipelago/src/api/rpc/mod.rs:call()` → routing to `core/archipelago/src/api/rpc/container.rs:install()` - -7. **Service method executes** (e.g., `container.rs:install()` calls orchestrator, updates state) - - Location: `core/archipelago/src/api/rpc/container.rs` (calls methods on ContainerOrchestrator) - -8. **StateManager.update_data()** broadcasts the new state to all WebSocket subscribers - - Location: `core/archipelago/src/state.rs:update_data()` → broadcast channel - - All connected WebSocket clients receive `{ rev: N, data: {...} }` update - -9. **Frontend receives state update**, updates Pinia stores, re-renders UI - - Location: `neode-ui/src/stores/` (Pinia stores mutate) → Vue reactivity chain → DOM update - -**State Management:** -- All reads from `StateManager` go through `get_snapshot()` which acquires read-lock -- All writes go through `update_data()` which acquires write-lock + increments revision -- Broadcast channel has ~100-message buffer; slow subscribers may lose old updates (by design — UI only needs latest) -- WebSocket clients re-sync on reconnect via `get_snapshot()` call (full state transfer) - -### Secondary Flow: Scheduled Reconciliation (Convergence Loop) - -1. **BootReconciler spawned at startup** in `main.rs` - - Location: `core/archipelago/src/main.rs` (line ~338-348) - -2. **Reconciler runs every `RECONCILER_DEFAULT_INTERVAL`** (~30s typical) - - Location: `core/archipelago/src/container/boot_reconciler.rs:run_forever()` - -3. **Compares desired manifests (disk + registry catalog) vs actual Podman state** - - Looks for: containers missing, containers orphaned, image updates, secret changes - -4. **Applies remediation** (create, delete, restart containers) - - Calls: orchestrator.reconcile_*() methods - -5. **Logs changes, broadcasts state update if anything changed** - - Frontend receives update, shows user the reconciled app state - -This ensures apps survive crashes, OTA updates, or manual Podman edits — the desired state always converges. - -## Key Abstractions - -**ContainerOrchestrator trait:** -- Purpose: Abstract container lifecycle behind a trait so Prod (Podman-based) and Dev (in-memory) modes can coexist -- Examples: `core/archipelago/src/container/prod_orchestrator.rs`, `core/archipelago/src/container/dev_orchestrator.rs` -- Pattern: Trait-based strategy; RpcHandler holds `Arc`, switches at runtime -- Methods: create, start, stop, delete, adopt, list, reconcile, install, upgrade - -**Manifest (YAML-based declarative app):** -- Purpose: Fully describe an app's container, dependencies, secrets, ports, UI in one file -- Examples: `/opt/archipelago/apps/*/manifest.yml` (on-disk) or registry-delivered catalogs -- Pattern: Custom extensions over OCI/Docker Compose (e.g., `interfaces.main.ui`, `generated_secrets`) -- Parsed into: `container::manifest::Manifest` struct, consumed by orchestrator - -**RPC Method Modules:** -- Purpose: Group related JSON-RPC methods by domain (auth, container, bitcoin, mesh, etc.) -- Examples: `core/archipelago/src/api/rpc/auth.rs`, `core/archipelago/src/api/rpc/bitcoin.rs` -- Pattern: Each module exports `pub async fn method_name(handler, params) -> Result` -- Registration: Hardcoded dispatch in `RpcHandler::call()` (no reflection; methods are explicit) - -**BlobStore (Content-Addressed):** -- Purpose: Store attachments/files by SHA-256 hash; issue time-limited capability tokens for access -- Examples: Used by mesh.send-content, federation attachments, backup archives -- Pattern: Capability-based access control (CBAC); tokens scoped to issuer pubkey + hash -- Located: `core/archipelago/src/blobs.rs` + `core/archipelago/src/content_server.rs` - -**StateManager + DataModel:** -- Purpose: Single source of truth for UI state; broadcast updates to all clients -- Pattern: Read-write lock over a serde-serializable struct tree; broadcast channel for efficiency -- Persistence: Most state is ephemeral (app listings, UI settings); durable state persists to disk separately -- Clients: Frontend (WebSocket subscriber), internal services (read via get_snapshot), monitoring/debug - -**Session Store:** -- Purpose: Track authenticated HTTP sessions (cookie → user identity mapping) -- Examples: SQLite-backed or in-memory store -- Pattern: Session token issued at login, validated on each request, expires after TTL -- Used by: ApiHandler auth check, rate limiter (per IP + per user) - -## Entry Points - -**Backend Daemon (Binary):** -- Location: `core/archipelago/src/main.rs` -- Triggers: `systemd start archipelago.service` or manual `./archipelago` on development node -- Responsibilities: Parse config, init tracing, load/reconcile containers, start HTTP server, spawn background tasks -- Key setup: Load identity → setup auth → spawn orchestrator → load manifests → start reconciler → start server - -**Frontend SPA:** -- Location: `neode-ui/src/main.ts` -- Triggers: Browser loads `/index.html` (served by HTTP server from `/opt/archipelago/web-ui/`) -- Responsibilities: Boot Vue app, setup Router, setup Pinia stores, establish WebSocket to backend -- Key setup: Mount app → router ready → fetch initial state → subscribe to updates - -**RPC Endpoints (HTTP + WebSocket):** -- Location: `core/archipelago/src/api/` (handler routes requests here) -- Endpoint: `/api/v0` (JSON-RPC 2.0 POST or WebSocket upgrade) -- Methods: ~200+ RPCs across domains (auth, container, bitcoin, mesh, federation, etc.) -- Example: `POST /api/v0` with body `{"jsonrpc": "2.0", "method": "auth.login", "params": {...}, "id": 1}` - -**Background Tasks (Spawned at startup):** -- BootReconciler: Periodic manifest reconciliation loop -- Health Monitor: Periodic app health checks + restart -- Update Scheduler: Periodic app update checks -- Mesh Service: P2P mesh listener + sender (federation, LoRa) -- Webhook Relay: Listens for inbound webhooks, broadcasts to subscribers -- WebSocket Listener: Upgraded HTTP connections → broadcast state subscriber -- See: `core/archipelago/src/main.rs` (lines ~400-450 show the spawned tasks) - -## Architectural Constraints - -- **Single event loop** — All I/O-bound work runs on a single Tokio multi-threaded runtime; no worker threads by default (some container ops are blocking, run in tokio::task::spawn_blocking) -- **Global state via broadcast** — StateManager broadcasts to all WebSocket clients; no request-response for state changes (async by design) -- **Container state mutability** — Podman state can drift from manifest (manual edits, crashes); reconciler runs periodically to converge -- **No in-process data consistency** — Multiple services can mutate StateManager concurrently; last write wins (fine for UI; critical ops use locks) -- **Shared blob store** — All services that need to share content use the same BlobStore instance (single cap_key, single root directory) -- **Rate limiting per IP + method** — Prevents brute-force login, but shared IPs see shared limits (edge case: family users, proxies) -- **Session cookie same-site** — WebSocket + HTTP POST must be same-origin; CORS headers controlled by ApiHandler - -## Anti-Patterns - -### Circular RPC Dispatches - -**What happens:** An RPC method calls back into another RPC method, forming a cycle (e.g., auth.login → container.list → auth.check_permission → auth.login) -**Why it's wrong:** Deadlocks on RwLocks, infinite loops on state broadcasts, unclear error messages, hard to debug -**Do this instead:** Pass check result as a side-effect from the outer method; compute permissions once at the start. Use composable patterns in frontend instead (e.g., `useCanInstall()` checks perms once per component mount). - -### Synchronous blocking in RPC handlers - -**What happens:** RPC method calls `.unwrap()` on Podman command result, blocking the entire event loop -**Why it's wrong:** One slow container op (e.g., large image pull) blocks all concurrent users -**Do this instead:** Use `tokio::task::spawn_blocking()` for I/O that may take >100ms. See `core/archipelago/src/container/docker_packages.rs` for examples. - -### Hardcoding paths in app RPC modules - -**What happens:** `bitcoin.rs` hardcodes `/opt/archipelago/data/bitcoin.conf` instead of using `config.data_dir` -**Why it's wrong:** Dev mode, tests, and alternate installs all fail with "not found" -**Do this instead:** Read from `Config` struct, which is passed to every RPC method. See `core/archipelago/src/api/rpc/bitcoin.rs:status()` for correct pattern. - -### Frontend state outside Pinia stores - -**What happens:** Components use component-local ref<> for app list, duplicate the StateManager's data -**Why it's wrong:** Stale data after OTA updates, inconsistent with other users on the same node, race conditions on install/uninstall -**Do this instead:** Always derive from Pinia stores (e.g., `useAppStore().apps`). Stores subscribe to WebSocket updates. See `neode-ui/src/stores/appStore.ts`. - -### Not handling WebSocket reconnection - -**What happens:** Frontend goes offline for 10s (network glitch), WebSocket closes, frontend doesn't re-sync state -**Why it's wrong:** UI shows stale data (app still "installing" when actually done), user clicks again, double-action happens -**Do this instead:** WebSocket reconnect handler should re-fetch full state (`node.status`, etc.), re-subscribe. See `neode-ui/src/api/rpc-client.ts` for the reconnect loop. - -## Error Handling - -**Strategy:** Defensive layering — errors are caught at each tier, logged, and converted to user-facing messages. - -**Patterns:** -- HTTP layer: 4xx/5xx with JSON error (no 500s for logic errors; only for crashes) -- RPC layer: Serialize error as `{ error: { code: N, message: "...", data: {...} } }` per JSON-RPC spec -- Service layer: Use `anyhow::Result` + `?` operator for early exit; convert to `RpcError` at handler boundary -- Frontend: Catch RPC errors, show toast/modal, log to console (never crash the app) - -**Critical paths:** -- Auth failure: 401 Unauthorized + "Invalid password" (no "user not found" to leak usernames) -- Container ops: If reconciler sees drift, logs it but continues (never crashes the daemon) -- Image pull failure: Fallback to last-cached version if network timeout (user is never blocked on external registries) -- Podman socket unavailable: Return 503 Service Unavailable (user sees "Archipelago is starting") - ---- - -*Architecture analysis: 2026-07-29* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md deleted file mode 100644 index 33b335c3..00000000 --- a/.planning/codebase/CONCERNS.md +++ /dev/null @@ -1,195 +0,0 @@ -# Codebase Concerns - -**Analysis Date:** 2026-07-29 - -## Tech Debt - -**Federation node removal tombstone gap:** -- Issue: `federation::remove_node()` (`core/archipelago/src/federation/storage.rs:180-197`) calls `tombstone_did()` at line 193 but explicitly drops the error with `let _ = …`. If tombstone write fails (disk I/O, permission, transient), the peer is removed from `nodes.json` but never actually recorded as removed, so the next background sync/notify-join silently re-adds it. -- Files: `core/archipelago/src/federation/storage.rs:180-197`, `core/archipelago/src/api/rpc/federation/handlers.rs:272-300` -- Impact: Federation peers marked for removal can reappear after the next sync cycle, confusing the operator and potentially re-establishing unwanted connections. -- Fix approach: Surface the tombstone-write failure instead of swallowing it; consider retry logic with backoff; add integration test via `tests/multinode/smoke.sh` to verify removal sticks across sync cycles. - -**Container reconciler observability gap:** -- Issue: No metrics distinguish "settling after restart" from "flapping" — container thrashing is invisible until anecdotal reports. No per-app restart counter or log line when an app restarts >N times in M minutes. -- Files: `core/archipelago/src/container/prod_orchestrator.rs` (reconciler loop), `core/archipelago/src/health_monitor.rs` -- Impact: Silent restart storms go unnoticed; users see frequent service interruptions without diagnostics; operator can't distinguish normal convergence from a crash loop. -- Fix approach: Add per-app restart counter + log line when threshold exceeded; emit metric on each restart; wire restart count into health/status RPC output. - -**Failed systemd unit self-healing gap:** -- Issue: When a Quadlet-backed app's `.service` unit enters `failed` state (e.g., exit 255), the reconciler does not automatically `reset-failed` + `start` it. The unit sits failed until the operator manually intervenes or the service restarts. -- Files: `core/archipelago/src/container/prod_orchestrator.rs` (reconcile loop) -- Impact: Apps with transient failures go down and stay down; no automatic recovery; operator must manually reset or restart the orchestrator. -- Fix approach: Add reconcile step: quadlet-backed app whose `.service` is `failed` and not user-stopped → call `systemctl --user reset-failed ` + `start`; add backoff to avoid busy-loop on persistent failures. - -**Bitcoin RPC credentials not retrieved from config/secrets:** -- Issue: `core/container/src/bitcoin_simulator.rs:158` has a TODO marking hardcoded (or missing) RPC credentials in the Bitcoin simulator real-mode path. Credentials should be fetched from the secret store. -- Files: `core/container/src/bitcoin_simulator.rs:155-165` -- Impact: Bitcoin simulator in real mode (Testnet/Mainnet) cannot authenticate to the node; RPC calls fail. -- Fix approach: Inject `SecretsProvider` into `BitcoinSimulator::new()` or pass credentials as constructor args; fetch via `config/secrets` at runtime; handle credential rotation. - -**Container security policies not wired in:** -- Issue: `core/security/src/container_policies.rs` generates AppArmor/SELinux profiles but the `apply_profile()` function has a TODO at line 71: "Configure Podman to use the profile" — the profiles are generated but never applied to running containers. -- Files: `core/security/src/container_policies.rs:63-75` -- Impact: Security profiles exist but provide zero protection; containers run without the intended isolation constraints. -- Fix approach: Pass `--security-opt apparmor=` (or SELinux equivalent) to Podman at container creation; verify profile loads via `apparmor_status`; add CI check that profiles compile cleanly. - -**Dynamic resource adjustment not implemented:** -- Issue: `core/performance/src/resource_manager.rs:86` has a TODO for dynamic resource adjustment based on usage. The allocator is static; no adaptive rebalancing when load patterns shift. -- Files: `core/performance/src/resource_manager.rs:86-88` -- Impact: Resource allocation is rigid; a node with skewed usage (e.g., one app consuming all memory) has no mechanism to rebalance dynamically. -- Fix approach: Monitor per-app resource usage via cgroup stats; implement feedback loop to adjust limits; gate on production deployment (likely Phase 3+). - -## Known Bugs - -**Multinode RPC robustness gap:** -- Symptoms: The `node_rpc()` function in `tests/multinode/lib/multinode.bash` lacks `--max-time` on curl calls — a slow server-side RPC can hang the test suite indefinitely with zero feedback. -- Files: `tests/multinode/lib/multinode.bash` (exact line TBD; see grep for `node_rpc`) -- Trigger: Run multinode federation/mesh test against a slow or overloaded node; curl will block forever. -- Workaround: Manually kill the test process and diagnose the hanging RPC manually; no automatic timeout recovery. -- Fix approach: Add `--max-time 30` to all curl calls in `node_rpc()`; re-run `tests/multinode/smoke.sh` to verify. - -## Security Considerations - -**Secrets environment variable exposure risk:** -- Risk: Bitcoin and other service credentials are materialized as env vars in `ARCHIPELAGO_*` (e.g., `BITCOIN_RPC_PASSWORD`). Env vars are visible via `/proc//environ` and potentially logged. -- Files: `core/archipelago/src/container/prod_orchestrator.rs`, `core/container/src/manifest.rs`, `core/archipelago/src/api/rpc/package/config.rs` -- Current mitigation: Secrets are declared as `generated_secrets` in manifests and materialized 0600/rootless; the orchestrator avoids logging values. -- Recommendations: Audit all env-var passing to containers; consider switching high-sensitivity secrets (bitcoin RPC, LND macaroons) to file-based secrets mounted read-only; add audit logging for secret access. - -**Federation DID validation incomplete:** -- Risk: Federation peer DIDs are added via the RPC without cryptographic verification of ownership. A compromised peer could advertise arbitrary DIDs. -- Files: `core/archipelago/src/api/rpc/federation/handlers.rs` (add-node path), `core/archipelago/src/federation/storage.rs` -- Current mitigation: DIDs are stored locally; transitive federation discovery uses the tombstone list to block removed peers. -- Recommendations: Add DID-ownership proof (e.g., signed proof-of-identity) before accepting a peer's advertised DID; document the trust model; consider user warnings when adding peers. - -**AppArmor profiles overly permissive:** -- Risk: Generated AppArmor profiles use blanket `network,` instead of per-port/protocol rules. Readonly flag is checkbox only, not enforced per actual app needs. -- Files: `core/security/src/container_policies.rs:46-54` -- Current mitigation: None (profiles not applied). -- Recommendations: Refine per-app capabilities based on manifest's declared needs; add integration test verifying readonly mounts are enforced; apply profiles in development before prod. - -## Performance Bottlenecks - -**Container thrashing during reconcile:** -- Problem: Restarting `archipelago.service` SIGKILLs every container, forcing a full rebuild over several minutes. Uninstall + reinstall loops can cascade-trigger restarts. -- Files: `core/archipelago/src/container/prod_orchestrator.rs` (the reconciler's desired-state machine) -- Cause: Pre-Phase-3 architecture: containers run in systemd cgroup, not as independent Quadlet units. -- Improvement path: Phase-3 Quadlet default-flip (`config.rs:256`) — each app becomes an independent `.container` unit; restart only the affected app, not the entire cgroup. - -**Reconciler churn on boot:** -- Problem: Boot reconciler makes multiple passes reconciling drift; during each pass, containers may be recreated. Post-OTA health checks deliberately skip per-app container assertions because of restart-storm unpredictability. -- Files: `core/archipelago/src/container/prod_orchestrator.rs`, `core/archipelago/src/bootstrap.rs` -- Cause: Multi-pass reconciliation + no incremental diff detection. -- Improvement path: Consolidate reconciler into single pass for boot; cache manifest/config diffs to avoid redundant comparisons; add boot-only fast-path. - -**Bitcoin IBD on .198 stalled (disk I/O):** -- Problem: .198 bitcoin is mid-IBD with only 21% progress; disk is 448GB (below 1TB archival threshold); load is high (~3–5). -- Files: `tests/multinode-testing-plan.md` (documented issue) -- Cause: Undersized/slow disk; concurrent workload. -- Improvement path: User decision required: swap in a different node (already done for gate run, using .5 instead) or add storage + wait for sync. Not a code issue. - -## Fragile Areas - -**Uninstall + reinstall lifecycle:** -- Files: `core/archipelago/src/api/rpc/package/install.rs`, `core/archipelago/src/container/quadlet.rs:disable_remove()`, `neode-ui/src/components/AppCard.vue` -- Why fragile: Pre-2026-07-26, `quadlet::disable_remove()` called systemd + podman with no timeouts, causing hangs. Fixed by commit `71cc9ac4` (added `QUADLET_STOP_TIMEOUT`, SIGKILL escalation, reset-failed). AppCard was hardcoding uninstall bar to "stuck full-red" (fixed `9f17ba68`). Tests for reinstall/cascade are still opt-in. -- Safe modification: Any changes to the uninstall path must be tested via `cascade-uninstall.bats` (7/7 on .228); extend coverage to multi-container stacks (immich, btcpay). Verify on .228 before fleet roll. -- Test coverage: `tests/lifecycle/bats/cascade-uninstall.bats` exists but not in canonical gate; must opt-in with `ARCHY_GATE_CASCADE=1`. - -**Production orchestrator state machine:** -- Files: `core/archipelago/src/container/prod_orchestrator.rs` (6291 lines) -- Why fragile: Largest file in the codebase; owns install/start/stop/restart/remove/upgrade for every app; per-app mutex + RwLock concurrency model; complex dependency resolution, adoption scan, Quadlet rendering, and host-port-wait logic interleaved. -- Safe modification: Understand the per-app mutex protocol before touching state mutation; test all changes via the lifecycle gate on .228; use the adoption scan + manifest merge logic for any new manifest evolution. -- Test coverage: 667 unit tests green (2026-07-01); lifecycle gate covers ~8 core apps; ~30 apps untested in gate. - -**Mesh radio configuration + boot race:** -- Files: `core/archipelago/src/mesh/meshtastic.rs`, `core/archipelago/src/mesh/mod.rs`, tests at `tests/lifecycle/bats/meshtastic.bats` -- Why fragile: Radio boot-race fixed (2026-07-28, `a8c4694c`/`3f76b496`); on-air config apply must finish before device is used. Earlier versions had probe-boot-race + live config propagation issues. Must verify on real hardware. -- Safe modification: Any mesh changes require E2E test on real LoRa radios (dev-box ↔ x250-dev, or fleet broadcast); unit tests alone won't catch RF timing issues. -- Test coverage: 8-stage on-air smoke test in `tests/multinode/meshtastic.sh` (run manually; not in canonical gate). - -**Lightning payment state machine:** -- Files: `core/archipelago/src/api/rpc/lnd/wallet.rs:payinvoice()` -- Why fragile: Slow multi-hop payments (>15s) previously surfaced as "failed" while settling in background; client-side 15s timeout was aborting the wait. Fixed by commit `614a0f5a` (120s wait, pending status, lnd.paymentstatus poll). Must verify on Framework PT with real multi-hop. -- Safe modification: Any lnd state changes must test full payment lifecycle: invoice creation, encoding, send, multi-hop wait, settlement confirmation. Verify on Framework PT before release. -- Test coverage: Local LND payinvoice smoke test; no multinode lightning routing test in gate. - -## Scaling Limits - -**Uninstall progress bar truthfulness:** -- Current capacity: Uninstall now has timeouts (fixed 2026-07-26) but progress-bar still reports fake stages (full-red full-opacity). -- Limit: Long uninstalls (>30s) show no real progress; bar claims "uninstalling" for the full duration. -- Scaling path: Backend must emit real progress events (% complete, stage name); UI must poll + display truthfully; integrate into all 5 gate iterations (not just 1 throw-away app). - -**Federation node list deduplication on disk bloat:** -- Current capacity: `federation/storage.rs:dedup_nodes_by_onion()` reads entire nodes.json into memory each time a node is added/synced. At N federated peers, O(N) memory + O(N²) comparisons per operation. -- Limit: No hard limit measured; scales fine up to hundreds of peers. Beyond 1000+ peers, memory/time may become visible. -- Scaling path: Switch to a disk-backed database (e.g., rocksdb) for federation state if peer count grows; or implement incremental dedup on disk writes (preserve dedup state, only recompute on load). - -**Lifecycle gate iteration count:** -- Current capacity: `ARCHY_ITERATIONS=5` runs 5 full cycles (stop/start/restart/survive per app). Entire run takes ~8–12 hours on .228. -- Limit: Cannot easily scale to 10+ iterations without timeout risks; per-app timeout tuning is manual. -- Scaling path: Add per-app timeout tuning (manifest field); parallelize per-app tests where safe (currently serial to avoid contention). - -## Dependencies at Risk - -**Reticulum transport daemon process group:** -- Risk: Pre-fix (before `be50c886`), process group wasn't cleaned up on drop. Fork-bombs or dangling processes possible under error conditions. -- Impact: Stale reticulum processes accumulating over time; resource leaks on node. -- Migration plan: Code fix already deployed (commit `7a7fec21`); no active risk. Monitor fleet for stale python processes post-deployment. - -**Podman socket mount security model:** -- Risk: Apps mounting `/run/podman/podman.sock` get full container-management access. Not restricted by the security policy (AppArmor profiles not applied). -- Files: `core/archipelago/src/container/prod_orchestrator.rs:135-137` (detection), manifests for apps with podman mounts (e.g., portainer) -- Impact: A compromised app with podman socket access can start/stop/delete any container on the node. -- Recommendation: Restrict podman socket mounts to admin-only apps (portainer, docker-api tools); document risk; consider socket filtering layer (selinux context, etc.) once AppArmor is wired. - -**Bitcoin version multi-version branch not fleet-wide:** -- Risk: Branch `bitcoin-version-bulletproof` (base `095a76cd`) carries multi-version support but hasn't been deployed fleet-wide yet. .228 carries it; others still run single version. -- Impact: Users on single-version nodes can't switch versions; version mismatch across fleet breaks federation. -- Migration plan: Coordinated OTA + catalog publish + `:latest` repoint sequencing per `docs/bitcoin-version-bulletproof-rollout.md`. Awaiting user decision on timing. - -## Missing Critical Features - -**Developer tooling CLI suite:** -- Problem: Third-party developers need `archy app validate/render/local-install/lifecycle-test` tooling before external registry launches. -- Blocks: External marketplace (workstream C); external developer onboarding. -- Status: Not yet built; documented in APP-PACKAGING-MIGRATION-PLAN.md step 5. - -**Manifest-distributed registry flip:** -- Problem: Manifests still travel via OTA disk rsync. The signed catalog currently distributes only image overrides, not full manifests. Workstream B phases 1+2 done; not yet fleet-deployed. -- Blocks: Cannot confidently add/bump apps without re-signing the catalog. -- Status: Code ready; flip awaits authorization + timing call from user. - -**Phase-3 Quadlet default-flip:** -- Problem: Orchestrator still uses legacy cgroup-based container management; Phase-3 `use_quadlet_backends` switch exists but is opt-in only. -- Blocks: Resolves container thrashing; unlocks independent app restarts; unblocks lifecycle perfection (workstream F). -- Status: Code validated on .228/.198 (commit pending); ready to flip when multinode gate passes. - -## Test Coverage Gaps - -**~30 apps with zero app-specific assertions:** -- What's not tested: Apps like grafana, jellyfin, vaultwarden, penpot, nextcloud, photoprism, uptime-kuma, homeassistant, etc. have no app-specific health checks beyond "container running." -- Files: `tests/lifecycle/bats/all-apps-matrix.bats`, `tests/lifecycle/bats/all-apps-lifecycle.bats` (generic baseline coverage) -- Risk: App-specific bugs (API down, data corruption, dependency failure) go unnoticed until user encounters them. -- Priority: Medium — baseline coverage is a real safety net; app-specific assertions are a "nice to harden" backlog item, not a gate blocker. -- Approach: Add per-app health RPC endpoints or HTTP probes; wire into the gate as opt-in per-app test suites. - -**Progress UI assertions incomplete:** -- What's not tested: Install + uninstall must report monotonic, truthful progress. No stage/percentage assertions in the gate. -- Files: `neode-ui/src/components/AppCard.vue`, `core/archipelago/src/api/rpc/package/install.rs` (backend progress events) -- Risk: Silent hangs or fake progress bars are invisible to the gate. -- Priority: High — immich/grafana uninstall was stuck full-red (fixed); progress truthfulness is part of definition of done for workstream F. -- Approach: Backend must emit real progress events; UI must display & test them; integrate into canonical gate (currently opt-in). - -**All-apps matrix in cascade gate:** -- What's not tested: `ARCHY_GATE_CASCADE=1` runs ONE throwaway app's uninstall/reinstall. Must extend to multi-container stacks (immich, btcpay, mempool) and all ~40 installed apps. -- Files: `tests/lifecycle/bats/cascade-uninstall.bats` (single-app variant) -- Risk: Multi-container app uninstall bugs (e.g., orphan postgres container) go undetected. -- Priority: High — part of workstream F definition of done. -- Approach: Parametrize cascade test over all manifest IDs; run 5 cascades total (not 5 per app to save time); gate-pass requires zero ghost containers post-uninstall. - ---- - -*Analysis based on codebase state 2026-07-29. Issues tracked in `docs/UNIFIED-TASK-TRACKER.md` (day-to-day) and `docs/PRODUCTION-MASTER-PLAN.md` (historical narrative).* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md deleted file mode 100644 index 2cd4f060..00000000 --- a/.planning/codebase/CONVENTIONS.md +++ /dev/null @@ -1,159 +0,0 @@ -# Coding Conventions - -**Analysis Date:** 2026-07-29 - -## Naming Patterns - -**Files:** -- TypeScript/Vue: PascalCase for components (e.g., `ToggleSwitch.vue`, `SendBitcoinModal.vue`), camelCase for composables and stores (e.g., `useFileType.ts`, `controller.ts`) -- Rust: snake_case for modules and files (e.g., `bitcoin_rpc.rs`, `storage_crypto.rs`) -- Test files: co-located with source in `__tests__/` subdirectories with `.test.ts` or `.spec.ts` suffix for Vitest, `.bats` for shell tests -- Constants in TypeScript use UPPER_SNAKE_CASE within modules (e.g., `IMAGE_EXTS`, `CATEGORY_COLORS` in `useFileType.ts`) - -**Functions:** -- TypeScript/Vue: camelCase for all functions (e.g., `getFileCategory`, `formatSize`, `useFileType`) -- Composables: `use` prefix for Vue composables (e.g., `useFileType`, `useToast`, `useMessageToast`) — exported as named exports or default exports -- Store functions (Pinia): defined with snake_case action names, exported from `defineStore` factory -- Rust: snake_case for all functions and methods (e.g., `doesnt_reallocate`, following Rust conventions) - -**Variables:** -- TypeScript: camelCase for local variables and reactive refs (e.g., `modelValue`, `isActive`, `gamepadCount`) -- Refs (Vue 3): prefix not required, but convention is lowercase start (e.g., `const ext = ref('jpg')`) -- Computed properties: camelCase, explicit `.value` suffix in templates when needed -- Parameters: camelCase, typed explicitly in TypeScript (e.g., `password: string`, `isDir: Ref`) - -**Types:** -- TypeScript: PascalCase for type aliases and interfaces (e.g., `RPCOptions`, `FileCategory`, `CatalogVersionInfo`) -- Union types: PascalCase (e.g., `PendingState = 'pending' | 'sent' | 'approved'`) -- Component props: typed with `defineProps<{ ... }>()` syntax in `';` - — a classic (non-module) script injected at end of head still executes - BEFORE the SPA's deferred module bundle, which is what the seeding needs. - Add `location = /__demo/indee-demo-signin.js { root /usr/share/nginx/html; }` - (or alias) inside the 2101 server so the seed script is served same-origin - to the iframe. Update the comment block that currently explains why - IndeeHub is not proxied (lines ~106-109) to describe the new :2101 design. - - Create neode-ui/docker/indee-demo-signin.js: a small plain-JS classic - script, clearly headed with a comment stating it is PUBLIC-DEMO-ONLY and - that the embedded key is a freshly generated THROWAWAY demo identity, not - a real secret. Generate ONE fresh secp256k1 keypair at implementation time - (e.g. `node -e` with a tiny script using any available schnorr/secp lib, or - a one-off `npx` of nostr-tools in the scratchpad — the generator itself is - not committed) and embed hex sk + hex pk as constants. The script: if - `localStorage.getItem('indeedhub-accounts')` is empty/absent, write the - two keys IndeeHub's boot-restore reads — `indeedhub-accounts` (JSON array - with ONE serialized private-key account: verify the exact `type` string - and common-field shape against the live bundle per verified_findings, shape - `{ id, type, pubkey, signer: { key } }` + whatever `loadCommonFields` - round-trips, give it a friendly name/metadata like "Archy Demo" if the - shape supports it) and `indeedhub-active-account` (that account's id). - Because the script runs on the :2101 origin inside the iframe, this - touches only the proxied app's isolated storage. IndeeHub then restores - the account on boot and self-signs with its own bundled signer — no - window.nostr and no parent bridge required. Do NOT define a partial - `window.nostr` in this approach (a pubkey-only shim with a broken - signEvent causes worse failures than no shim). - - FALLBACK (only if live testing in Task-3 verification shows the seeded - account shape is not accepted): seed an `"extension"`-type account - instead, define a `window.nostr` postMessage client in this same script - (request/response protocol matching useNostrBridge: post - `{type:'nostr-request', id, method, params}` to `window.parent`, resolve on - `{type:'nostr-response', id, ...}`), and implement `node.nostr-sign` / - `identity.nostr-sign` in mock-backend.js with real schnorr signatures over - the same throwaway key (add `nostr-tools` to neode-ui dependencies — it is - pure JS and Dockerfile.backend runs `npm install` over package.json). - Prefer the primary approach; only fall back with evidence. - - Wire the plumbing: `EXPOSE 2101` in Dockerfile.web (the seed script is - already inside `neode-ui/` so the existing `COPY neode-ui/ ./` + - dist copy do NOT ship it — add an explicit - `COPY neode-ui/docker/indee-demo-signin.js /usr/share/nginx/html/__demo/indee-demo-signin.js` - in the nginx stage of Dockerfile.web; it lands only in the demo web image, - never in real-node artifacts). Publish the port in docker-compose.demo.yml - (`"2101:2101"` on neode-web) and demo-deploy/docker-compose.yml (use an - env-overridable mapping consistent with its existing `DEMO_WEB_PORT` - style, e.g. `"${DEMO_INDEE_PORT:-2101}:2101"`, and document it in that - file's header comment). Read docker-entrypoint.sh first and make sure the - new server block survives its template substitution exactly like the - existing blocks (same escaping convention for nginx `$` variables); touch - the entrypoint only if its substitution list needs it. - - Do not put any host IP in any of these files; upstream hostname - indee.tx1138.com is fine. - - - docker run --rm -v "$PWD/neode-ui/docker/nginx-demo.conf:/etc/nginx/nginx.conf:ro" nginx:alpine nginx -t (or, if docker unavailable locally, `nginx -t -c` via a podman run — config must parse). Plus: grep -c "2101" neode-ui/docker/nginx-demo.conf docker-compose.demo.yml demo-deploy/docker-compose.yml neode-ui/Dockerfile.web — each ≥1; grep -q "indee-demo-signin" neode-ui/docker/nginx-demo.conf && grep -qi "throwaway" neode-ui/docker/indee-demo-signin.js - - nginx config parses with the new :2101 whole-origin proxy block (framing headers stripped, sub_filter injection, WS upgrade); seed script exists with labelled throwaway demo key and idempotent localStorage seeding; both compose files publish 2101; demo web image copies the script and exposes the port; no host IPs added anywhere. - - - - Task 2: demo frontend — iframe launch via :2101 and no identity-picker wall - neode-ui/src/composables/useDemoIntro.ts, neode-ui/src/views/appSession/useAppIdentity.ts - - In useDemoIntro.ts: remove `indeedhub` from `DEMO_EXTERNAL_URLS` (delete - the map entirely if it becomes empty, simplifying `isDemoExternal` to - return false — keep the exported function so call sites in appLauncher.ts - and AppSession.vue compile unchanged). Make `demoAppUrl('indeedhub')` - return the proxied origin built at runtime: - `${window.location.protocol}//${window.location.hostname}:2101/` - (hostname, never a hardcoded host/IP — works on any deploy host). Keep - `isDemoApp('indeedhub')` true (it must stay in the demoable set so the - NEW_TAB bypass in appLauncher.openSession and AppSession.mustOpenNewTab - keeps routing it into the in-app iframe session, and so the install - button stays enabled). Update the file-header comment block that - currently documents the external-tab workaround to describe the :2101 - whole-origin proxy design instead. SSR-safety is not a concern (Vite SPA) - but guard `typeof window !== 'undefined'` if other tests import the module - in node context — check the existing unit tests under - src/views/appSession/__tests__/ and src/stores/__tests__/ for assertions - about indeedhub being demo-external and update them to the new behavior. - - In useAppIdentity.ts: gate the picker for the demo. Import IS_DEMO from - useDemoIntro and in `onIframeLoadIdentity` / `handleIdentityRequest`, - when IS_DEMO is true, never set `showIdentityPicker` — the demo visitor - must not be interrupted by an identity modal (the embedded IndeeHub is - already signed in via the seeded account from Task 1, and `sendIdentity`'s - `identity.sign` RPC is not what logs it in). Real-node behavior - (picker on first launch) is untouched because IS_DEMO is compile-time - false there. - - - cd neode-ui && npx vitest run src/views/appSession src/stores --silent 2>&1 | tail -5 (all green) && VITE_DEMO=1 npm run build && grep -rq "2101" dist/assets && npm run build && grep -rq "indee.tx1138.com" dist/assets && echo BUNDLE-OK - - Demo build (VITE_DEMO=1) bundle contains the :2101 launch logic (grep hit proves the build didn't silently no-op — per CLAUDE.md); plain build still compiles and demo-gated branches do not alter non-demo behavior; unit tests updated and green; launching indeedhub in demo resolves to the same-host :2101 origin in the iframe session; identity picker suppressed only under IS_DEMO. - - - - Task 3: mock backend — IndeeHub pre-installed on fresh demo sessions - neode-ui/mock-backend.js - - Add an `indeedhub` entry to `staticDevApps` in mock-backend.js using the - existing `staticApp({...})` helper: id `indeedhub`, title `Indeehub` - (match the existing title map at ~line 537 and APP_TITLES), a short/long - description consistent with the marketplace copy ("Bitcoin documentary - streaming platform" per the existing entry), `state: 'running'`, - `lanPort: 8190` (matches the existing port map), icon - `/assets/img/app-icons/indeedhub.png`. Because per-session demo state is - `structuredClone(staticDevApps)`, this alone makes it installed+running on - every fresh session. Then reconcile the rest of the mock so nothing - contradicts installed status: check the marketplace/available-apps mock - responses and any install/uninstall handlers (~lines 540-740, 1900-1960, - 4900+) for `indeedhub` entries that would render it as not-installed or - double-listed, and check `DEMO_APP_PAGES` does NOT grow an indeedhub - placeholder (the demo launch URL bypasses /app/indeedhub/ entirely — the - iframe goes to the :2101 origin). Keep the existing `node.nostr-pubkey` - mock as-is unless Task 1's fallback path was taken (in which case align - its pubkey with the throwaway demo key and add the sign handlers described - there). - - - cd neode-ui && node -e "const s=require('fs').readFileSync('mock-backend.js','utf8'); if(!/staticDevApps[\s\S]*?indeedhub:\s*staticApp/.test(s)) process.exit(1)" && (DEMO=1 timeout 20 node mock-backend.js & sleep 4; curl -s -X POST localhost:5959/rpc/v1 -H 'content-type: application/json' -d '{"method":"server.data","id":1}' -H 'cookie: demo=fresh' | grep -o '"indeedhub"' | head -1; kill %1 2>/dev/null) — expect an indeedhub hit in fresh-session package-data (adapt the RPC method/auth to what the mock actually serves; a login with the demo password first is fine) - - A fresh demo session's package-data includes indeedhub as installed and running with launchable UI; My Apps shows it without an install step; no duplicate/contradictory indeedhub listing in marketplace mocks; mock backend boots cleanly with DEMO=1. - - - - - -## Trust Boundaries - -| Boundary | Description | -|----------|-------------| -| demo nginx :2101 → indee.tx1138.com | demo host proxies an external site; upstream content is served under the demo host | -| iframe (:2101 origin) ↔ parent (:2100 origin) | cross-origin; parent NIP-07 bridge only used in fallback path | -| public visitors → demo host | anyone can drive the proxy | - -## STRIDE Threat Register - -| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | -|-----------|----------|-----------|----------|-------------|-----------------| -| T-gjd-01 | Spoofing | throwaway demo nostr key | low | accept | key is a labelled public demo identity by design; generated fresh, never a real user key; anyone extracting it can only impersonate "the demo visitor" | -| T-gjd-02 | Info disclosure | private release-server IP in served content | high | mitigate | no host IPs added in any changed file; iframe URL derived from window.location.hostname; existing Docker-build scrub+fail guards remain the backstop | -| T-gjd-03 | Tampering | open reverse proxy on :2101 | medium | mitigate | proxy is pinned to a single upstream host (proxy_pass fixed hostname + proxy_ssl_name), no dynamic upstreams, no request-driven destinations — it cannot be used as an open proxy | -| T-gjd-04 | Elevation | header stripping (X-Frame-Options/CSP) | low | accept | stripping applies only to the :2101 demo proxy of one known site, demo image only; real-node builds never carry this config | -| T-gjd-SC | Tampering | npm installs | low | accept | primary path adds no dependencies; fallback path adds only nostr-tools (well-known, verify on npmjs.com before install) | - - - -Local (executor, before commit): -1. nginx config parses (Task 1 verify). -2. Unit tests green; VITE_DEMO=1 build contains ":2101" logic; plain build - unaffected (Task 2 verify). Note: demo-gated strings are tree-shaken out of - the plain build — that is EXPECTED; the bundle-grep for demo strings must be - done on the VITE_DEMO=1 build, which is exactly what the demo Docker image - builds (Dockerfile.web defaults ARG VITE_DEMO=1). -3. Fresh-session mock package-data includes indeedhub (Task 3 verify). -4. Optional full-stack smoke: `docker compose -f docker-compose.demo.yml up - --build` locally, browse http://localhost:2100 in a private window → - login `entertoexit` → IndeeHub installed → launch → iframe renders the - proxied site from http://localhost:2101 with a signed-in account. -5. `git status` — confirm nothing under indeedhub/ is staged, ever. - -Post-deploy on vps2 (orchestrator deploys; verify on http://146.59.87.168:2100): -1. `curl -sI http://146.59.87.168:2101/` returns 200 with NO X-Frame-Options - header and the injected `indee-demo-signin.js` tag in the HTML body - (`curl -s http://146.59.87.168:2101/ | grep indee-demo-signin`). If the - port is unreachable, the vps2 firewall needs 2101 opened — flag to - orchestrator. -2. Fresh private browser window → :2100 → login → IndeeHub shows installed/ - running on the dashboard/My Apps without any install action. -3. Launch IndeeHub → renders inside the in-app iframe (panel/overlay), not a - new tab; content browsable; no identity-picker modal. -4. Signed-in check: IndeeHub header shows an active account (avatar/profile - instead of a sign-in button). If the seeded account shape was rejected - (login wall still visible), execute the documented fallback (extension - account + window.nostr shim + mock signer) and redeploy. -5. View-source/network spot-check: no occurrence of the private - release-server IP in any served response. -6. Repeat-visit check: reload the iframe once — a service worker registered by - IndeeHub may serve cached HTML without the injected tag on later loads; - that is acceptable because localStorage is already seeded on first load, - but confirm sign-in persists. - - - -- Demo visitor on a fresh browser sees IndeeHub installed, launches it into - the in-app iframe, and browses indee.tx1138.com content signed in — zero - clicks spent on install/login/identity modals. -- Real-node build behavior unchanged (all changes IS_DEMO- or demo-image-gated). -- No secrets committed beyond the labelled throwaway demo key; nothing staged - under indeedhub/; demo serves no private release-server IP. -- Work committed in focused commits (infra / frontend / mock) with the - Co-Authored-By trailer and pushed via gitea-ai per CLAUDE.md; docs left to - the orchestrator. - - - -Create `.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-SUMMARY.md` when done. - diff --git a/.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-SUMMARY.md b/.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-SUMMARY.md deleted file mode 100644 index 15f50ae7..00000000 --- a/.planning/quick/260729-gjd-demo-make-indee-tx1138-com-work-in-the-a/260729-gjd-SUMMARY.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -phase: quick-260729-gjd -plan: 01 -subsystem: public-demo -tags: [demo, indeedhub, nginx, reverse-proxy, nostr, mock-backend] -requires: [] -provides: - - "IndeeHub whole-origin demo proxy on :2101 (framing headers stripped, sign-in seeded)" - - "Demo iframe launch of indeedhub via demoAppUrl → :2101/" - - "IndeeHub pre-installed/running on every fresh demo session" -affects: [demo-deploy, neode-ui demo image] -tech-stack: - added: [] - patterns: - - "Whole-origin per-port reverse proxy for frame-busting external SPAs (vs broken path-prefix sub_filter)" - - "localStorage seeding via injected classic script on the proxied origin (applesauce-accounts nsec account)" -key-files: - created: - - neode-ui/docker/indee-demo-signin.js - modified: - - neode-ui/docker/nginx-demo.conf - - neode-ui/Dockerfile.web - - docker-compose.demo.yml - - demo-deploy/docker-compose.yml - - neode-ui/src/composables/useDemoIntro.ts - - neode-ui/src/views/appSession/useAppIdentity.ts - - neode-ui/mock-backend.js -decisions: - - "Primary sign-in path used (seeded nsec account, self-signing) — NIP-07 bridge fallback NOT needed; verified against the live bundle" - - "Dropped `sub_filter_types text/html` (text/html is nginx's default sub_filter type; explicit listing produced a duplicate-MIME warning)" -metrics: - duration: "~50 min" - completed: 2026-07-29 -status: complete ---- - -# Quick Task 260729-gjd: IndeeHub in the Demo Summary - -**One-liner:** Whole-origin nginx proxy of indee.tx1138.com on :2101 with an injected throwaway-nsec sign-in seeder, demo iframe launch via same-host :2101, and IndeeHub pre-installed in every fresh mock-backend session. - -## Commits - -| Task | Commit | Scope | -|------|--------|-------| -| 1 | 69bc3d3f | nginx :2101 whole-origin proxy + indee-demo-signin.js seeder + Dockerfile.web COPY/EXPOSE + both compose files publish 2101 | -| 2 | 66d540f8 | useDemoIntro: DEMO_EXTERNAL_URLS → DEMO_PROXY_PORTS, demoAppUrl builds `//:2101/`; useAppIdentity: picker suppressed under IS_DEMO | -| 3 | d00ca624 | mock-backend.js staticDevApps gains indeedhub (running, lanPort 8190) → installed on every fresh session | - -## What was verified at exec time (live-bundle facts) - -- Live site still serves `X-Frame-Options: SAMEORIGIN`, no CSP; bundle `assets/index-BMWtjRCn.js`. -- Account serialization confirmed by de-minifying the live bundle: private-key account class has `static type="nsec"`, `toJSON` → `{ signer: { key: }, id, pubkey, metadata, type }`; the manager registers the nsec type (`MM(Fe)` registers `mr`) and restores from `indeedhub-accounts` + activates by id from `indeedhub-active-account`. `Vn`/`je` confirmed hex decode/encode. -- Pubkey math independently validated against BIP340 test vectors (sk=1 → Gx, sk=3 → F9308A01…) before embedding the generated pair. Mismatch would trigger the bundle's "Account signer mismatch" guard, so this was load-bearing. - -## Throwaway demo identity - -Freshly generated 2026-07-29 for this task (generator ran in scratchpad, not committed): -- pk `7261540160244ec65ce0bf86ba03997e9b1b3b35c277e416bf1c7ba4271fee31` -- sk embedded in `neode-ui/docker/indee-demo-signin.js`, clearly labelled PUBLIC-DEMO-ONLY / not a secret (threat T-gjd-01: accepted by design). Never a real user key. - -## Local verification results - -1. **nginx parse:** `nginx -t` clean in `nginx:alpine` (podman, with `--add-host neode-backend:127.0.0.1` to satisfy the pre-existing upstream reference). -2. **Live proxy smoke (podman, config + seeder mounted):** `curl` through :2101 → 200, **no X-Frame-Options / CSP**, injected `'"'"';" /tmp/ih-nginx.conf - podman_quick cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true - rm -f /tmp/ih-nginx.conf - CHANGED=true - echo " Injected nostr-provider.js into IndeedHub nginx" - fi - - # Replace DNS-based upstream resolution with hardcoded container IPs - # (podman DNS resolver 127.0.0.11 is unreliable, causing 502 errors) - API_IP=$(podman_quick inspect indeedhub-build_api_1 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null || true) - MINIO_IP=$(podman_quick inspect indeedhub-minio --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null || true) - RELAY_IP=$(podman_quick inspect indeedhub-relay --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null || true) - - if [ -n "$API_IP" ] && [ -n "$MINIO_IP" ] && [ -n "$RELAY_IP" ]; then - podman_exec_quick indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null || true - # Remove DNS resolver lines and replace upstream variables with hardcoded IPs - sed -i "s|resolver 127.0.0.11 valid=30s ipv6=off;||g" /tmp/ih-nginx.conf - sed -i "s|set \$api_upstream http://api:4000;|set \$api_upstream http://$API_IP:4000;|g" /tmp/ih-nginx.conf - sed -i "s|set \$minio_upstream http://minio:9000;|set \$minio_upstream http://$MINIO_IP:9000;|g" /tmp/ih-nginx.conf - sed -i "s|set \$relay_upstream http://relay:8080;|set \$relay_upstream http://$RELAY_IP:8080;|g" /tmp/ih-nginx.conf - sed -i "s|proxy_set_header Host \$host;|proxy_set_header Host \$http_host;|g" /tmp/ih-nginx.conf - podman_quick cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true - rm -f /tmp/ih-nginx.conf - CHANGED=true - echo " Patched IndeedHub nginx with container IPs (API=$API_IP MINIO=$MINIO_IP RELAY=$RELAY_IP)" - fi - - if [ "$CHANGED" = true ]; then - podman_exec_quick indeedhub nginx -s reload 2>/dev/null || true - fi - fi - ' 2>&1 | sed 's/^/ /' || true - - # Run container doctor — auto-fix common container health issues - progress "Running container doctor" - "$SCRIPT_DIR/container-doctor.sh" "$TARGET_HOST" 2>&1 | sed 's/^/ /' || true - - # Post-deploy health check — wait up to 60s for server to come healthy - echo "" - progress "Post-deploy health check" - HEALTH_OK=false - for i in $(seq 1 12); do - POST_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || { echo "WARNING: Post-deploy health check failed for $TARGET_IP_ONLY" >&2; echo "000"; }) - if [ "$POST_HEALTH" = "200" ]; then - echo " Health: OK (200) after $((i * 5))s" - HEALTH_OK=true - break - fi - echo " Health: $POST_HEALTH (waiting... ${i}/12)" - sleep 5 - done - if [ "$HEALTH_OK" = false ]; then - echo " ⚠️ Server did not become healthy within 60s (last: $POST_HEALTH)" - echo " Attempting automatic rollback..." - ssh $SSH_OPTS "$TARGET_HOST" ' - if [ -f /opt/archipelago/rollback/archipelago.bak ]; then - sudo systemctl stop archipelago 2>/dev/null - sudo cp /opt/archipelago/rollback/archipelago.bak /usr/local/bin/archipelago - if [ -f /opt/archipelago/rollback/web-ui.tar ]; then - sudo find /opt/archipelago/web-ui -mindepth 1 -maxdepth 1 ! -name "aiui" ! -name "claude-login.html" -exec rm -rf {} + - sudo tar xf /opt/archipelago/rollback/web-ui.tar -C /opt/archipelago/web-ui - fi - sudo systemctl start archipelago - echo "ROLLBACK_DONE" - else - echo "NO_ROLLBACK_AVAILABLE" - fi - ' 2>/dev/null | while IFS= read -r line; do - if [ "$line" = "ROLLBACK_DONE" ]; then - echo " 🔄 Rollback complete — previous version restored" - elif [ "$line" = "NO_ROLLBACK_AVAILABLE" ]; then - echo " ⚠️ No rollback backup available" - fi - done - echo " Check: sudo journalctl -u archipelago -n 50" - fi - - DEPLOY_END=$(date +%s) - DEPLOY_ELAPSED=$((DEPLOY_END - DEPLOY_START)) - - # Append to local deploy history log (gitignored) - DEPLOY_LOG="$PROJECT_DIR/scripts/deploy-history.log" - echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $DEPLOY_BRANCH@$DEPLOY_COMMIT | dirty=$DEPLOY_DIRTY | target=$TARGET_HOST | ${DEPLOY_ELAPSED}s" >> "$DEPLOY_LOG" - - # Auto-tag successful deploys (only on clean commits, skip if already tagged) - if [ "$DEPLOY_DIRTY" = "0" ]; then - EXISTING_TAG=$(git tag --points-at "$DEPLOY_COMMIT" 2>/dev/null | grep "^v" | head -1) - if [ -z "$EXISTING_TAG" ]; then - LAST_ALPHA=$(git tag -l 'v1.2.0-alpha.*' | sort -V | tail -1 | sed 's/.*alpha\.//') - NEXT_ALPHA=$(( ${LAST_ALPHA:-0} + 1 )) - DEPLOY_TAG="v1.2.0-alpha.${NEXT_ALPHA}" - git tag -a "$DEPLOY_TAG" "$DEPLOY_COMMIT" -m "Auto-tagged by deploy to $TARGET_IP_ONLY" 2>/dev/null && \ - echo " Tagged: $DEPLOY_TAG" || true - fi - fi - - echo "" - echo "$(timestamp) ✅ Deployed to live system! (${DEPLOY_ELAPSED}s total)" - echo " Commit: $DEPLOY_BRANCH @ $DEPLOY_COMMIT (dirty=$DEPLOY_DIRTY)" - echo " Backend: $(ssh $SSH_OPTS "$TARGET_HOST" 'sudo systemctl is-active archipelago')" - echo " Web UI: http://$TARGET_IP_ONLY" - echo " PWA install: https://$TARGET_IP_ONLY (use HTTPS, accept cert once, then Install app)" -else - echo "" - echo "✅ Build complete!" - echo "" - echo "To test frontend dev server:" - echo " ssh $TARGET_HOST" - echo " cd ~/archy/neode-ui && npm run dev -- --host 0.0.0.0" - echo " Then open: http://$(echo "$TARGET_HOST" | cut -d@ -f2):5173" - echo "" - echo "To deploy to live system:" - echo " ./scripts/deploy-to-target.sh --live" -fi diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index 6751f51b..242ed15d 100755 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -4,7 +4,7 @@ # Creates core containers so My Apps works out of the box after ISO install # Runs after archipelago-load-images.service and archipelago-setup-tor.service # -# Based on scripts/deploy-to-target.sh (--live) container logic - do not diverge. +# Container logic mirrors the deploy path - do not diverge. # No set -e: each section continues even if one fails (idempotent, best-effort). # # Image versions: sourced from /opt/archipelago/image-versions.sh (single source of truth). diff --git a/scripts/fleet-fips-pair.sh b/scripts/fleet-fips-pair.sh deleted file mode 100755 index 78bce572..00000000 --- a/scripts/fleet-fips-pair.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/bin/bash -# LAN fast-path pairing for our 4 dev fleet nodes. -# -# ── Is this needed for every archipelago install? No. ──────────────── -# For nodes deployed anywhere in the world, FIPS-to-FIPS routing by -# npub works via the anchor peer network (fips.v0l.io ships by default -# in /etc/fips/fips.yaml on every install — that anchor bootstraps DHT -# routing for any npub the node has ever heard about). The peer's -# fips_npub is advertised in our federation invite codes (since v1.4), -# so accepting an invite is enough for `dial::peer_base_url(npub)` to -# reach the peer through the anchor mesh. -# -# ── Why this script exists ─────────────────────────────────────────── -# Our 4 fleet nodes are all on 192.168.1.0/24. Hopping through the -# fips.v0l.io anchor for intra-LAN traffic is wasteful when the peers -# are on the same wire. This script writes per-node fips.yaml with: -# 1. The public anchor (fips.v0l.io) so internet peers still route. -# 2. The other 3 fleet nodes as static LAN peers (UDP 2121 / TCP -# 8443) so LAN traffic stays on LAN. -# 3. `persistent: true` so the npub is stable across restarts — -# without this the daemon rolls a new keypair on every restart -# and any federation invite we advertised goes stale. -# -# Idempotent: re-running picks up any newly-added or removed nodes. -# -# For a production install on an unknown LAN, this script isn't the -# mechanism — the ISO install writes the anchor-only fips.yaml and -# identity comes from the archipelago seed; peer discovery is purely -# through the DHT + federation invites. -# -# Usage: -# scripts/fleet-fips-pair.sh # apply to all nodes -# scripts/fleet-fips-pair.sh --verify # just print the peer state - -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -. "$SCRIPT_DIR/lib/common.sh" - -# Fleet roster: " " -NODES=( - "116 enp0s25 npub1mxavs6scfgl056k6lm4mk73ddnrhjewg78zlyzfn2lmr0rfyrs5qhcr03g" - "198 enp2s0 npub13cy4lml94cj4rdu8runrr945z2muszuvr5tql8mr9m063d7xzpqqu3k8se" - "228 enp2s0 npub1a0xxcqce2tsv8ulwastep23jtf3h4wvvry8r8nklnl36jtrdnefqh5qn6h" - "253 enx9cbf0d0129f9 npub1dl0m0yfzfw6467c3z6q63s7ggzd77yg97j90ptfrheprxeypt3msj0mq4g" -) - -LAN_PREFIX="192.168.1" -UDP_PORT=2121 -TCP_PORT=8443 - -if [ "${1:-}" = "--verify" ]; then - for row in "${NODES[@]}"; do - read -r node _nic _npub <<< "$row" - echo "=== .$node ===" - ssh_cmd "$LAN_PREFIX.$node" "sudo fipsctl show peers 2>/dev/null | python3 -c 'import sys,json; d=json.load(sys.stdin); print(f\"{len(d[\"peers\"])} authenticated peers\"); [print(\" npub=\", p.get(\"npub\",\"?\"), \"alias=\", p.get(\"alias\",\"?\")) for p in d[\"peers\"]]' || echo ' fipsctl show peers failed'" - done - exit 0 -fi - -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -generate_yaml() { - # $1 = self node octet, $2 = self nic - local self_node="$1" - local self_nic="$2" - local out="$TMP_ROOT/fips.yaml.$self_node" - - cat > "$out" <> "$out" </dev/null 2>&1; then break; fi - sleep 0.5 - done - sudo systemctl is-active fips.service - ' -} - -for row in "${NODES[@]}"; do - read -r node nic _npub <<< "$row" - deploy_to "$node" "$nic" -done - -echo -log_info "Waiting 10s for peer handshakes to settle…" -sleep 10 - -echo -log_info "Post-pair peer state:" -for row in "${NODES[@]}"; do - read -r node _nic _npub <<< "$row" - count=$(ssh_cmd "$LAN_PREFIX.$node" "sudo fipsctl show peers 2>/dev/null | grep -c '\"npub\"' || echo 0") - log_info " .$node: $count authenticated peers" -done diff --git a/scripts/fleet-fips-unpair.sh b/scripts/fleet-fips-unpair.sh deleted file mode 100755 index e56c0274..00000000 --- a/scripts/fleet-fips-unpair.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -# Strip the LAN fast-path peers from all 4 fleet nodes' fips.yaml, -# leaving only the public anchor (fips.v0l.io). Restart fips.service -# on each node. -# -# Purpose: verify that the general-case deployment (nodes anywhere in -# the world, no LAN between them) actually works — i.e. that two -# paired archipelago peers can reach each other purely through the -# FIPS DHT bootstrapped from the anchor. -# -# After running this, test with: -# scripts/fleet-fips-pair.sh --verify (peer state per node) -# for ip in 116 198 228 253; do -# ssh archipelago@192.168.1.$ip "dig @127.0.0.1 -p 5354 +short \ -# .fips AAAA" -# done -# -# To restore the LAN fast-path: re-run scripts/fleet-fips-pair.sh. -# -# Usage: scripts/fleet-fips-unpair.sh - -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -. "$SCRIPT_DIR/lib/common.sh" - -# Roster — only need NIC names to preserve them in the yaml. -NODES=( - "116 enp0s25" - "198 enp2s0" - "228 enp2s0" - "253 enx9cbf0d0129f9" -) - -TMP_ROOT=$(mktemp -d) -trap 'rm -rf "$TMP_ROOT"' EXIT - -for row in "${NODES[@]}"; do - read -r node nic <<< "$row" - out="$TMP_ROOT/fips.yaml.$node" - cat > "$out" </dev/null 2>&1; then break; fi - sleep 0.5 - done - sudo systemctl is-active fips.service - ' -done - -echo -log_info "Waiting 20s for anchor handshake + DHT propagation…" -sleep 20 - -echo -log_info "Post-unpair state (should show only fips.v0l.io as an authenticated peer):" -for row in "${NODES[@]}"; do - read -r node _nic <<< "$row" - ip="192.168.1.$node" - count=$(ssh_cmd "$ip" "sudo fipsctl show peers 2>/dev/null | grep -c '\"npub\"' || echo 0") - log_info " .$node: $count authenticated peers" -done - -echo -log_info "DHT resolution test — each node resolves the other 3 by npub:" -declare -A NPUBS=( - [116]="npub1mxavs6scfgl056k6lm4mk73ddnrhjewg78zlyzfn2lmr0rfyrs5qhcr03g" - [198]="npub13cy4lml94cj4rdu8runrr945z2muszuvr5tql8mr9m063d7xzpqqu3k8se" - [228]="npub1a0xxcqce2tsv8ulwastep23jtf3h4wvvry8r8nklnl36jtrdnefqh5qn6h" - [253]="npub1dl0m0yfzfw6467c3z6q63s7ggzd77yg97j90ptfrheprxeypt3msj0mq4g" -) -for row in "${NODES[@]}"; do - read -r self_node _ <<< "$row" - ip="192.168.1.$self_node" - echo ".${self_node}:" - for other in 116 198 228 253; do - [ "$other" = "$self_node" ] && continue - r=$(ssh_cmd "$ip" "dig @127.0.0.1 -p 5354 +short +time=3 +tries=1 ${NPUBS[$other]}.fips AAAA" 2>&1) - if [ -z "$r" ]; then - echo " .${other} → unresolved (DHT route not found)" - else - echo " .${other} → $r" - fi - done -done diff --git a/scripts/node-profile.sh b/scripts/node-profile.sh deleted file mode 100755 index c22de203..00000000 --- a/scripts/node-profile.sh +++ /dev/null @@ -1,252 +0,0 @@ -#!/bin/bash -# node-profile.sh — CPU/memory/container profiling across all Archipelago nodes -# -# Usage: -# ./scripts/node-profile.sh # All reachable nodes -# ./scripts/node-profile.sh 192.168.1.228 # Single node -# ./scripts/node-profile.sh --watch # Repeat every 30s -# -# Requires: SSH key at ~/.ssh/archipelago-deploy (or ARCHIPELAGO_SSH_KEY) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/lib/common.sh" -source "$SCRIPT_DIR/deploy-config-defaults.sh" -[ -f "$SCRIPT_DIR/deploy-config.sh" ] && source "$SCRIPT_DIR/deploy-config.sh" - -ALL_NODES=( - "$DEFAULT_PRIMARY" - "$DEFAULT_SECONDARY" - "$TAILSCALE_ARCH1" - "$TAILSCALE_ARCH2" - "$TAILSCALE_ARCH3" -) - -NODE_LABELS=( - "primary (.228)" - "secondary (.198)" - "tailscale-1" - "tailscale-2" - "tailscale-3" -) - -WATCH_MODE=false -WATCH_INTERVAL=30 -TARGET_NODES=() - -# ── Parse args ───────────────────────────────────────────────────────── - -while [[ $# -gt 0 ]]; do - case "$1" in - --watch) - WATCH_MODE=true - shift - ;; - --interval) - WATCH_INTERVAL="$2" - shift 2 - ;; - *) - TARGET_NODES+=("$1") - shift - ;; - esac -done - -# If specific nodes given, use those; otherwise use all -if [ ${#TARGET_NODES[@]} -eq 0 ]; then - TARGET_NODES=("${ALL_NODES[@]}") -fi - -# ── Remote profiling command ─────────────────────────────────────────── - -PROFILE_CMD=' -hostname_val=$(hostname 2>/dev/null || echo "unknown") -uptime_val=$(uptime -p 2>/dev/null || uptime | sed "s/.*up/up/;s/,.*//") - -# CPU info -cpu_cores=$(nproc 2>/dev/null || echo "?") -load_avg=$(cat /proc/loadavg 2>/dev/null | awk "{print \$1, \$2, \$3}") - -# Memory -mem_info=$(free -h 2>/dev/null | awk "/^Mem:/{printf \"%s / %s (%s free)\", \$3, \$2, \$4}") -swap_info=$(free -h 2>/dev/null | awk "/^Swap:/{if(\$2 != \"0B\" && \$2 != \"0\") printf \"%s / %s\", \$3, \$2; else print \"none\"}") - -# Disk -disk_info=$(df -h / 2>/dev/null | awk "NR==2{printf \"%s / %s (%s)\", \$3, \$2, \$5}") - -# CPU temperature (if available) -temp="n/a" -if [ -f /sys/class/thermal/thermal_zone0/temp ]; then - raw=$(cat /sys/class/thermal/thermal_zone0/temp) - temp="$((raw / 1000))°C" -fi - -echo "HEADER|${hostname_val}|${uptime_val}|${cpu_cores} cores|load ${load_avg}|${temp}" -echo "MEM|${mem_info}" -echo "SWAP|${swap_info}" -echo "DISK|${disk_info}" - -# Top 10 processes by CPU -echo "PROCS_START" -ps aux --sort=-%cpu 2>/dev/null | head -11 | awk "NR>1{printf \"%-6s %-5s %-5s %s\n\", \$2, \$3, \$4, \$11}" 2>/dev/null -echo "PROCS_END" - -# Container status -echo "CONTAINERS_START" -if command -v podman >/dev/null 2>&1; then - podman ps -a --format "{{.Names}}|{{.Status}}|{{.Size}}" 2>/dev/null || \ - podman ps -a --format "{{.Names}}|{{.Status}}" 2>/dev/null || \ - echo "podman error" -elif command -v docker >/dev/null 2>&1; then - docker ps -a --format "{{.Names}}|{{.Status}}" 2>/dev/null || echo "docker error" -else - echo "no container runtime" -fi -echo "CONTAINERS_END" -' - -# ── Formatting ───────────────────────────────────────────────────────── - -BOLD="\033[1m" -DIM="\033[2m" -GREEN="\033[0;32m" -YELLOW="\033[0;33m" -RED="\033[0;31m" -CYAN="\033[0;36m" -RESET="\033[0m" - -SEP="━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - -print_node_report() { - local ip="$1" - local label="$2" - local output="$3" - - echo -e "\n${BOLD}${CYAN}${SEP}${RESET}" - echo -e "${BOLD}${CYAN} ${label} ${DIM}(${ip})${RESET}" - echo -e "${BOLD}${CYAN}${SEP}${RESET}" - - # Parse HEADER line - local header - header=$(echo "$output" | grep "^HEADER|" | head -1) - if [ -n "$header" ]; then - IFS='|' read -r _ hostname uptime cores load temp <<< "$header" - echo -e " ${BOLD}Host:${RESET} ${hostname} ${DIM}${uptime}${RESET}" - echo -e " ${BOLD}CPU:${RESET} ${cores} ${load} ${temp}" - fi - - # Memory - local mem - mem=$(echo "$output" | grep "^MEM|" | cut -d'|' -f2) - [ -n "$mem" ] && echo -e " ${BOLD}Mem:${RESET} ${mem}" - - local swap - swap=$(echo "$output" | grep "^SWAP|" | cut -d'|' -f2) - [ -n "$swap" ] && echo -e " ${BOLD}Swap:${RESET} ${swap}" - - local disk - disk=$(echo "$output" | grep "^DISK|" | cut -d'|' -f2) - [ -n "$disk" ] && echo -e " ${BOLD}Disk:${RESET} ${disk}" - - # Top processes - echo "" - echo -e " ${BOLD}Top processes by CPU:${RESET}" - echo -e " ${DIM}PID CPU% MEM% Command${RESET}" - local procs - procs=$(echo "$output" | sed -n '/^PROCS_START$/,/^PROCS_END$/p' | grep -v "^PROCS_") - if [ -n "$procs" ]; then - while IFS= read -r line; do - local cpu_pct - cpu_pct=$(echo "$line" | awk '{print $2}' | tr -d '.') - if [ "${cpu_pct:-0}" -gt 500 ] 2>/dev/null; then - echo -e " ${RED}${line}${RESET}" - elif [ "${cpu_pct:-0}" -gt 100 ] 2>/dev/null; then - echo -e " ${YELLOW}${line}${RESET}" - else - echo -e " ${line}" - fi - done <<< "$procs" - else - echo -e " ${DIM}(no process data)${RESET}" - fi - - # Containers - echo "" - echo -e " ${BOLD}Containers:${RESET}" - local containers - containers=$(echo "$output" | sed -n '/^CONTAINERS_START$/,/^CONTAINERS_END$/p' | grep -v "^CONTAINERS_") - if [ -n "$containers" ] && [ "$containers" != "no container runtime" ] && [ "$containers" != "podman error" ]; then - while IFS='|' read -r name status size; do - local icon - if echo "$status" | grep -qi "up"; then - icon="${GREEN}●${RESET}" - else - icon="${RED}○${RESET}" - fi - echo -e " ${icon} ${BOLD}${name}${RESET} ${DIM}${status}${RESET}" - done <<< "$containers" - else - echo -e " ${DIM}${containers:-none}${RESET}" - fi -} - -# ── Main profiling loop ─────────────────────────────────────────────── - -profile_all() { - echo -e "\n${BOLD}Archipelago Node Profile${RESET} ${DIM}$(date '+%Y-%m-%d %H:%M:%S')${RESET}" - - local tmpdir - tmpdir=$(mktemp -d) - - # Probe all nodes in parallel - local pids=() - for i in "${!TARGET_NODES[@]}"; do - local ip="${TARGET_NODES[$i]}" - local label="${NODE_LABELS[$i]:-$ip}" - ( - result=$(ssh_cmd "$ip" "$PROFILE_CMD" 2>/dev/null) && \ - echo "$result" > "$tmpdir/$i.out" || \ - echo "UNREACHABLE" > "$tmpdir/$i.out" - ) & - pids+=($!) - done - - # Wait for all probes - for pid in "${pids[@]}"; do - wait "$pid" 2>/dev/null || true - done - - # Print reports - local reachable=0 unreachable=0 - for i in "${!TARGET_NODES[@]}"; do - local ip="${TARGET_NODES[$i]}" - local label="${NODE_LABELS[$i]:-$ip}" - local outfile="$tmpdir/$i.out" - - if [ -f "$outfile" ] && [ "$(cat "$outfile")" != "UNREACHABLE" ]; then - print_node_report "$ip" "$label" "$(cat "$outfile")" - reachable=$((reachable + 1)) - else - echo -e "\n${DIM}${SEP}${RESET}" - echo -e "${RED} ${label} (${ip}) — unreachable${RESET}" - echo -e "${DIM}${SEP}${RESET}" - unreachable=$((unreachable + 1)) - fi - done - - echo -e "\n${DIM}${reachable} reachable, ${unreachable} unreachable${RESET}\n" - rm -rf "$tmpdir" -} - -if $WATCH_MODE; then - while true; do - clear - profile_all - echo -e "${DIM}Refreshing every ${WATCH_INTERVAL}s — Ctrl+C to stop${RESET}" - sleep "$WATCH_INTERVAL" - done -else - profile_all -fi diff --git a/scripts/self-update.sh b/scripts/self-update.sh index fb2ed45f..dc243ca9 100755 --- a/scripts/self-update.sh +++ b/scripts/self-update.sh @@ -217,7 +217,7 @@ ok "Backend installed" # Non-fatal: archipelago falls back to its dev venv path if the packaged # binaries aren't present, so a missing/failed build here degrades mesh # Reticulum support rather than breaking the update. This mirrors -# deploy-to-target.sh's existing manual-deploy step, which until now was the +# the existing manual-deploy step, which until now was the # only path that ever installed these — a node that only ever received OTA # self-updates had neither binary. if [ -f "$REPO_DIR/reticulum-daemon/build.sh" ]; then @@ -329,7 +329,7 @@ UI_REBUILD_LIST="" # /opt/archipelago/docker/, and nothing was ever updating that directory. # So source edits to those two trees reached nodes through no path at all: # their nginx kept listening on 0.0.0.0 and served the Guardian and FIPS -# screens unauthenticated on every interface (found by scanning archi-dev-box +# screens unauthenticated on every interface (found by scanning a test node # from outside, 2026-08-05 — the in-node audit could not see them). for ui in bitcoin-ui lnd-ui electrs-ui fips-ui fedimint-ui; do src="$REPO_DIR/docker/$ui" diff --git a/scripts/setup-aiui-server.sh b/scripts/setup-aiui-server.sh deleted file mode 100755 index cbf3fc4a..00000000 --- a/scripts/setup-aiui-server.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/bin/bash -# -# Setup AIUI + Claude API proxy + FileBrowser on any Archipelago server -# -# Usage: -# ./scripts/setup-aiui-server.sh -# ./scripts/setup-aiui-server.sh archipelago@192.168.1.198 -# ./scripts/setup-aiui-server.sh archipelago@192.168.1.228 -# -# What it does: -# 1. Deploys AIUI files (from local build) -# 2. Configures nginx Claude API proxy (direct to Anthropic with API key) -# 3. Fixes FileBrowser container (removes read-only root if needed) -# 4. Reloads nginx -# -# Prerequisites: -# - AIUI must be built locally first: cd AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build -# - SSH key access to target server - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}" -SSH_OPTS="-o StrictHostKeyChecking=no -i $SSH_KEY" - -# Anthropic API key used by the AIUI Claude chat proxy. Keep this in the -# caller's environment or scripts/deploy-config.sh; never commit live keys. -ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" - -TARGET_HOST="$1" -if [ -z "$TARGET_HOST" ]; then - echo "Usage: $0 " - echo " e.g. $0 archipelago@192.168.1.198" - exit 1 -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo "ERROR: ANTHROPIC_API_KEY must be set in the environment." - echo "Example: ANTHROPIC_API_KEY= $0 $TARGET_HOST" - exit 1 -fi - -AIUI_DIST="$PROJECT_DIR/../AIUI/packages/app/dist" -if [ ! -f "$AIUI_DIST/index.html" ]; then - echo "ERROR: AIUI build not found at $AIUI_DIST" - echo "Build it first: cd ../AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build" - exit 1 -fi - -timestamp() { echo "[$(date +%H:%M:%S)]"; } - -echo "╔════════════════════════════════════════════════════════════╗" -echo "║ Archipelago AIUI + Claude API Setup ║" -echo "║ Target: $TARGET_HOST" -echo "╚════════════════════════════════════════════════════════════╝" - -# --- Step 1: Deploy AIUI files --- -echo "" -echo "$(timestamp) 📦 Deploying AIUI files..." - -# Check if rsync is available on remote -if ssh $SSH_OPTS "$TARGET_HOST" "which rsync" &>/dev/null; then - rsync -avz --delete -e "ssh $SSH_OPTS" "$AIUI_DIST/" "$TARGET_HOST:/opt/archipelago/web-ui/aiui/" 2>&1 | tail -3 -else - echo " rsync not available, using tar+scp..." - TMPTAR=$(mktemp /tmp/aiui-dist-XXXXX.tar.gz) - (cd "$AIUI_DIST" && tar czf "$TMPTAR" .) - scp $SSH_OPTS "$TMPTAR" "$TARGET_HOST:/tmp/aiui-dist.tar.gz" - ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /opt/archipelago/web-ui/aiui && cd /opt/archipelago/web-ui/aiui && sudo tar xzf /tmp/aiui-dist.tar.gz --overwrite" - rm -f "$TMPTAR" -fi -echo " AIUI deployed." - -# --- Step 2: Configure nginx Claude API proxy --- -echo "" -echo "$(timestamp) 🔧 Configuring nginx Claude API proxy..." - -# Create a Python script to patch nginx config -cat << 'PYSCRIPT' > /tmp/patch-nginx-claude.py -import sys -import re - -API_KEY = sys.argv[1] - -with open("/etc/nginx/sites-available/archipelago") as f: - content = f.read() - -# The new Claude API proxy block -new_block = '''location /aiui/api/claude/ { - if ($cookie_session = "") { - return 401 '{"error":"Unauthorized"}'; - } - proxy_pass https://api.anthropic.com/; - proxy_http_version 1.1; - proxy_set_header Host api.anthropic.com; - proxy_set_header x-api-key "''' + API_KEY + '''"; - proxy_set_header anthropic-version "2023-06-01"; - proxy_set_header anthropic-dangerous-direct-browser-access "true"; - proxy_ssl_server_name on; - proxy_set_header X-Real-IP $remote_addr; - proxy_buffering off; - proxy_cache off; - proxy_connect_timeout 120s; - proxy_read_timeout 300s; - proxy_send_timeout 120s; - }''' - -# Replace existing Claude API proxy blocks (handles both old proxy and direct patterns) -pattern = r'location /aiui/api/claude/ \{[^}]*(?:\{[^}]*\}[^}]*)*\}' -content = re.sub(pattern, new_block, content) - -with open("/etc/nginx/sites-available/archipelago", "w") as f: - f.write(content) - -# Verify -count = content.count("api.anthropic.com") -print(f" Patched {count // 2} Claude API proxy blocks (HTTP + HTTPS)") -PYSCRIPT - -scp $SSH_OPTS /tmp/patch-nginx-claude.py "$TARGET_HOST:/tmp/patch-nginx-claude.py" -ssh $SSH_OPTS "$TARGET_HOST" "sudo python3 /tmp/patch-nginx-claude.py '$ANTHROPIC_API_KEY'" - -# Test and reload nginx -echo " Testing nginx config..." -ssh $SSH_OPTS "$TARGET_HOST" "sudo nginx -t 2>&1 && sudo systemctl reload nginx && echo ' Nginx reloaded OK'" || { - echo " ERROR: nginx config test failed!" - exit 1 -} - -# --- Step 3: Fix FileBrowser container --- -echo "" -echo "$(timestamp) 📁 Checking FileBrowser..." - -FB_STATUS=$(ssh $SSH_OPTS "$TARGET_HOST" "podman inspect filebrowser 2>/dev/null | grep -oP '\"ReadonlyRootfs\":\s*\K\w+'" 2>/dev/null || echo "not_found") - -if [ "$FB_STATUS" = "true" ]; then - echo " FileBrowser has read-only root — recreating..." - ssh $SSH_OPTS "$TARGET_HOST" " - podman stop filebrowser 2>/dev/null - podman rm filebrowser 2>/dev/null - sudo mkdir -p /var/lib/archipelago/filebrowser - podman run -d --name filebrowser --restart=always \ - -p 8083:80 \ - -v /var/lib/archipelago/filebrowser:/srv \ - filebrowser/filebrowser:v2.27.0 - " 2>&1 | tail -2 - echo " FileBrowser recreated." -elif [ "$FB_STATUS" = "not_found" ]; then - echo " FileBrowser not found — creating..." - ssh $SSH_OPTS "$TARGET_HOST" " - sudo mkdir -p /var/lib/archipelago/filebrowser - podman run -d --name filebrowser --restart=always \ - -p 8083:80 \ - -v /var/lib/archipelago/filebrowser:/srv \ - filebrowser/filebrowser:v2.27.0 - " 2>&1 | tail -2 - echo " FileBrowser created." -else - echo " FileBrowser OK (ReadonlyRootfs: $FB_STATUS)" -fi - -# --- Step 4: Verify --- -echo "" -echo "$(timestamp) ✅ Verification..." -ssh $SSH_OPTS "$TARGET_HOST" " - echo \" AIUI index: \$(ls -la /opt/archipelago/web-ui/aiui/index.html 2>/dev/null | awk '{print \$6,\$7,\$8}')\" - echo \" FileBrowser: \$(podman ps --format '{{.Names}} {{.Status}}' | grep filebrowser)\" - echo \" Nginx: \$(systemctl is-active nginx)\" - echo \" Backend: \$(systemctl is-active archipelago)\" - echo \" Claude API test: \$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost/aiui/api/claude/v1/messages -H 'Content-Type: application/json' -H 'Cookie: session=test' -d '{\"model\":\"claude-sonnet-4-20250514\",\"max_tokens\":5,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}')\" -" - -echo "" -echo "$(timestamp) Done! Server configured." -echo " Access: http://$(echo $TARGET_HOST | cut -d@ -f2)" diff --git a/scripts/setup-https-dev.sh b/scripts/setup-https-dev.sh deleted file mode 100644 index 47630873..00000000 --- a/scripts/setup-https-dev.sh +++ /dev/null @@ -1,280 +0,0 @@ -#!/bin/bash -# -# Set up HTTPS on Archipelago dev server for PWA installability. -# Browsers require HTTPS (or localhost) to install PWAs. -# Generates a self-signed certificate and configures nginx. -# -# Run on the target server: sudo ./setup-https-dev.sh -# Or via deploy: the deploy script runs this automatically. -# - -set -e - -SSL_DIR="/etc/archipelago/ssl" -NGINX_CFG="/etc/nginx/sites-available/archipelago" -CERT="$SSL_DIR/archipelago.crt" -KEY="$SSL_DIR/archipelago.key" - -# Create SSL directory -mkdir -p "$SSL_DIR" -chmod 755 "$SSL_DIR" - -# Generate self-signed cert if missing (valid 365 days) -# SAN includes common dev IPs so cert works when accessing via IP -# Build dynamic SAN with all node IPs (LAN + Tailscale + loopback) -SAN_IPS="DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" -# Add all IPv4 addresses on this machine (LAN, Tailscale, etc.) -for ip in $(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.' | grep -v '^127\.'); do - SAN_IPS="$SAN_IPS,IP:$ip" -done -# Always include common LAN IPs as fallback -for ip in 192.168.1.228 192.168.1.198 10.0.0.1; do - echo "$SAN_IPS" | grep -q "$ip" || SAN_IPS="$SAN_IPS,IP:$ip" -done - -# Regenerate cert if missing OR if current cert doesn't include this node's primary IP -REGEN=false -if [ ! -f "$CERT" ] || [ ! -f "$KEY" ]; then - REGEN=true -else - # Check if cert has this node's primary IP - MY_IP=$(hostname -I 2>/dev/null | awk '{print $1}') - if [ -n "$MY_IP" ] && ! openssl x509 -in "$CERT" -noout -text 2>/dev/null | grep -q "$MY_IP"; then - echo " Certificate missing this node's IP ($MY_IP) — regenerating..." - REGEN=true - fi -fi - -if [ "$REGEN" = true ]; then - echo "Generating self-signed certificate for PWA (HTTPS)..." - echo " SAN: $SAN_IPS" - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout "$KEY" \ - -out "$CERT" \ - -subj "/CN=archipelago.local/O=Archipelago/C=US" \ - -addext "subjectAltName=$SAN_IPS" - chmod 644 "$CERT" - chmod 600 "$KEY" - echo " Certificate created at $CERT" -fi - -# PWA snippet for manifest + service worker headers (required for Android install) -NGINX_SNIPPETS="/etc/nginx/snippets" -PWA_SNIPPET="$NGINX_SNIPPETS/archipelago-pwa.conf" -mkdir -p "$NGINX_SNIPPETS" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if [ -f "$SCRIPT_DIR/nginx-pwa-snippet.conf" ]; then - cp "$SCRIPT_DIR/nginx-pwa-snippet.conf" "$PWA_SNIPPET" - echo " PWA nginx snippet installed at $PWA_SNIPPET" -fi - -# Add PWA snippet include to existing HTTPS block if missing -if grep -q "listen 443 ssl" "$NGINX_CFG" 2>/dev/null && [ -f "$PWA_SNIPPET" ]; then - if ! grep -q "archipelago-pwa" "$NGINX_CFG" 2>/dev/null; then - echo " Adding PWA snippet include to HTTPS block..." - # Insert include after "index index.html;" within the HTTPS server block (listen 443 to next }) - sed -i '/listen 443 ssl/,/^}$/{ - /index index.html;/a\ - include snippets/archipelago-pwa.conf; - }' "$NGINX_CFG" 2>/dev/null || true - fi -fi - -# Install app proxies snippet (mempool, fedimint, lnd, etc.) - fixes apps not opening over HTTPS (mixed content) -APPS_SNIPPET="$NGINX_SNIPPETS/archipelago-https-app-proxies.conf" -if [ -f "$SCRIPT_DIR/nginx-https-app-proxies.conf" ]; then - cp "$SCRIPT_DIR/nginx-https-app-proxies.conf" "$APPS_SNIPPET" - echo " HTTPS app proxies snippet installed at $APPS_SNIPPET" - # Add include to HTTPS block if missing - if grep -q "listen 443 ssl" "$NGINX_CFG" 2>/dev/null && ! grep -q "archipelago-https-app-proxies" "$NGINX_CFG" 2>/dev/null; then - echo " Adding app proxies include to HTTPS block..." - sed -i '/listen 443 ssl/,/^}$/{ - /location \/ws {/i\ - include snippets/archipelago-https-app-proxies.conf; - }' "$NGINX_CFG" 2>/dev/null || true - fi -fi - -# Check if HTTPS is already configured -if grep -q "listen 443 ssl" "$NGINX_CFG" 2>/dev/null; then - echo "HTTPS already configured in nginx." - nginx -t 2>/dev/null && systemctl reload nginx - MY_IP=$(hostname -I 2>/dev/null | awk '{print $1}') - echo "" - echo "PWA: Use https://${MY_IP:-192.168.1.228} (not http) - accept cert once, then Install app." - exit 0 -fi - -# Add HTTPS server block (duplicate of HTTP block with SSL) -# PWA requires HTTPS for install on Android -HTTPS_BLOCK=' -# HTTPS - required for PWA install (Add to Home Screen) from dev servers -server { - listen 443 ssl; - server_name _; - - ssl_certificate '"$CERT"'; - ssl_certificate_key '"$KEY"'; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; - - root /opt/archipelago/web-ui; - index index.html; - include snippets/archipelago-pwa.conf; - - location / { - try_files $uri $uri/ /index.html; - } - - location /archipelago/ { - proxy_pass http://127.0.0.1:5678; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - - location /rpc/ { - proxy_pass http://127.0.0.1:5678; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_connect_timeout 600s; - proxy_send_timeout 600s; - proxy_read_timeout 600s; - } - - location /app/nextcloud/ { - proxy_pass http://127.0.0.1:8085/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - location /app/vaultwarden/ { - proxy_pass http://127.0.0.1:8082/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - } - location /app/immich/ { - proxy_pass http://127.0.0.1:2283/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - location /app/penpot/ { - proxy_pass http://127.0.0.1:9001/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - location /app/btcpay/ { - proxy_pass http://127.0.0.1:23000/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - } - location /app/homeassistant/ { - proxy_pass http://127.0.0.1:8123/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_read_timeout 86400s; - proxy_send_timeout 86400s; - } - location /app/mempool/ { - proxy_pass http://127.0.0.1:4080/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - location /app/fedimint/ { - proxy_pass http://127.0.0.1:8175/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - location /app/lnd/ { - proxy_pass http://127.0.0.1:18083/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - location /app/bitcoin-ui/ { - proxy_pass http://127.0.0.1:8334/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_hide_header X-Frame-Options; - proxy_hide_header Content-Security-Policy; - } - - location /ws { - proxy_pass http://127.0.0.1:5678; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_read_timeout 86400s; - } -} -' - -# Append HTTPS block to nginx config -echo "$HTTPS_BLOCK" >> "$NGINX_CFG" -echo "Added HTTPS (port 443) to nginx config." - -# Test and reload -nginx -t && systemctl reload nginx -echo "" -MY_IP=$(hostname -I 2>/dev/null | awk '{print $1}') -echo "HTTPS enabled. PWA install: https://${MY_IP:-192.168.1.228} (accept the certificate warning once, then Install app)." diff --git a/scripts/setup-target-dev.sh b/scripts/setup-target-dev.sh deleted file mode 100755 index 251a53e1..00000000 --- a/scripts/setup-target-dev.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/bin/bash -# -# Setup development environment on Archipelago target machine -# -# Run this ON the HP ProDesk via SSH: -# curl -sSL https://raw.githubusercontent.com/.../setup-target-dev.sh | bash -# Or copy and run locally: -# scp scripts/setup-target-dev.sh archipelago@192.168.1.228:~/ -# ssh archipelago@192.168.1.228 'bash ~/setup-target-dev.sh' -# - -set -e - -echo "╔════════════════════════════════════════════════════════════════╗" -echo "║ Setting up Archipelago Development Environment ║" -echo "╚════════════════════════════════════════════════════════════════╝" -echo "" - -# Update packages -echo "📦 Updating packages..." -sudo apt update - -# Install Node.js (for Vue.js frontend) -echo "" -echo "📦 Installing Node.js..." -if ! command -v node &> /dev/null; then - curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - - sudo apt install -y nodejs -else - echo " Node.js already installed: $(node --version)" -fi - -# Install Rust (for backend) -echo "" -echo "📦 Installing Rust..." -if ! command -v cargo &> /dev/null; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source ~/.cargo/env -else - echo " Rust already installed: $(rustc --version)" -fi - -# Install build tools -echo "" -echo "📦 Installing build tools..." -sudo apt install -y \ - build-essential \ - pkg-config \ - libssl-dev \ - git - -# Create development directory -echo "" -echo "📁 Creating development directory..." -mkdir -p ~/archy - -# Fix XDG_RUNTIME_DIR for rootless Podman (add to bashrc) -if ! grep -q "XDG_RUNTIME_DIR" ~/.bashrc; then - echo "" - echo "🔧 Fixing Podman rootless setup..." - cat >> ~/.bashrc << 'EOF' - -# Fix for rootless Podman -if [ -z "$XDG_RUNTIME_DIR" ]; then - export XDG_RUNTIME_DIR=/run/user/$(id -u) - if [ ! -d "$XDG_RUNTIME_DIR" ]; then - sudo mkdir -p "$XDG_RUNTIME_DIR" - sudo chown $(whoami):$(whoami) "$XDG_RUNTIME_DIR" - sudo chmod 700 "$XDG_RUNTIME_DIR" - fi -fi -EOF -fi - -# Enable user lingering for Podman -sudo loginctl enable-linger archipelago 2>/dev/null || true - -echo "" -echo "╔════════════════════════════════════════════════════════════════╗" -echo "║ ✅ Development environment ready! ║" -echo "╚════════════════════════════════════════════════════════════════╝" -echo "" -echo "Installed:" -echo " • Node.js: $(node --version 2>/dev/null || echo 'not found')" -echo " • npm: $(npm --version 2>/dev/null || echo 'not found')" -echo " • Rust: $(rustc --version 2>/dev/null || echo 'not found')" -echo " • Cargo: $(cargo --version 2>/dev/null || echo 'not found')" -echo "" -echo "Next steps:" -echo " 1. Log out and back in (or run: source ~/.bashrc)" -echo " 2. From your Mac, run: ./scripts/deploy-to-target.sh" -echo " 3. To start Vue.js dev server: cd ~/archy/neode-ui && npm run dev -- --host 0.0.0.0" -echo ""