- Add ACTIVATION_FILE constant to api/main.py
- Import registry and annuaire_client modules
- Replace GET /services with live catalog⨝subscriptions⨝overlay⨝legacy merge
- Add POST /services/auto-register: activates local offers, subscribes to remote
- Add POST /services/{service_id}/request: single-service subscription
- Add POST /services/{service_id}/activate: mark service locally active
- Harden init_dirs() for test monkeypatching (PermissionError guard + patched-path mkdir)
- Add tests/test_services_endpoints.py: 3 tests, all green (32/32 suite)
Adds api/annuaire_client.py — a stdlib-only HTTP client over AF_UNIX
(/run/secubox/annuaire.sock, never the aggregator) with four public
functions: node_identity, get_catalog, get_subscriptions, subscribe.
All calls return (data, error) and never raise into the caller.
did_from_pubkey_hex mirrors annuaire/crypto.did_from_pubkey exactly.
Tests use a real _UnixSocketHTTPServer (BaseServer subclass with
fileno()) for the end-to-end socket test, plus real-missing-socket and
real-key-file tests. 4/4 pass; full suite 29/29.
Wires the gondwana P1 directory (NodeRecord/ConfigBlob/full-log federation +
mesh_sync loop) onto #766's trustless bootstrap:
- mesh listener (annuaire-mesh.conf.tpl) now also serves GET /log/export
(signed, self-certifying, read-only) for cross-node replication.
- mesh_sync pulls /log/export from the #766 listener (wg-mesh-ip:8799), not the
main :9080.
- node identity reuses #766's genesis()/annuairectl/node.key.
Combined suite 167 green.
Slice 5b of gondwana P1: the wg-mesh directory sync. Runs IN-PROCESS in the
annuaire service (the journal owner) rather than an external p2p timer — this
sidesteps both the JWT session-validator (no localhost bypass in require_jwt)
and the single-writer journal race. secubox-p2p stays the peer-list owner
(wg_mesh.json); annuaire only reads it.
- annuaire/mesh_sync.py: read_mesh_peers + sync_once (network injected) — pulls
each peer's public /log/export over the mesh (nginx 0.0.0.0:9080) and merges
via import_entries; never raises on an unreachable peer.
- api/main.py: @on_event startup launches the loop (blocking fetch+merge in a
worker thread); disabled via ANNUAIRE_DIR_SYNC=0.
TDD: +5 tests (converge / unreachable / idempotent / peer-parse); suite 160 green.
Slice 5 (part 1) of gondwana P1. Adds the SERVICE-placement role to p2p.toml,
distinct from [wireguard].role (transport rendezvous). Consumed by the
directory / config-apply / routing layers; single-writer = the 'primary'.
Slice 4 of gondwana P1: thin FastAPI layer over the tested directory verbs.
- GET /nodes, GET /config[?scope=] (public reads)
- GET /log/export (public; full signed log + author_pubkey for federation —
distinct from the truncated recent GET /log)
- POST /node/publish, /config/publish, /config/revoke (JWT)
- POST /log/pull (JWT; fetch a peer /log/export, import_entries; graceful on
an offline peer)
TDD: tests/test_api_directory.py (5 TestClient wiring tests); full suite 155 green.
Security-review follow-ups (no exploitable forgery was found; these address
board-wide nginx availability):
- postinst validates the rendered /etc/nginx/conf.d/annuaire-mesh.conf with
nginx -t and removes it on failure, so a broken render can never persist and
break a later nginx -t by any other actor (reboot, sibling postinst).
- ship /etc/sysctl.d/30-secubox-nonlocal-bind.conf (ip_nonlocal_bind=1) so
nginx binds the specific wg-mesh IP even when wg-quick@wg-mesh starts after
nginx at boot (the board currently has it =0, and already binds specific IPs
→ real bind-failure risk). postinst applies it before rendering the listener.
Slice 3 of gondwana P1: the generic /log replication core. Federation
re-appends each foreign entry's payload+sig into the LOCAL chain (prev/entry
hashes are local), preserving the author's signature. Security:
- dedup by (author, sig) — idempotent re-pulls
- self-certifying binding enforced: did_from_pubkey(author_pubkey) == author
- sig verified over canonical payload BEFORE append
Forged-payload and author-spoofed entries are rejected, never landed.
TDD: +4 adversarial tests; directory suite 23 green, full suite 150 green.
Self-certifying federation (the core fix):
- ingest_offer enforces did_from_pubkey(pubkey) == offer.provider BEFORE the
signature check — closes 'bring your own key, claim any provider DID'. The
sig alone only proves key-ownership; did:plc = sha256(pubkey)[:32] binds the
key to the DID with no directory and no prior trust.
- _get_offers / GET /services emit self-contained verifiable offers: the stored
payload omits sig/signer_did and never carried the pubkey (so a stranger
could not verify at all). Now carry sig + signer_did + provider_pubkey;
provider_pubkey is transport metadata, stripped before reconstructing the
extra=forbid ServiceOffer model.
- pull_services uses the carried pubkey → self-certifying ingest.
Node bootstrap:
- genesis() verb: a node self-attests as founding MEMBER (root of trust),
breaking the invite/join/subscribe chicken-and-egg. Self-cert DID, empty
invited_by, idempotent. Op.GENESIS (additive, chain-compatible).
- crypto.public_from_private().
- /usr/sbin/annuairectl: init|whoami|status|offer|services|pull. Operates the
journal directly as secubox (no JWT for one-time bootstrap); key 0600 in
/etc/secubox/secrets/annuaire/.
Mesh listener:
- nginx/annuaire-mesh.conf.tpl: binds ONLY wg-mesh IPv4:8799, allow
10.10.0.0/24 + deny all, ONLY GET /api/v1/annuaire/services. postinst renders
with detected mesh IP (skips if no mesh); postrm cleans up.
tests: +7 federation tests (134 passing). changelog 0.2.0.
First slice of gondwana P1: make the annuaire the distributed directory.
Adds Op.NODE_PUBLISH/CONFIG_PUBLISH/CONFIG_REVOKE and the NodeRecord (signed
mesh peer registry, public wg key only) + ConfigBlob (signed, versioned,
single-writer config distribution) models — modelled on ServiceOffer.
Carries the consolidated clone/distribute/emancipate roadmap doc.
TDD: tests/test_directory.py (9 tests); full annuaire suite 136 green.
dh_installsystemd's deb-systemd-invoke restart won't start a disabled unit, so
an upgrade on a board where secubox-yacy.service was disabled left
/run/secubox/yacy.sock gone and nginx 502. postinst now does an unconditional
plain systemctl restart (host uvicorn API has no LXC dependency, safe on fresh
installs). Documents the live nginx drift root-cause + Phase-2 detection fix.
ef09b108 shipped the index.html/yacyctl/sudoers but dropped the debian/changelog
1.0.12 block and the rules sudoers-install line, and a later stash-pop layered the
final probe-based yacyctl on top. This reconciles source to the deployed board:
- debian/changelog: 1.0.12-1~bookworm1 entry
- debian/rules: install /etc/sudoers.d/secubox-yacy
- yacyctl: liveness PROBED via curl http://$LXC_IP:$HTTP_PORT/ (no privilege;
preserves the hardened User=secubox+NNP unit); lxc-info best-effort enrichment
- sudoers comment: reflects probe-not-introspect rationale (reload still needs root)