fix/mesh-send-content-inline-federation-fallback
313
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ce88de87f | Merge remote-tracking branch 'gitea-ai/main' into gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf | ||
|
|
cfa6c6cb0d |
fix(lnd): hold LND's lifecycle lock across a rotation; mock the rotation RPCs
Demo images / Build & push demo images (push) Successful in 3m55s
Reviewing the rotation against what this dev node actually did to LND today —
25 restarts, most of them automatic — surfaced a race the code did not defend
against. Between "stop LND" and "start LND" the rotation owns a stopped
container whose credential material is being deleted, and two background actors
step in there unasked: the health monitor restarts any container it finds
stopped, and the reconciler starts one whose unit is enabled.
Either brings LND back up mid-deletion. LND re-mints macaroons.db on unlock, so
the deletion loop would race a live process writing that file, or "succeed"
against material that had already been regenerated — and the operator would be
told they had rotated while the old root key was still in service. That is the
one outcome this feature exists to make impossible.
It now holds `app_ops::op_lock("lnd")` for the whole rotation. That is the lock
both actors already consult (`lifecycle_op_in_flight`; the health monitor
reaches it through `lifecycle_op_covers_container`), and it additionally
serialises against the package.start/stop/restart workers, so "Restart" on
Lightning mid-rotation queues instead of interleaving. A rotation requested
while one of those is in flight fails fast with a short explanation rather than
waiting silently behind an operation that may itself take minutes.
Deliberately NOT the `user-stopped` marker `recreate_wallet_destructively` uses
for its own window. That marker is a file on disk: a rotation that died between
marking and clearing would leave Lightning suppressed permanently, fixable only
by finding and editing JSON on the node. A lock guard releases when it drops, on
every path including a panic.
Also mocks the three RPCs in mock-backend.js, so the Settings section can be
driven end-to-end without a node — the dev preview otherwise shows only a load
error. The mock advances one step per poll rather than on a timer, which is
deterministic and makes every intermediate state observable.
Verified: cargo check + fmt clean, 6/6 rotation tests, 12/12 component tests,
mock-rpc-parity unchanged (its 2 failures are the in-flight Reticulum panel, not
this), and the three RPCs driven against the live mock through the full arc —
idle → started → 7 steps → ok with the channel count preserved, plus both
password-rejection paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b1523d3e42 |
merge: bring the open-source readiness work onto the phase-13 branch
Merges gitea-ai/main (65 commits) into the phase-13 branch (419) so one
build carries both lines — the AIUI/assistant/container work and the
open-source readiness work (licensing, the marketplace DID signature layer,
the registry domain migration, the secrets and infrastructure scrub).
Every Rust file auto-merged. The container fixes from this branch and main's
registry-domain migration and node-name genericisation coexist without
manual intervention.
Conflict resolution — all of them were modify/delete, and all were resolved
in main's favour deliberately:
`.planning/**`, `scripts/deploy-to-target.sh` and `scripts/setup-aiui-server.sh`
were deleted by main's `6ba05996` ("security: remove all infrastructure and
internal process material from the repo") and added to .gitignore there.
Keeping this branch's copies would have re-committed internal process and
infrastructure material into a repo being prepared for publication, silently
undoing that cleanup. Resolved with `git rm --cached`, so every file remains
on disk locally and in this branch's history — it is untracked, not lost.
The remaining .planning files this branch added after the merge base were
untracked the same way, so the result is consistent rather than half-tracked.
Container suite 221/221 on the merged tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7ed28c3c02 |
chore(open-source): Phase 2 — drop generated, stale and inert tracked artifacts
Open-source readiness plan, Phase 2. Removed (~17 MB, 12.8k lines):
- `.githooks/pre-push` — the hook that re-committed the 27 MB companion APK on
every push, which the plan names as the root cause of the 5.5 GB history.
Verified inert first: `core.hooksPath` is unset, so it only ever ran for a dev
who opted in by hand.
- `neode-ui/dev-dist/` — generated vite-plugin-pwa output (a Workbox bundle),
tracked and not ignored. Added to .gitignore so it cannot come back.
- `Android/archipelago-0.3.0-debug.apk.zip` — 16 MB, stale, zero references.
- `RELEASE-NOTES-v1.0.0.md` — superseded by CHANGELOG.md.
- `docs/container-architecture.html` (311 KB) and the two generated archive
HTML artefacts, whose rows are removed from the archive index in the same
commit so the table doesn't point at deleted files.
THREE items the plan lists were verified and deliberately NOT deleted — the
plan is wrong about each, and following it literally would have lost content or
broken a build:
- `neode-ui/docs/GAMEPAD-NAV-MAP.md` is called "a duplicate of
docs/GAMEPAD-NAV.md". It is 660 lines against that file's 159 — four times the
content, not a copy. Needs a human read to decide what to keep.
- `Android/app/debug.keystore` is called "standard practice" to remove. This
repo deliberately commits it: `build.gradle.kts` sets
`storeFile = file("debug.keystore")` and `Android/.gitignore` carries an
explicit `!/app/debug.keystore`, with a comment explaining it exists so every
machine produces the same debug signing identity. Deleting it breaks Android
debug builds.
- The three "move to release assets" binaries are not a pure git operation —
two have live consumers. Detailed in the next message rather than guessed at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d15cd58d7f |
feat(lnd): rotate Lightning macaroons from the dashboard, and stop stranding BTCPay
Demo images / Build & push demo images (push) Successful in 3m34s
Rotating LND's macaroons was an SSH-only script, which in practice meant it did not happen — while a macaroon is a bearer token with no revocation and no expiry, so anything that ever read one keeps the ability to spend until they are replaced. Settings → Lightning credentials now does it behind the node password, shows a step checklist, and refuses to report success unless it has confirmed the node identity and channel census are unchanged. Three findings from performing a real rotation on a dev node, each fixed here: 1. BTCPay was left holding a dead credential, silently. Its connection string carries the macaroon INLINE (LND's datadir is owned by its container subuid, so btcpay cannot bind-mount the file), and the daemon only regenerates that secret when LND's TLS cert thumbprint changes — which macaroon rotation does not touch. Result: btcpay up, LND up, both healthy, every Lightning payment failing, nothing anywhere saying why. 2. Rewriting the secret is not enough to fix it. `secret_env_hash` makes the change visible as env drift, but the reconcile loop runs `ExistingOnly` at boot AND periodically, and there it deliberately leaves running restart-sensitive apps untouched — observed once per tick for half an hour on the dev node. So this reuses FED-07's `credential_rotated` carve-out via a new default-no-op `ContainerOrchestrator::mark_credential_rotated`, on the same reasoning: restart sensitivity protects apps that are working, and this one is working only in appearance. The shell script cannot reach an in-process flag, so it removes the container and lets desired-state recovery rebuild it. 3. LND stayed locked forever on a loaded node. The unlocker is only served after channel.db/graph.db/wallet.db open, measured at 2m38s on a box running 30 containers; the unlock helper gave up at ~60s. That is not a harmless retry — reconcile records the post-start hook as failed, restarts LND, and the slow open begins again, so the wallet never opens and every LND-dependent app stays broken. The not-ready budget is now ~10 minutes; a genuinely wrong password still exits on the first pass via `all_rejected`. Safety properties worth not regressing: - No macaroon content in any response, error, log line or the polled progress feed — digests and byte counts only. - Rotation unlocks via a new `unlock_existing_wallet_no_wipe`, so there is no code path from "rotate my credentials" to `recreate_wallet_destructively`. A wallet whose password this node lacks fails the rotation with the wallet intact. - Channels are compared as active+inactive totals, not `num_active_channels`, which legitimately dips after any restart while peers reconnect. - Backup verified by file count before anything is deleted. Verified: cargo check + fmt clean, 6 new unit tests and the 6 existing container::lnd tests pass, vue-tsc clean, and the built bundle contains the three new RPC method names (the frontend build can silently no-op). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f0c289a415 |
feat(marketplace): implement the DID signature layer that was only specified
docs/marketplace-protocol.md described a full authorship-verification chain and
was marked "shipped end-to-end". It wasn't: `signatures.manifest_hash` and
`signatures.did_signature` existed only as two struct fields that nothing read.
The authenticity actually delivered was the Nostr event's NIP-01 Schnorr
signature — which proves who *relayed* an event, not who *authored* the manifest
inside it. Anyone could republish someone else's manifest under their own DID.
Implemented:
- `canonical_signing_bytes` / `manifest_digest` — the signed preimage is the
manifest as canonical JSON (recursively sorted keys, no whitespace) with
`signatures` omitted, SHA-256'd. Canonicalisation is load-bearing, not
cosmetic: `container.env` is a HashMap with per-process random iteration
order, and `serde_json::Map` is only sorted while the `preserve_order` feature
stays off — a feature any crate in the graph can enable for everyone via
feature unification. Either would make the digest vary between runs, so
signatures would fail *intermittently*, which is far worse to diagnose than
failing cleanly.
- `sign_manifest` / `verify_manifest_signature` — Ed25519 over the 32 raw digest
bytes, verified against the key `author.did` encodes (reusing the existing
`identity::pubkey_bytes_from_did_key`).
- `publish` signs before broadcasting, fills `author.did` when empty, and
**refuses** to publish under a DID this node cannot sign as — otherwise we'd
spray manifests across every relay that every verifier then rejects.
- `discover` verifies before caching. A `missing` signature is a normal
unsigned publisher: listed, but earning no identity trust. An `invalid` one is
tampered or forged, so it is **dropped entirely** and logged — it fails closed
rather than appearing behind a warning badge a user can click through.
Trust scoring now requires proof for both identity-derived factors:
- The 30-point identity factor was `did.starts_with("did:")`. An unsigned
manifest with a plausible DID string and a pinned image scored 65 —
"Community" — on no cryptography at all. It now scores 35, "Unverified".
- **The 20-point federation factor is gated too**, which the original spec did
not say. An unverified `author.did` is just a string the publisher chose, so
without this an attacker could copy the DID of a peer the user federates with
and be rewarded for impersonating the party they trust most.
`marketplace.verify` now returns the signature verdict separately from the
advisory policy issues — `valid` has always meant "passes the advisory security
checks", so conflating it with authenticity would have been its own trap.
Tests (22 pass), weighted to the adversarial cases: tampering; tampering that
also rewrites `manifest_hash` while reusing the stolen signature; signing with
key A while claiming B's DID; undecodable did:keys including the old
`z6MkTest123` fixture that used to score 30/30; malformed base64 and
wrong-length signatures; digest stability across map insertion order; the digest
ignoring the `signatures` block; the federation-impersonation case; and a legacy
cache without the new field loading as `missing` rather than defaulting trusted.
Protocol doc rewritten so the preimage rules are normative — a third-party
implementation that canonicalises differently produces signatures we reject, so
"sorted keys, no whitespace, signatures omitted, sign the raw digest" now has to
be stated exactly rather than sketched.
Not included: surfacing the verdict in Marketplace.vue, which reads only
trust_score/trust_tier today. The field reaches the frontend; where the badge
goes is a UI call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
fe46c898d1 |
fix(license): replace the LGPL zbase32 crate with an in-tree implementation
`zbase32 0.1.2` is LGPL-3.0+ — the only hard copyleft dependency in the whole
Rust graph and the last remaining blocker for the MIT release
(docs/LICENSE-COMPLIANCE-AUDIT.md §2). Statically linking LGPL code into a Rust
binary obliges us to ship relinkable objects, which is impractical for a node
image.
The audit offered two routes: the MIT `z32` crate, or an original
implementation. Took the latter — z-base-32 is an alphabet substitution over a
bit stream, so ~60 lines removes the blocker while adding *zero* new
dependencies rather than trading one supply-chain entry for another.
**Byte-compatibility was the requirement, not a nice-to-have.** A `did:dht`
identifier IS this encoding of an Ed25519 public key, so any drift would
silently rotate every node's DID and orphan its already-published DHT records.
So the semantics were not guessed: I read the vendored zbase32-0.1.2 source to
extract exactly what `encode_full_bytes` and `decode_full_bytes_str` do —
including that decode truncates to the next lower byte boundary, which is why a
52-character string round-trips to 32 bytes while discarding 4 padding bits.
A model implementation was then validated against three independent sources
before any Rust was written, all five vectors agreeing:
encode(b"testdata", 64) -> qt1zg7drcf4gn (crate doctest)
encode_full_bytes("Just an…") -> jj4zg7bycfzn… (crate doctest)
decode_full_bytes("qb1ze3m1") -> b"peter" (crate doctest)
encode([f0,bf,c7]) -> 6n9hq (Zimmermann spec)
encode([d4,7a,04]) -> 4t7ye (Zimmermann spec)
The module pins all of those plus four known 32-byte keys, a 0..40-byte
round-trip sweep, a 52-char/round-trip check over 64 keys, rejection of the
characters z-base-32 deliberately omits (`l`, `v`, `2`, `0`) and of non-ASCII,
and an alphabet/decode-table consistency check so the compile-time reverse table
can't drift from the alphabet.
`did_dht.rs` gains `did_for_a_known_key_is_stable`, which pins the full
identifier string for a known key — the regression that would actually hurt,
asserted at the call site that gives the string its meaning.
Dropped from Cargo.toml and Cargo.lock (7 lines); no other user in the tree.
Verified: 28/28 network tests pass, zero copyleft crates remain in the lockfile.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6d33fea157 |
chore(license): actually delete the proprietary fonts and unused packages
Demo images / Build & push demo images (push) Successful in 3m38s
The audit has claimed since 2026-07-23 that these were git-rm'd. They weren't —
only the web/dist copies went, and all of them were still tracked at HEAD nearly
three weeks later, in a repo about to be published under MIT.
Deleted (~40.7 MB):
neode-ui/public/assets/fonts/Courier_New/{CourierNew-Bold,CourierNew-Regular}.ttf
neode-ui/public/assets/fonts/Benton_Sans/BentonSans-Regular.otf
neode-ui/public/assets/fonts/Redacted/redacted.regular.ttf
neode-ui/public/packages/wireguard.apk (17 MB)
neode-ui/public/packages/atob.s9pk (23 MB)
Courier New is Monotype proprietary and Benton Sans is a commercial Font Bureau
typeface — neither is redistributable. wireguard.apk carries GPL-2.0 libwg
components, so shipping it triggers a source offer. atob.s9pk is a Start9
package of unknown license. Redacted's upstream is OFL-1.1 but no license text
was shipped; deleting was cheaper than sourcing it, since it was unused.
Verified unreferenced before deleting, not after:
- Every @font-face rule in the tree (2 in src/style.css, 2 in
public/entropy/index.html) loads Montserrat. None of these files was ever
loaded by CSS.
- The three `Courier New` hits (tailwind.config.js `mono`, two public HTML
font-family lists) name the *system* font as a fallback — they are not
@font-face sources, so rendering is unchanged.
- wireguard.apk and atob.s9pk have zero references in any tracked file.
- These live under neode-ui/public/, which Vite copies verbatim rather than
resolving, so their absence cannot break a build.
Deliberately kept: neode-ui/public/packages/archipelago-companion.apk, which IS
live (staged by .githooks/pre-push, the Android release flow, and the in-app
pairing QR); Montserrat (OFL.txt) and Open Sans (LICENSE.txt), both properly
licensed; and neode-ui/test-install.sh, which the same audit line listed but
which is not a licensing concern.
Audit updated: §1 and §3's font/package items marked closed, the false DONE
entry rewritten as a history note rather than deleted — a DONE line here is a
claim and should be re-verified with git ls-tree, which is exactly the lesson.
§2 (zbase32, LGPL-3.0+) is now the last hard blocker.
Side effect: ~40 MB off the frontend OTA tarball.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c3341fc680 |
docs(signing-runbook): Workstream B is complete — the anchor is pinned
The runbook still opened with "the catalog is accepted unsigned (migration window) and the anchor is unpinned (RELEASE_ROOT_PUBKEY_HEX = None)". Both have been true-for-a-while false: `trust::anchor::RELEASE_ROOT_PUBKEY_HEX` is a `Some(...)` with a verification note in its doc comment, and `releases/app-catalog.json` carries both a `signature` and a `signed_by` did:key. This one matters more than a normal stale status: a reader taking the header at face value would think the fleet still accepts unsigned catalogs and that the one-way anchor-pinning door is still open. It isn't — pinning already happened, so any future ceremony is a *rotation*, which is the case the doc's own warning about mismatched-signature hard-rejection applies to most sharply. Marked complete and kept the procedure verbatim below, since it's exactly what a key rotation or publisher change needs. Also dropped a stale `:21` line number from the anchor.rs citation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a7992233ab |
docs: two design docs say "no code" for subsystems that exist
The inverse of the usual drift — these understate rather than overstate, which is just as misleading for someone deciding what is safe to change. **dht-distribution-design.md** was headed "Status: Design (no code yet)". `core/archipelago/src/swarm/` has five modules plus `content_hash.rs`. **phase4-streaming-ecash-plan.md** was headed "not implemented". `swarm/paid.rs` states in its own header that it implements "DHT distribution plan, Phase 4 step F", and there is a `streaming::` module behind five `streaming.*` RPCs (list-services, configure-service, toggle-service, pay, prepare-payment). Neither is reachable in a stock build, which is presumably why the headers were never updated — and that is the part worth documenting rather than eliding. Both now state the gates: the `iroh-swarm` cargo feature is off by default (iroh and iroh-blobs are optional deps pulled in only by it), `config.swarm_enabled` is off by default, and paid serving stays free for everyone until the operator enables the `content-download` streaming service. Checked the other plan-only docs for the same error; these two were the only ones. `nostr-identity-import-plan.md`, `nostr-signer-login-research.md` and `hardware-signer-design.md` correctly say no code exists — verified: no identity import or NIP-07 login RPC, and no TROPIC01 reference anywhere in core. `dual-ecash-design.md`'s "in progress" is right too — the `wallet.fedimint-*` RPCs exist, no Cashu ones do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1481b873f8 |
docs(license-audit): the "deleted" proprietary fonts and APKs were never deleted
The 2026-07-23 status block lists as DONE: "Deleted: Courier_New/, Benton_Sans/,
Redacted/ fonts; wireguard.apk; atob.s9pk; obsolete test-install.sh (all
git-rm'd)". All seven are still tracked at HEAD and present on disk. Only the
web/dist copies went; the sources never did.
git ls-tree -r HEAD --name-only | grep -iE 'Courier_New|Benton_Sans|Redacted/|wireguard.apk|atob.s9pk'
That means a repo about to be published under MIT still carries a commercial
Font Bureau typeface and two proprietary Monotype fonts — precisely what §3 of
this audit says must not ship. An audit that reports a blocker as closed is
worse than one that never checked, so the entry is now struck through with the
file list and the verification command inline.
Deleting them is safe and I checked before saying so: nothing references the
font *files* (the three `Courier New` hits are CSS font-family fallbacks naming
the system font, not @font-face sources), and wireguard.apk / atob.s9pk have
zero references anywhere in the tree. Left the deletion itself to the operator —
it is 40 MB of tracked binaries and outside a docs pass. Removing them also
takes 40 MB off the frontend OTA tarball, which is a separate open item.
Also re-verified the rest of the remaining list:
- `zbase32` (LGPL-3.0+) is still a direct dep (Cargo.toml:113, did_dht.rs:40,49).
Still the only hard copyleft blocker.
- LICENSE (MIT), NOTICE and both THIRD-PARTY-LICENSES inventories are present —
so the headline "no license of its own" is closed; softened the verdict to say
which blockers remain rather than leaving a stale "not releasable as-is".
- The four StartOS-derived crates still exist; flagged that KEY-05 cites
core/models, so that one needs review rather than a blind delete.
- Item 6 (git filter-repo history purge) is superseded — the launch plan is a
fresh-history publish, so there is no history to rewrite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b33138a13d |
docs(adr): record what ADR-009 actually enforces and amend ADR-004
**ADR-009** lists six "non-negotiable" mandatory security defaults. Checked each against `core/container/src/manifest.rs` and `core/security/src/`: - `seccomp_profile: Default` — the string `seccomp` appears **nowhere in `core/`**. Not as code, not as a TODO. This constraint is entirely fictional. - AppArmor — `container_policies.rs` generates and `apparmor_parser -r`s a profile, but its own comment reads `TODO: Configure Podman to use the profile`. `security.apparmor_profile` parses into a manifest field that nothing ever reads. - `user` UID > 1000 — no UID validation exists in the runtime parser at all. - `image_tag` pinned — preflight script only; the parser accepts `:latest`. - `readonly_root` / `no_new_privileges` — safe defaults when omitted, but `validate_security()` never rejects an explicit `false`, so the ADR's "Reject manifests that violate mandatory defaults" step does not exist. Genuinely enforced: the capability allow-list and bind-mount confinement (the latter stronger than the ADR describes). Added an Implementation status section saying so per-row. The decision stands; the claim of enforcement did not, and on a security ADR that gap is the whole point of writing it down. **ADR-004** said Tor carries *all* inter-node communication and runs as the `archy-tor` container. Neither holds: transport priority is mesh → LAN → FIPS → Tor (`TransportKind` 1-4, Tor as last fallback, largely because of the latency this ADR itself lists), and Tor is the host Debian service driven by `archipelago-tor-helper` — `container-doctor.sh` actively removes an `archy-tor` container if it finds one, and no `apps/tor` manifest exists. Added an amendment rather than rewriting the record. Worth flagging that both changes landed without their own ADR. All 10 ADRs are Status: Accepted; 001-003, 005-008 and 011 verified consistent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7c214e6497 |
docs(bulletproof-containers): say which parts of the plan actually shipped
Header claimed the whole 2026-04-22 plan "has been implemented". The architecture was adopted, but checking each item against the tree: - The `core/archipelago/src/reconcile/` module the doc lays out in detail — desired.rs / current.rs / diff.rs / apply.rs / derived.rs / backoff.rs — was never created. The reconciler shipped as container/boot_reconciler.rs + container/prod_orchestrator.rs instead. - FM2's named fix `reconcile::derived::render_bitcoin_conf` does not exist. The drift was eliminated a different way: bitcoind runs with an explicit `-conf` derived from secrets each start, and stale datadir configs are removed. - FM1/FM3 are partial — companion UIs are Quadlet units, main app containers are not, since `use_quadlet_backends` still defaults false. The "v1.7.48+ full reconcile module / main containers become Quadlet units" step has not happened. - **FM6 was never implemented.** There is no podman corrupt-state probe and no `system renumber` recovery anywhere in the tree. The 2026-04 failure that made a registry node unreachable would still require manual SSH today — which is precisely the "zero-manual-intervention" target this doc opens with. FM4 and FM5 did ship as described. Replaced the blanket claim with a per-item table so the doc stays useful as incident history without reading as a description of the code, and noted that the unit path throughout says /etc/containers/systemd/ while units are actually written to ~/.config/containers/systemd/ (the path is rootless). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ecd9295e96 |
docs: fix the wrong journalctl scope and document the host-network port drop
**container-lifecycle.md** told operators to read the reconciler's decisions with `journalctl --user -u archipelago`. That returns nothing: `archipelago.service` is a SYSTEM unit (`WantedBy=multi-user.target`) that merely runs as `User=archipelago`. It's `sudo journalctl -u archipelago`. Easy to get wrong because the companion Quadlet units next door genuinely are `--user`, so both forms appear in the docs and only one is right per unit — spelled that out inline. Swept the rest of docs/: no other instance. **quadlet-compilation.md** — added the `Network=host` case. Podman rejects `PublishPort` with host networking (crash-loop, exit 125), so the renderer drops declared ports rather than emitting them (`render_host_network_omits_publish_ports`). A developer reading the directive list would otherwise expect a mapping that never appears. Everything else in both docs verified against quadlet.rs / prod_orchestrator.rs / boot_reconciler.rs: the unit dir, the DO-NOT-EDIT header, Pull=never, DropCapability=ALL, Secret=…,type=env, TimeoutStartSec=0, RestartSec=10, WantedBy=default.target, the render/write_if_changed/enable_now/disable_remove four-step, uid 1000, adopt_existing, the user-stopped.json / user-uninstalled.json desired-state gates, and the 30s tick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
623eb0f033 |
docs(SEED-VERIFICATION): add the missing FIPS key, fix the comparison commands
Ran the doc's script rather than only reading it, and cross-checked every primitive it implements against independent libraries (bip_utils for BIP-39 seed / BIP-32 derivation / bech32, and cryptography's own HKDF): BIP-39 seed, m/44'/1237'/0'/0/0, m/84'/0'/0', x-only pubkey, npub encoding and HKDF-SHA256(salt=None) all match byte for byte. The hand-rolled crypto in this doc is correct. Two real gaps fixed: - **The FIPS mesh transport key was missing.** `seed.rs:227` derives it from the same master seed via `archipelago/fips/secp256k1/v1`, and a user verifying their backup had no way to check it — despite it being the key that authenticates them on the mesh. Added it to the diagram and as section 2b of the script (same shape as the node Nostr key; verified against `derive_fips_key` and `hkdf_derive` using `Hkdf::new(None, ikm)`). - **The "compare with your node" commands were wrong.** The RPC endpoint is `/rpc/v1`, not `/api/rpc`, and `identity.get-node` is not a method — the real ones are `node.did` and `node.nostr-pubkey`. Also dropped "UI: Settings > Identity", which is not a screen that exists, in favour of the two identity files on disk. Verified and left alone: all five other HKDF info strings, both BIP-32 paths, and the `node_key.pub` filename. The release-root key (`archipelago/release/root/ed25519/v1`) is deliberately still absent — it is derived from the project's signing seed, not a user's node seed. Noted separately: `system.get-node-key` sits in the CSRF-exempt list (`api/rpc/mod.rs:337`) but has no dispatcher arm, so it is an exemption for a method that does not exist. Harmless, but it should be removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
db60c3382d |
docs(marketplace-protocol): the DID signature layer is specified but not implemented
This doc is marked "Status: implemented ... shipped end-to-end" and then
describes a cryptographic verification chain that does not exist. On a repo
about to go public, that is the single worst kind of doc bug: it promises a
security property.
`signatures.manifest_hash` / `signatures.did_signature` appear exactly once in
the codebase — as two struct fields at `marketplace.rs:106-107`. Nothing reads
them. There is no hash comparison, no DID resolution, no signature check. The
authenticity actually delivered is the Nostr event's own NIP-01 Schnorr
signature, which proves the publishing key sent the event but says nothing about
the DID the manifest names.
Added a warning at the top, marked the "Manifest Signing (DID Layer)" section
and steps 3-6 of the verification flow as not implemented, and annotated steps
7-8 as advisory (validate_manifest returns scoring issues; it does not block
discovery or install).
The trust model was overstated in the same direction:
- "DID Verification | 30 | Manifest is signed by a valid DID key" is a
`did.starts_with("did:")` string test. Any publisher can claim any DID and
take the 30 points.
- "Relay Consensus | 20" is graduated and never zero (1 relay still scores 5).
- "Version History | 15 | multiple published versions (shows maintenance)" —
nothing counts versions; it's 10 for a 3-part semver plus 5 for a non-empty
repo_url.
Worked the arithmetic through: an unsigned manifest with a plausible DID string
and a pinned image scores 65, landing in the "Community" tier. Said so.
Other corrections:
- `marketplace.unpublish` is documented but was never implemented (the string
appears nowhere); removed it and noted why NIP-33 makes it non-trivial. Added
the two payment methods that do exist (`create-invoice`, `check-payment`).
- The schema section said marketplace manifests "follow the existing
apps/{app-id}/manifest.yml schema", contradicting the header three paragraphs
above. They are separate types.
- The security-enforcement list claimed a capability allow-list, a
host-networking ban and system-path mount restrictions. Those rules are real
but live in the runtime manifest parser for a different schema — marketplace
validation checks four things and gates none of them.
- `run_as_user` documented as "> 1000" in two places while the code checks
`>= 1000` and the doc's own example uses 1000.
- Data-storage tree listed `cache/trust-scores.json` and `config.json`; neither
is ever written.
- The 15-minute cache TTL and 30-minute background refresh don't exist —
discovery is RPC-triggered and the cache has no expiry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
69f1d18ee7 |
docs(README): index the contributor docs the launch reader needs
The index covered users, architecture and app development but had no entry point for "I want to work on Archipelago itself" — so eight tracked docs were reachable only by guessing filenames, including the two that matter most to a newcomer: developer-guide.md (how to build the workspace, the frontend and an ISO) and LICENSE-COMPLIANCE-AUDIT.md (dependency licensing, which is exactly what a reader checks first on an open-source repo). Added a "Contributing to Archipelago itself" section covering those plus bulletproof-containers, the signing runbook, the 1.8.0 hardening plan and CLAUDE.md; filed pine-voice-commands under Getting started and demo-build-info under contributing. Also noted that ADR-010 was never issued — verified across all history, so the 009 → 011 gap is not a missing file — and added the two archived session logs (HANDOVER-2026-07-02, SESSION-1.8.0-OTA-PROGRESS) to the archive table, which already claimed to cover completed session logs but listed none. Link check re-run across docs/: 0 broken. Only RELEASE_NOTES_BACKLOG.md is now deliberately unindexed (internal working list). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a28b3b696d |
docs(registry-manifest-design): stop describing the pre-Phase-1 state as "today"
The header says Phases 1-3 shipped, then §1 "Where we are today" described the world before any of them: catalog carrying "version + image override only", the manifest "never registry-distributed", counts of 48 disk manifests and 28 catalog entries. A reader hits the contradiction immediately and can't tell which half is current. Retitled §1 as the pre-Phase-1 baseline it is, and added the actual state: `releases/app-catalog.json` has 66 entries and 56 embed a full `manifest` block — one for every `apps/*/manifest.yml` in the tree (the stale counts were 48 and 28). What's genuinely left is Phase 4 (build-context apps) and Phase 5 (drop `apps/` from the OTA rsync), which the phase list already marks ⏳. Also: - The install arrow claimed "render Quadlet unit"; same overstatement corrected in architecture.md and app-manifest-spec.md — Quadlet is opt-in, the default is podman create+start. - §8's open question "generated_files with inline content — already supported?" is answered: `app.files[]` takes inline `content` with placeholder rendering. Marked answered rather than leaving a resolved question looking open. Verified present and unchanged: `catalog_manifest_to_overlay`, `install_stack_via_orchestrator`, `install_immich_stack`, and the catalog-wins merge semantics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2c53a7d77f |
docs: fix the CSRF-exempt list and describe how secret_env actually reaches a container
**COMMANDS.md** named six CSRF-exempt read-only methods, two of which
(`bitcoin.getinfo`, `monitoring.current`) are not exempt — a client trusting the
doc would send them with the cookie alone and get rejected. The real set is
twelve (`api/rpc/mod.rs:326-340`); listed all of them and said plainly that
everything else needs the header. The rest of the doc verified clean: the 480 /
200 / 160-char caps, the four `assistant_*` config keys, both default model ids,
`is_sender_allowed`, `strip_archy_trigger` / `run_node_cmd`, the three
unauthenticated HTTP endpoints, and `auth.login.totp` all match the code.
**secrets.md** said `secret_env` "sets `<key>` in the container's environment",
which reads as a plain `-e KEY=value` and undersells the design. It isn't:
resolved pairs are registered as podman secrets named
`archy-env-<app-id>-<key>` and referenced by name, precisely so the value stays
out of `podman inspect` and out of plaintext `Environment=` lines in Quadlet
units. Also documented the interpolation-taint rule — a plain `environment`
entry that expands `${SECRET}` (BTCPay's connection strings) is itself treated
as secret-bearing rather than left in the clear, which is what makes it safe to
build connection strings from secrets.
Everything else in secrets.md verified against `container/secrets.rs`: the four
kinds and their file shapes, the bare-filename rule, the every-tick idempotent
`ensure_generated_secrets`, and the atomic 0600 temp-fsync-rename writer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7adc3260a6 |
docs(app-manifest-spec): fix the id rule, the Quadlet claim, and the declarative overstatement
Narrative pass over the manifest spec, plus one correction to the guide I committed in |
||
|
|
ba052736be |
docs(app-developer-guide): describe the security rules the parser actually enforces
Narrative pass. The Security Requirements section described a blocklist where
the code enforces an allow-list, and attributed enforcement to the wrong layer:
- **"Forbidden: mounting system paths /, /etc, /var, /usr, /proc, /sys"** — the
real rule (`manifest.rs:1290-1313`) is the inverse: `volumes[].source` must be
absolute and under `/var/lib/archipelago/`, or a plain named volume, or one of
two reviewed exceptions (`/run/user/1000/podman/podman.sock`, `/var/run/dbus`).
Anything else is a parse error. The old wording also listed `/var` as
forbidden while every app in the repo binds `/var/lib/archipelago/<id>` — a
developer reading it would not know where their own data goes.
- **"enforced by the marketplace/catalog pipeline and the node"** — split by
layer instead. The capability allow-list is parser-enforced (verified against
the 9 entries at `manifest.rs:1089-1099`); `:latest` is NOT — only
`validate-app-manifest.sh` checks it, and a `:latest` manifest still installs.
readonly_root / no_new_privileges / network_policy=isolated are parser
defaults, so omitting them is safe rather than dangerous.
Also:
- `derived_env` documented `HOST_IP`/`HOST_MDNS`/`DISK_GB` "such as"; the set is
closed and includes a fourth, `{{BITCOIN_HOST}}`. Noted that unknown
placeholders pass through verbatim rather than erroring, so a typo silently
ships `{{FOO}}` into the container.
- The networking example hardcoded `bitcoin-knots`; `{{BITCOIN_HOST}}` resolves
to knots or core depending on what's installed.
- Documented the `files[].content` placeholder set, which is a different set
from derived_env and wasn't mentioned at all — notably `{{NETWORK_GATEWAY}}`
(the nginx `resolver` fix for post-restart 502s) and `{{secret:NAME}}`.
- The "check the UI" URL `/app/my-app/` is not a route; it's
`/dashboard/apps/:id` (detail) or `/dashboard/app-session/:appId` (embed).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2ed6c71e0c |
docs(troubleshooting): fix advice that doesn't match the node
Narrative pass over troubleshooting.md against the code. Seven claims were wrong, several of them actively misleading: - **Tor is not a container.** §15/§16 told operators to run `podman ps --filter name=tor` / `podman restart tor` and to read `/var/lib/archipelago/tor/hidden_service/hostname`. Tor is the host's Debian package running as `debian-tor`; Archipelago drives it by staging a torrc and poking `archipelago-tor-helper` (`scripts/tor-helper.sh`, which does `systemctl restart tor`). The hidden-service dir is `hidden_service_archipelago` (suffixed), it's root-owned 0700, and the file a normal user can actually read is the synced copy at `/var/lib/archipelago/tor-hostnames/<service>`. - **The USB installer has no "Repair" mode.** Cited three times as the recovery path. The boot menu has exactly three entries: Install, Install (verbose), Boot from local disk. Replaced with what those entries can actually do, plus the fact that the installer prompts for a disk and requires typing `yes`, so booting it isn't itself destructive. - **`bitcoin-cli -datadir=/data`** — the container's datadir is `/home/bitcoin/.bitcoin` and RPC creds are in a generated `/tmp/rpc.conf`; the documented command could not have authenticated. - **"edit bitcoin.conf to add addnode="** — the entrypoint passes an explicit `-conf` and logs "ignoring legacy datadir bitcoin.conf". Flags come from the manifest (and the signed catalog entry that overrides it). - **"Bitcoin requires 600GB+"** — only above the manifest's 1000 GB threshold; below it the node runs pruned at `-prune=550`. - **`sudo systemctl restart podman`** — apps run under rootless Podman as the `archipelago` user, so that restarts an unrelated root socket. - **"Settings > Network"** — DNS config and disk cleanup are both on the Server page (`/server`), not Settings. Also: header claimed "the 20 most common issues" over 21 sections, and §16 presented Tor as required for peering when it's the last fallback after mesh → LAN → FIPS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e7d8dfb633 |
docs: correct the "default password123" claim — production nodes have none
The walkthrough told new users to log in with `password123` and said they'd
be "prompted to change this password immediately". Neither is true on a
release build:
- `AuthManager::ensure_default_user` is never called. `main.rs:356-362`
says so explicitly ("Don't auto-create default user — let onboarding flow
handle password setup via auth.setup"), and the function is `#[allow(dead_code)]`.
- The only `password123` login path is `api/rpc/auth.rs:36-46`, which is
`#[cfg(debug_assertions)]` AND `dev_mode` AND only fires *before* setup —
no release binary carries it.
- `Login.vue` calls `auth.isSetup` on mount and renders the "Set Up Your
Node" password-creation form when it returns false. That is the real
first-boot screen, and it is the only `auth.setup` caller in the frontend.
So there is nothing to be "prompted to change" — the user creates the
password themselves, and the doc's version taught them to look for a
default that does not exist.
Fixed in four places:
- user-walkthrough Step 8 rewritten as "Create Your Password"
- troubleshooting's "Default password is password123" solution replaced,
including the warning that deleting user.json does NOT recover a lost
password (the onboarding gate refuses auth.setup on a provisioned node)
- api-reference cURL example uses a placeholder, not the fake default
- 1.8.0 hardening plan's "kill default credentials" item now reflects that
the web half is done and only the SSH defaults still ship
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e0cc41d31e |
docs(developer-guide): fix stale ISO-builder path and CLAUDE.md label
Verified the project-structure tree against the tree. Two stale entries: - image-recipe/build-auto-installer-iso.sh was the old builder, now under _archived/; the current builder is image-recipe/build-debian-iso.sh (the release workflow drives it via scripts/build-iso-release.sh). Repointed. - CLAUDE.md was labelled "AI development instructions"; it is now the sanitized public contributor guide. Relabelled. Everything else verified: run-tests.sh, first-boot-containers.sh, container.rs, vpn.rs all exist; the add-an-endpoint / add-a-Vue-page tutorials match the current dispatch pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f55ed6bf45 |
docs(architecture): correct the "apps run as Quadlet units" overstatement
The overview stated apps install as user.slice Quadlet units. Verified against prod_orchestrator.rs: use_quadlet_backends defaults to false, so regular apps install via the raw podman path today; the companion UI containers are the ones that run as Quadlet units (companion.rs owns them), and the Quadlet flip to default for all apps is opt-in/held. Reworded both places (the layer diagram and the App Platform section) to match reality and the container-lifecycle / quadlet-compilation dev docs: the orchestrator owns and self-heals app containers; companion UIs run as Quadlet units, the validated path being flipped to default. Everything else in the doc verified accurate — crate table, module map, data paths, security model, and the note that the four orphan crates still exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4155cefdf5 |
docs(security): genericize a node address in the RPC-proxy incident record
BITCOIN-RPC-PROXY-EXPOSURE.md's port claims verify against code (Bitcoin RPC on 127.0.0.1:8332, the bitcoin-ui proxy on 127.0.0.1:8334). But its incident narrative named a specific node's LAN address (192.168.63.240, five times) on a subnet the earlier 192.168.1.x sweep did not cover. Replaced with the RFC 5737 documentation address 192.0.2.240. The incident content — the exposure, the probes, the fix — is unchanged and remains a legitimate public security record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc2d79ce77 |
docs(security): self-contain KEY-05 and PSBT; record the completed entropy migration
Verified the security subsystem's design-doc claims against code:
- KEY-05's foundational claims are accurate: entropy::draw_key_bytes exists,
KeyGenRng is sealed with OsRng as its sole production member, MIN_GUARDED_LEN
is 12, and core/clippy.toml bans rand::random/thread_rng exactly as stated.
- But its per-site table listed every production nonce/key site as disposition
"migrate" (pending), when all of them have since been migrated to
draw_key_bytes(OsRng) — storage_crypto, credentials/store, wallet/bdhke,
mesh/x3dh — and zero rand::random/thread_rng remain in production. Added a
completion note so the doc no longer reads as pending work.
Both KEY-05 and PSBT-SIGNING-ARCHITECTURE referenced
ENTROPY-SEED-AUDIT-2026-07-31.md five times as their evidence base — a doc that
was moved to local-only, so a public reader could not follow it. Reworded all
five to state the audit's findings inline ("the internal entropy audit found
...") without the unresolvable path. No published doc references it now. The
link-checker missed these because they were inline code, not markdown links.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
bcbd4a7032 |
docs(app-developer-guide): add the missing local manifest-validation step
The guide walked a developer from manifest to install but never told them how to validate the manifest locally first — despite scripts/validate-app-manifest.sh existing for exactly that. A developer's first signal that their manifest was wrong would have been an install failure on a node. Adds a "Validate Your Manifest" step at the top of Testing, pointing at the script (recently fixed — it had been rejecting every manifest because it shelled out to a missing ruby). Notes the strict behaviour a new submitter hits, e.g. an unpinned :latest tag is rejected, and that the Rust parser is canonical. Verified: the install RPC example in this guide (id + dockerImage) matches the handler; the cargo test target crate name (archipelago-container) is correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8a341de4b5 |
docs(api-reference): fix the one fabricated RPC method
Verified all 144 documented RPC methods against the dispatcher. 143 are live;
one was fabricated: `mesh.discover` (params { timeout_secs? }, returns
{ nodes: MeshNode[] }) does not exist — "mesh discovery" appears only in code
comments as a concept, never as a method. A developer calling it gets "unknown
method".
Replaced with the real peer-listing method `mesh.peers` (no params, returns
{ peers, count }), which the frontend actually uses and which was undocumented.
Also verified: every source path cited across the docs resolves (placeholders
and a correctly-recorded deletion aside), and every documented app-manifest
field exists in the schema (no fabricated fields).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
68e3f61121 |
docs: remove the last private MEMORY references from design docs
Three `MEMORY → <note>` see-also references pointed at the private agent-memory system from public docs (demo-deployment-design.md x2, registry-manifest-design.md x1). Removed. No tracked doc references the memory system now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a2efdf7358 |
docs: current-state the bitcoin multi-version design; move its rollout handoff local
bitcoin-multi-version-design.md carried three layers of stale internal content: an 80-line HTML-comment work-tracking block (per-phase status with "UNCOMMITTED on the branch", node numbers, "Next action when resuming", "Decisions still needed from user"); a rendered "Status: design (2026-06-22)" header that was wrong — the feature shipped, all four phases, with the downgrade guard added today; two private `MEMORY →` references; and a node-numbered scheduling note. Now: the comment block is gone, the status reflects reality, the MEMORY references and node numbers are removed, and "verify on a real node" replaced the specific fleet addresses. The design content (source-of-truth decision, phase designs, invariants) is unchanged. Separately, bitcoin-version-bulletproof-rollout.md was an inter-agent rollout handoff — node numbers, branch coordination, "the other agent owns" — not a design or reference doc. Moved to local-only (still on disk, gitignored) like the other handoffs; its two path references (a plan doc and a script comment) are generalized. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b4e4189407 |
docs: reframe bulletproof-containers as a historical record; scrub internals
This 2026-04 plan has been implemented, but it still read as an active plan
("implementation started"), linked private agent-memory paths, and ended with a
stale "To resume" work block naming fleet nodes, dated fleet state, and the next
file to edit.
- Header now marks it a historical design record and points at
container-lifecycle.md for the current behaviour.
- Removed the two private ~/.claude/.../memory/ references from the header and
the entire "To resume" section (private paths, node numbers, 2026-04-22 fleet
snapshot — none of it belongs in a public design doc).
- Genericized the one remaining node-number reference in the incident narrative.
The valuable content — the six failure modes and the reconciler reasoning that
answered them — is kept intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
599787690a |
docs: write the three missing app-developer docs (secrets, quadlet, lifecycle)
The open-source plan flagged three references as "the real gaps for app developers", and the docs index named them as not-yet-written. Written now, each from the code rather than stubbed: - secrets.md — generated_secrets/secret_env: the two halves, the four kinds (hex16/hex32/base64/bcrypt) and which files each writes, the idempotent self-healing 0600 materialisation, and the rules a developer must not break (no hardcoded fallbacks, one canonical name, right encoding). From container/secrets.rs and the manifest schema. - quadlet-compilation.md — manifest -> .container unit: the full directive mapping (including Secret= by reference, never value, and Pull=never), where units land (~/.config/containers/systemd, systemctl --user), the render/write/enable/disable lifecycle with write-if-changed, and how to inspect one. From container/quadlet.rs, scoped accurately to the companion-UI path it drives today. - container-lifecycle.md — the level-triggered 30s reconciler: desired state from user-stopped/user-uninstalled/manifest set, the operations table, the self-heal-vs-respect-a-deliberate-stop rule, and migrations-never-destroy-data. From prod_orchestrator.rs and boot_reconciler.rs. Index updated to link all three under App development and the "known gap" note removed. Every link across the docs tree resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
661f3eda25 |
docs: add a grouped documentation index; fix references to now-local-only docs
Two concrete, verifiable documentation gaps from the open-source review: - docs/ had no index. Adds docs/README.md grouping the 60-odd published docs by task — getting started, architecture, app development, design docs, ADRs, security, roadmap — in the bitcoin/bitcoin doc/ style the plan called for. Every link in it resolves (checked). The top-level README now points at it as the front door rather than duplicating the list. - ROADMAP.md and tests/lifecycle/TESTING.md linked docs/multinode-testing-plan.md, which moved to local-only (it is a fleet node inventory, not published). Those references now describe the scope split in prose instead of pointing at a file that is not in the public tree. The index is honest about what is missing: it names the three app-developer docs the plan flagged as gaps (quadlet compilation, container lifecycle, secrets materialisation) as not-yet-written, and points at the authoritative code for each rather than pretending they exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8e814ca06a |
feat(registry): move image and OTA references to the public domain
Demo images / Build & push demo images (push) Failing after 2m22s
Replaces the registry host across 86 files: 309 references, covering all 40 app manifests, the orchestrator and container crates, the release and catalog scripts, both demo-images workflows, the ISO builder, demo-deploy, and the frontend marketplace data. Verified the domain actually serves the registry before rewriting anything, rather than assuming the web host implies the registry: - TLS verifies clean, HTTP/2 on the web root - an anonymous token grants a manifest fetch (HTTP 200) with no credentials - skopeo inspect --no-creds resolves an image and lists its tags That last check is the one that matters: an outside developer with no account can now pull, which was the functional blocker for publishing at all. Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop crossing the network in the clear. Deliberately NOT rewritten: - The public FIPS anchor on port 8444. It is a functional network endpoint every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded seeds than to leaked infrastructure. The domain does resolve to the same host, so it could become a hostname, but that adds a DNS dependency to the path used precisely when things are broken. Worth a deliberate decision, not a side effect of this change. - The companion APK on port 2100. The domain returns 404 for that path, so rewriting it would swap a working URL for a broken one. The Releases page does serve (200), which is where the plan already wants those binaries. - releases/app-catalog.json, releases/manifest.json and release-manifest.json. These carry `signature` and `signed_by`; editing their contents invalidates the signature and the fleet refuses artifacts that fail verification. They were rewritten in a first pass and reverted — they must be regenerated and re-signed through the signing ceremony instead, which needs the mnemonic. So the catalog still advertises the old host until that ceremony runs. Nodes resolve images through the signed catalog, not the on-disk manifests, so this commit alone does not change what a node pulls. Verified: archipelago-container 75/75; every manifest still parses with a top-level app block; no signed artifact modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6ba0599639 |
security: remove all infrastructure and internal process material from the repo
Demo images / Build & push demo images (push) Failing after 2m13s
The repo is source code and guidelines only. Nothing about how Archipelago's own fleet is run, or how the team works, stays in it. Untracked (kept on disk, gitignored) — 250 files: - .planning/ (199) and loop/ — internal development process - fleet operations tooling that targets specific nodes: deploy-to-target, deploy-tailscale, deploy-config-defaults, setup-target-dev, setup-aiui-server, setup-https-dev, debug-frontend, node-profile, fleet-fips-pair/unpair, image-recipe/sync-from-live.sh - image-recipe/INTEGRATION-GUIDE.md and docs/multinode-testing-plan.md, both of which are live-server workflow and fleet node inventories - the Phase 10 on-node verification and evidence records, which cite .planning/ as their evidence base KEY-05-ENTROPY-ENFORCEMENT.md was initially moved out with the other Phase 10 docs and then put back: it is cited as normative rationale from ten places in the codebase, including core/clippy.toml, which bans rand::thread_rng and points at it for the reason. That makes it a guideline, not an internal record. Node names removed from source (48 occurrences across comments, manifests and test fixtures): archi-dev-box, archy-x250*, shorty-s, framework-pt, zaza-optiplex, archi-thinkpad. Comments keep the engineering context and the date, which is what carried the meaning; the machine name did not. Three of those were live test values rather than comments and were replaced with valid stand-ins, not prose: two mDNS hostnames and a mesh peer name. An earlier pass substituted "a test node" into a hostname assertion, producing an invalid hostname; caught and fixed as test-node.local. Wipe mechanism: .local-only/manifest.txt inventories every local-only path and .local-only/wipe.sh deletes them on one confirmation, refusing to touch anything git still tracks. Both are themselves untracked, so the public repo does not carry a map of internal filenames. Verified: cargo check -p archipelago --all-features clean; archipelago-container 75/75 tests pass; appOrigin vitest 7/7; audit-secrets 5/5; every relative link in tracked markdown resolves (0 broken). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cc00884b98 |
docs: repair links left dangling by the operations-doc removal
Untracking the ops docs broke every reference to them. Repoints or removes those references across README, architecture, ROADMAP, the archive index, lifecycle TESTING, the hardening plan, and the security docs — pointing at the issue tracker where a live task list was meant, and dropping the entry entirely where it only existed to link an internal file. Also fixes two pre-existing broken links found by validating every relative link in the tracked docs: - README linked docs/OPEN_SOURCE_READINESS.md, which never existed (underscores vs hyphens). - reticulum-daemon/README.md linked a local Claude session plan at ../../.claude/plans/enchanted-strolling-rocket.md — outside the repo, and a path that would have shipped publicly pointing at nothing. All relative links in tracked markdown now resolve: 0 broken. audit-secrets.sh still 5/5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fda7feda60 |
security: untrack operations docs; scrub infra identifiers from public docs
Operations docs move out of git entirely rather than being sanitized. They
stay on disk for local use and are gitignored, so the Phase 6 export (which
takes HEAD) can never carry them. 15 files: the fleet runbook, hotfix
process, node inventories, internal trackers, session handoffs, the key
rotation/signing-posture records, and the open-source plan itself.
For the docs that remain public, infra identifiers are replaced with things
that are better documentation rather than placeholders: curl examples now
use `archipelago.local`, the product's own mDNS name, so a reader can run
them as-is instead of substituting an address that was never theirs.
Deliberately NOT scrubbed, both verified as functional rather than leaked:
- `tx1138.com` is the shipped default block explorer (DEFAULT_TX_EXPLORER in
useTxExplorer.ts, surfaced in WalletSettingsModal). Product behavior.
- `git.tx1138.com` in core/container/{image_policy,registry}.rs is a retired-
registry constant the code matches on to strip stale entries from legacy
node configs. Removing it would break migration for older nodes.
- `192.168.1.254` in bulletproof-containers.md is the LAN gateway in a podman
bug description, and `192.168.1.x` in user-walkthrough.md is already generic.
Whether a personal domain should be the shipped explorer default in a public
product is a separate product question, not a security one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
39eb6b0553 |
docs(open-source): move Phase 0 rotation to a pre-publish gate
Sequencing change per user decision: credential rotation/revocation runs last, immediately before Phase 6, instead of first. Safe under fresh-history publish — the scrub commits never become public — but recorded as a HARD blocking gate on Phase 6, with an explicit rotation sign-off added as a numbered pre-publish step so "scrubbed" cannot be mistaken for "rotated". Also corrects the plan against what execution actually found: - the fleet password was in 8 tracked files, not 7 (3 in .planning/) - both Gitea tokens are already dead (401); only the `ai` password is live - the Framework node's SSH password was rotated out-of-band and is unrecorded, which would block it from receiving the fleet rotation - .planning/ is 199 tracked files of internal agent state — added to Phase 2 as the largest un-triaged internal block still in the tree Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
19082a44f0 |
security: remove node credentials from tracked files (open-source Phase 1)
Demo images / Build & push demo images (push) Failing after 2m28s
Scrubs the fleet SSH/UI password from every tracked file (22 occurrences) and removes inline credentials from the code paths that used them. Docs and trackers keep the surrounding context — these are published under docs/history/ per the open-source plan — with the literals replaced by <FLEET_PW> / <FLEET_PW_ALT> so the "two variants exist" detail survives without the values. Three of the eight files were in .planning/ and were NOT in the plan's enumerated list; the reworked audit-secrets.sh found them. Code changes: - neode-ui/test-openwrt.mjs: node URL and password come from ARCHY_NODE_URL / ARCHY_NODE_PW; the SSH target derives from the URL instead of a hardcoded tailnet IP; exits 2 when unset. - scripts/run-post-install-tests.sh: drops the built-in "testpass123!" default and adds --password-stdin; refuses to run unauthenticated instead of silently trying a known password. --phase1-only still needs no password. - .gitea/workflows/post-install-tests.yml: sshpass with an inline literal replaced by key auth (NODE_SSH_KEY secret); password comes from the NODE_UI_PASSWORD secret and is piped over stdin rather than argv, so it stays out of the node's process list and the job log. Default target IP removed. scripts/audit-secrets.sh now reports 5/5 pass, 0 fail. Note: rotation of the exposed credentials is deliberately deferred to the pre-publish gate and is NOT done by this commit — these values are still live. See Phase 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
57628a2aa4 |
fix(aiui)+docs: keyboard-resize parity for standalone AIUI; companion handover note
Operator-reported: keyboard opening in chat pads the tab bar and scrolls the page instead of scaling the chat window. Triage: neode-ui already ships interactive-widget=resizes-content + the --visual-viewport-height var, so mobile-web Chrome resizes correctly — but an Android WebView ignores that meta entirely, and the described pan-plus-padding is the adjustPan/edge-to-edge- without-IME-insets signature. Companion-side fix documented for handover in docs/companion-keyboard-viewport.md (manifest adjustResize, or IME insets when edge-to-edge, plus a chrome://inspect verification recipe). Web side gets the one real parity gap: AIUI's standalone index.html lacked the meta neode-ui has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e205f2c34a |
docs(security): prove the delivery path on hardware; close window 15
Controlled test on archi-dev-box with operator approval. The daemon was
stopped first so the reconciler could not repair the state before the
re-exposure was confirmed — without a confirmed 200, the later 401 would
be consistent with the state never having been broken at all.
1. stale conf installed + container restarted -> POST /bitcoin-rpc/
returned 200 with a real block height and Allow-Origin: *
2. daemon started 20:00:36, nothing else touched
3. 20:02:19 reconcile rendered the conf and logged the expected warn
line naming bitcoin-ui/archy-bitcoin-ui, then restarted it
4. POST -> 401, Allow-Origin origin-scoped
5. conf byte-identical to the pre-test known-good, container healthy
Both halves are now proven on real hardware: a05956c4's template (the
gate works) and f6b5245b's delivery path (the gate reaches a container
the reconciler had been skipping).
Also records the operator's decision AGAINST credential rotation — no
macaroon, no Bitcoin RPC password — with the trade it accepts stated
plainly, so it is not silently re-litigated later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5a21b58f69 |
docs(security): record what actually closed :8334, and what it does not prove
The node is closed and verified 401 with origin-scoped CORS. But an
unrelated bitcoin-ui rebuild at 18:36 cleared the stale conf before the
reconcile fix was deployed at 19:06, so the 401 proves a05956c4's
template and NOT the delivery path
|
||
|
|
ba493fb0fc |
docs(security): write up the Bitcoin RPC proxy that stayed open after it was fixed
The half that landed correctly (LND, clean 401) made the half that did not harder to notice, because the first check an operator would run returns a pass. Records the probes, the three-fact root cause, and the pass condition for re-probing a node. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a3283cffb4 |
feat(10-06): enable the entropy lint and supply-chain gates (KEY-05 b/c)
Layer (b) — core/clippy.toml bans rand::random and rand::thread_rng crate-wide, each with a reason naming KEY-05 and pointing at the evidence doc. No CI change was needed: the Rust job already runs `cargo clippy --all-targets --all-features -- -D warnings` from core/, so a disallowed_methods hit is already a build failure. --all-targets covers tests deliberately — a fixture keeping the default is a template for the next production call site. Ordering was asserted before the file was written, not after: the residual count of unmigrated call sites is 0, so this cannot turn CI red for other agents on this shared tree. Layer (c) — core/deny.toml makes the rand major split change-detecting: global multiple-versions = "allow", a per-crate deny-multiple-versions for rand, and a dated grandfather skip pinning =0.9.2 exactly. The tree as it stands passes; a third version or a change to either member fails. Both gates were OBSERVED working, not assumed: - Reintroducing one banned call produced the disallowed_methods error with the reason text reaching the developer at the failure point; reverting returned the residual count to 0. - `cargo deny check bans` exits 0 as-is. Removing the grandfather entry made it exit 2 and print both dependency trees, independently confirming F-07's account of where each rand version comes from. Restored, it exits 0 again. Policy (checkpoint Task 5, human-approved): bans-only. The advisories gate is NOT enabled — it fails builds when a new CVE is published against an existing dep with no local change, which on a tree where several agents push continuously would block everyone at an arbitrary hour, with remediation often meaning a bump to an exactly-pinned crypto dependency. No break-glass procedure exists. F-07's advisory half stays OPEN and is recorded as such. cargo-deny is pinned to 0.20.2 and installed from crates.io rather than via EmbarkStudios/cargo-deny-action, because that action exposes no input to pin the tool version — an unpinned supply-chain checker would reintroduce, at the CI layer, the exact "backend fixed by configuration rather than stated" shape this plan exists to remove. crates.io is also the source vetted at the Task 5 legitimacy gate (EmbarkStudios, repo resolves, ~4.79M downloads). RECORDED HONESTLY: layer (b)'s gate is live but not yet EFFECTIVE. The tree carries 42 pre-existing clippy warnings — unused imports, dead code, ~39 style lints — that are already errors under -D warnings, so that CI step cannot pass today for reasons unrelated to KEY-05. Until a dedicated lint-clearing pass lands, a new banned RNG call would be one error among many rather than a distinctive build-stopper. Pre-existing and out of scope; clearing it right before an OTA would be poor sequencing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a806a658a6 |
docs(10-04): C-3 FAILED — three live nodes share their SSH host keys
Audit checklist item C-3 ("the highest-value check here") is no longer
UNVERIFIED. It failed, and the failure is a live F-03 instance rather than a
theoretical one.
Three distinct fleet nodes — archipelago-1, archy-x250-beta and archipelago —
present byte-identical ECDSA, ED25519 and RSA host key fingerprints. Two of
them (archipelago-1, archy-x250-beta) also present the same TLS certificate,
so they share the TLS private key as well.
Gathered read-only and remotely: ssh-keyscan plus an anonymous TLS handshake.
No node was logged into, nothing was written, nothing was rotated. A weaker
instrument than the checklist's on-node commands, chosen because it needs no
access and therefore covers the reachable fleet rather than two nodes — and it
is sufficient for the FAIL condition, which is any fingerprint appearing twice.
Ruled out the obvious alternative (one machine registered three times on the
tailnet): all three answered live TCP within the same minute, and tailscale
ping resolves them to different physical endpoints on different continents
under different tailnet accounts.
One finding worth more than the count: `archipelago` has a UNIQUE TLS cert
(CN=austin-sapien) and SHARED SSH host keys, because it was renamed and
server.set-name re-mints the cert via regenerate_tls_cert() while touching
nothing else. So TLS uniqueness is not evidence that a node's key material is
per-node — any renamed node gets a unique certificate for free. Checked on TLS
alone, that node would have looked clean. Recorded because it justifies the
audit script reporting the two key classes separately instead of issuing one
node-level verdict.
All three are listed under "shared verdict, deliberately not rotated" with the
reason and the next step. A verification task that remediates is a
verification task that takes a node offline.
Also records what this does NOT establish, each with the evidence still
needed: same-ISO provenance, the script's own verdict on those nodes, that a
rotation preserves the operator's live session on real hardware, that
host_secrets reaches system.stats on a real node, and the four nodes that were
unreachable at scan time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f2f89b5fe3 |
docs(10-02): record C-6 evidence so far — probe-method correction, 3 transports still open
C-6 is NOT closed by this commit and is not marked verified.
Measured (read-only, on-node):
- loopback and self-LAN-IP: auth.isOnboardingComplete 200 (EXPOSED),
seed.status 401 (session enforcement intact) — no stop-the-plan finding.
- /rpc/ returns 404: nginx's second proxy block is not a second door, so
the unauthenticated surface is reachable through /rpc/v1 only.
NOT measured — needs a second machine: LAN, Tor, FIPS mesh ULA.
NOT performed — the KEY-01 refusal check and the fresh-node onboarding
walkthrough. No node runs 10-01's gate yet: the installed binary was built
at 06:37 and
|
||
|
|
257ca7e6ac |
docs(10-06): classify all 43 defaulted-RNG call sites with file:line evidence (KEY-05, F-10a)
F-10a recorded raw grep counts and deliberately declined to classify them. This resolves that: every one of the 43 matches under core/archipelago/src now carries a production/test verdict (evidenced by its file's `#[cfg(test)] mod tests` line), what the drawn value becomes, whether the degenerate-entropy guard applies, and a disposition. Tally: 41 migrate, 2 comment, 0 allow. No site needed an exemption, so the crate-wide ban will have no holes to audit. Two corrections to F-10a, each derived independently with its evidence line: session.rs is 4 production sites not 16 (mod tests begins :471), and mesh/x3dh.rs:100/:114 are u32 prekey identifiers, not key material -- the X25519 secrets come from crypto::generate_x25519_ephemeral() at :99/:113. The enforcement blast radius is pinned with `cargo metadata` output rather than asserted: models, helpers and js-engine are not workspace members, so the two core/models matches are outside the clippy build graph and are recorded as a stated limitation rather than omitted. Requirement: KEY-05. Supersedes R-13, absorbs R-05 and R-09. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
96dba73a16 |
docs(10-04): record D-06 rotation trigger as detect-report-then-apply
Task 1 of 10-04 is a blocking decision checkpoint, rated one-way: rotating a node's SSH host key invalidates every known_hosts entry for it fleet-wide and the old private key is destroyed by the swap. Chosen: detect-report-then-apply. auto-on-boot would fire simultaneous known_hosts breakage across the fleet during an OTA with no operator holding the new fingerprints, and a rotation that fails partway on a remote node (.228 is at a remote site and in real use) needs physical console access. It also cannot be dev-paired, which contradicts the standing verify-on-the-dev-pair- first policy — by the time it has been observed on the dev pair it has already run everywhere. The cost of the chosen option — exposure persists on any node nobody revisits — is bounded by making the verdict visible in system.stats rather than by automation, and by keeping a list in this document of every node that reported `shared` and was deliberately not rotated. Records what the decision binds: detect-only default, --apply inert without --yes, the boot unit carries no apply path, and --apply --yes refuses on any node whose verdict is not `shared`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0d513a0ef7 |
docs(10-05): record the Core-wallet fleet census — 4 nodes clear, 6 unchecked (D-07b)
Task 3 of plan 10-05, run by the operator over Tailscale on 2026-08-02 using the read-only procedure in KEY-03-SIGNING-POSTURE.md. No escalation: nothing found. Examined and CLEAR (4): archi-dev-box, shorty-s/.228, archy-x250-beta, archy-x250-pa. On every one there is no wallet named `archipelago` — the deleted handler's default wallet_name — `listwallets` returns only the unnamed default, and that default reports blank=true, keypoolsize=0, txcount=0, balance=0. The only named wallets are Fedimint gatewayd-*. The result holds across two container vintages (bitcoin-knots and bitcoin-core), so it is not four copies of one image behaving identically. Not examined (6), recorded with reasons rather than omitted: framework-pt, archipelago-1, archipelago and archy-dev-pa (SSH permission denied — password rotated/not held), archipelago-5 (timed out during banner exchange), and archy-x250-dev (offline). Password auth was deliberately not attempted: several fleet nodes lock PAM quickly on a wrong password, and locking out an in-use production node is a worse outcome than an incomplete census. The conclusion is stated at the strength the evidence supports — no *examined* node holds a wallet the deleted handler created, and no examined node holds any wallet with keys or funds. It is deliberately NOT generalised to "the fleet is clear" while six nodes are unknown. F-13 is closed by deletion regardless: the code that could create such a wallet is gone from every future build. No key material appeared in any output and `listdescriptors true` was never run. Also corrects the now-stale R-04/F-13 entry in UNIFIED-TASK-TRACKER.md, which still described `handle_bitcoin_init_wallet_from_seed` and a watch-only migration as pending work — that code no longer exists. Marks it done-by- deletion and adds the six unchecked nodes as a standing item, flagged as a natural fold-in for KEY-04's on-node work but tracked independently so it does not vanish if KEY-04 is re-scoped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |