Skip to content

feat(swarm): GET /.well-known/mycelium-node — self-signed advertisement (#87)#91

Merged
Dewinator merged 1 commit into
mainfrom
agent/issue-87-2026-04-28T08-01-09-365Z
Apr 28, 2026
Merged

feat(swarm): GET /.well-known/mycelium-node — self-signed advertisement (#87)#91
Dewinator merged 1 commit into
mainfrom
agent/issue-87-2026-04-28T08-01-09-365Z

Conversation

@Dewinator
Copy link
Copy Markdown
Owner

Summary

  • Closes the Swarm Phase 3c milestone: this node now serves its self-signed NodeAdvertisement at GET /.well-known/mycelium-node per SWARM_SPEC §3.3 + §4.1.
  • First end-to-end wire-path. Composes Phase 1b (NodeIdentityService.getSelf), Phase 2 (signWithSelfKey), and Phase 3a (WIRE_SPEC_VERSION + NodeAdvertisement type) without modifying any of them — Phase 3b's wire-validator already accepts the same shape.
  • Two new env vars: MYCELIUM_PUBLIC_URL (required for participation) and MYCELIUM_DISPLAY_NAME (optional, ≤ 64 chars).

Research summary

  • Existing HTTP entrypoint is scripts/dashboard-server.mjs — already lazy-imports compiled MCP-server modules from mcp-server/dist/. Same pattern here, no new server, no new port (MYCELIUM_PUBLIC_URL set by operator points at this listener).
  • NodeIdentityService.getSelf() returns { node_id, pubkey_b64 } from the nodes table where is_self=true (single-row partial unique index from migration 070). Re-encoded to unpadded base64url for the wire (§3.3 mandates that variant).
  • signWithSelfKey already attaches signed_at to the canonical bytes before signing, so the resulting record has signed_at inside the signature surface — exactly what rule 5 (signature verify) and rule 7 (clock-skew) need on the receiver.
  • Reused the same fixture pattern as wire-validator.test.ts: fresh Ed25519 keypair per test, last 32 bytes of the SPKI DER are the raw pubkey, computeNodeId(pubkeyRaw) is the node_id. Saved a redundant multihash helper that way.

What this does NOT do

  • No /swarm/peers, /swarm/lessons, /swarm/hubs — those are later Phase 3 / 4 issues. (Swarm Phase 3d: migration 071 — peer + signed-record storage (file only, do NOT run) #88 already covers the peer-storage migration.)
  • No TLS termination — assumed to be at the reverse proxy / dashboard layer; this endpoint serves plain HTTP and trusts the proxy for https://.
  • No key bootstrap. If nodes.is_self is missing, the endpoint returns 503 with a clear error — silent key generation here would orphan every signature this node has produced.
  • No new HTTP server framework. node:http route added to the existing dashboard server.

Constitution affirmation

Touches:

  • Pillar 1 — Decentralized, networked AI. Reinforces it: a peer can curl this endpoint and verify identity (multihash(pubkey) == node_id) plus signature without consulting any registry. No central authority.
  • Pillar 6 — Cyber security. Reinforces it: the advertisement is self-signed by the same key it declares; no out-of-band trust root. Failure to find a self-row is a 503, not a silent re-bootstrap — the "generate keys silently" anti-pattern is structurally unreachable.

Pillars 2 (Reproduction), 3 (Swarm intelligence), 4 (Microtransactions), and 5 (Experts) are not touched and remain governed by their respective subsystems. No pillar is weakened.

Test plan

A reviewer can verify with the existing test runner:

cd mcp-server
npm test

The new tests live in mcp-server/src/__tests__/node-advertisement-endpoint.test.ts. They cover:

  • Happy path — boots a real node:http server, hits /.well-known/mycelium-node via global fetch, asserts: status 200, Content-Type: application/json; charset=utf-8, Cache-Control: no-store, body parses as JSON, every required NodeAdvertisement field present, signed_at matches the §3 ISO 8601 ms-precision UTC pattern, multihash(pubkey) == node_id (via the same computeNodeId helper Phase 1b introduced), and the signature verifies via Phase 2's verify() against the on-wire pubkey.
  • 503 casesMYCELIUM_PUBLIC_URL missing, no self-row in nodes, MYCELIUM_DISPLAY_NAME over 64 chars.
  • Optional-field roundtrip — when display_name is unset, the wire body has no display_name key AND the signature still verifies (i.e. we did not silently include an empty string in the canonical bytes).

End-to-end smoke test for an operator with the dashboard running:

export MYCELIUM_PUBLIC_URL="https://your-node.tailnet.ts.net:8787"
export MYCELIUM_DISPLAY_NAME="my-mycelium"
# (restart dashboard server)
curl -s http://127.0.0.1:8787/.well-known/mycelium-node | jq .
# expect: NodeAdvertisement JSON with node_id, pubkey, endpoint_url,
# spec_version "1.0", signed_at, signature, plus your display_name.

🤖 Generated with Claude Code

…nt (#87)

Phase 3c of the Swarm Foundation Plan: serve this node's NodeAdvertisement
at the well-known URL per SWARM_SPEC §3.3 / §4.1. First end-to-end wire
path — composes Phase 1b (NodeIdentityService.getSelf), Phase 2
(signWithSelfKey), and Phase 3a (WIRE_SPEC_VERSION + NodeAdvertisement
type) without modifying any of them.

- new mcp-server/src/swarm/endpoints/node-advertisement.ts — pure handler
  that returns an HTTP-shaped triple (status/headers/body) so the
  surrounding server is policy-free.
- new mcp-server/src/__tests__/node-advertisement-endpoint.test.ts —
  boots a real http.Server, fetches the endpoint, asserts §4.1 + §4.5
  contract: status/content-type/cache-control, multihash(pubkey) ===
  node_id, and signature verifies via Phase 2's verifier against the
  on-wire pubkey. Plus 503 cases for missing public-url / display-name
  too long / no self-row.
- scripts/dashboard-server.mjs registers the route and a singleton
  NodeIdentityService — no new server, no new port (issue constraint).
- README documents MYCELIUM_PUBLIC_URL (required) and
  MYCELIUM_DISPLAY_NAME (optional, ≤ 64 chars).

Hard constraints honored: unauthenticated, no new web framework, no
silent key generation (missing self-row → 503), body is exactly a single
NodeAdvertisement with no envelope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Dewinator Dewinator added the agent-opened Opened by the autonomy loop label Apr 28, 2026
@Dewinator Dewinator merged commit be59267 into main Apr 28, 2026
@Dewinator Dewinator deleted the agent/issue-87-2026-04-28T08-01-09-365Z branch April 28, 2026 08:20
Dewinator added a commit that referenced this pull request Apr 29, 2026
Updates the stale "spec-only" header (phases 0–3 have all merged via
PRs #79/#81/#82/#85/#89/#91/#92) and pins each phase to its issue +
merged commit so a reader can tell at a glance which sections are wired
on `main` vs. still paper.

Phases 4–9 are deliberately listed as "_not yet issued_" — the project's
current priority is *Gehirn perfektionieren* per CLAUDE.md § Roadmap
(Reed 2026-04-26), and the wire contract is frozen at v1.0 so an
independent implementer can build a phase-3-equivalent peer today.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dewinator added a commit that referenced this pull request Apr 29, 2026
The cryptographic foundation of the swarm (SWARM_SPEC v1, Ed25519
signing, JCS canonicalization, wire-validator, .well-known discovery,
peer/signed-record storage — PRs #78,#79,#81,#82,#85,#89,#91,#92) was
landing on main while the README/MANIFESTO still claimed
"pairing/swarm/federation deferred". This commit fixes that mismatch.

README (EN+DE):
- new "Swarm — federation in flight" section with merged-PR table
  and a "what is next" subsection pointing to the swarm label
- Roadmap rewritten: phase 4-5 from "deferred" to "Phase 1 shipped"
- existing /.well-known/mycelium-node block folded into the new section
- promo video as a clickable poster near the top, served from a
  v0.4-swarm-phase-1 GitHub release asset (14 MB H.264 1080p)

MANIFESTO (EN+DE):
- "What is built today" split into brain core + Swarm Phase 1
- aspirational Tailscale+mTLS / mutual-pairing claims removed; those
  pieces remain on archive/swarm-deferred as historical reference
- "What is not built yet" sharpened to the social layer (verification,
  reputation, banishment-by-consensus, Sybil resistance) plus
  micro-transactions

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dewinator added a commit that referenced this pull request May 3, 2026
…nly Vererbung+federation-transport are deferred

Roadmap line 194 lumped "Schwarm + Vererbung + Föderation" as deferred,
but §10 mechanics (anti-echo, contradicts gate, diversity, REM-self-audit,
inbound POST /swarm/lessons admission) have been wired into active code on
main since PRs #91, #121, #128, #131, #148, #154, #155. Issues #137 and
#138 carry status snapshots from earlier ticks confirming end-to-end
wiring against migrations 080 + 082.

Splits item 5 into two distinct lines (Paarung/Vererbung — migrations.deferred
033–036, Cross-Host-Föderation — 038/041) and adds a clarifying paragraph
that points to the still-active §10 substrate plus docs/waves.md for the
cross-wave view. The closing paragraph now also names what specifically
the single brain doesn't know (cross-host trust transport), instead of the
catch-all "Schwarm/Föderation".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-opened Opened by the autonomy loop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant