diff --git a/backend/.env.example b/backend/.env.example index 75d5c61..8d63aff 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -230,7 +230,10 @@ AIS_API_KEY= # https://aisstream.io/ — free tier WebSocket key # Swarm discovery (signed peer manifest). Participants need only the public key; # the seed operator sets MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY (never commit it). # Generate a fleet keypair: uv run python backend/scripts/bootstrap_manifest_helper.py generate-keypair -# MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY= +# Public sb-testnet fleet defaults (auto-used when MESH_INFONET_FLEET_JOIN=true). +# MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY=ul1d0kj/ODPIp0OhHzX8eLAVXzJ3CVvzW1vn2IC6q3I= +# MESH_INFONET_FLEET_JOIN=true +# MESH_INFONET_FLEET_JOIN_DISABLED=false # MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY= # seed only # MESH_BOOTSTRAP_SIGNER_ID=shadowbroker-seed # MESH_PEER_REGISTRY_ENABLED=true # seed only (auto-enabled when private key is set) diff --git a/backend/main.py b/backend/main.py index 0c1ad58..a142415 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1289,10 +1289,14 @@ def _ensure_infonet_private_transport_ready(reason: str = "") -> bool: def _configured_bootstrap_seed_peer_urls() -> list[str]: + from services.mesh.mesh_fleet_defaults import configured_bootstrap_seed_peers_with_fleet_default + settings = get_settings() primary = str(getattr(settings, "MESH_BOOTSTRAP_SEED_PEERS", "") or "").strip() legacy = str(getattr(settings, "MESH_DEFAULT_SYNC_PEERS", "") or "").strip() - return parse_configured_relay_peers(primary or legacy) + return configured_bootstrap_seed_peers_with_fleet_default( + parse_configured_relay_peers(primary or legacy) + ) def _refresh_node_peer_store(*, now: float | None = None) -> dict[str, Any]: @@ -9238,9 +9242,35 @@ async def api_set_node_settings(request: Request, body: NodeSettingsUpdate): if bool(body.enabled): _start_infonet_node_runtime("operator_enable") _kick_public_sync_background("operator_enable") + threading.Thread(target=_swarm_bootstrap_after_transport_ready, daemon=True).start() return result +@app.post("/api/mesh/infonet/swarm/join") +@limiter.limit("10/minute") +async def infonet_swarm_join(request: Request): + """Announce this node to the fleet seed and pull the signed peer manifest.""" + if not _participant_node_enabled(): + return {"ok": False, "detail": "participant node is disabled"} + if _infonet_private_transport_required() and not _ensure_infonet_private_transport_ready("swarm_join"): + return JSONResponse( + {"ok": False, "detail": _infonet_private_transport_error()}, + status_code=503, + ) + + from services.mesh.mesh_swarm_runtime import announce_local_peer_to_seeds, refresh_swarm_manifest_from_seeds + + announce = await asyncio.to_thread(announce_local_peer_to_seeds, force=True) + manifest = await asyncio.to_thread(refresh_swarm_manifest_from_seeds, force=True) + if manifest.get("ok"): + await asyncio.to_thread(_refresh_node_peer_store) + return { + "ok": bool(announce.get("ok")) or bool(manifest.get("ok")), + "announce": announce, + "manifest_pull": manifest, + } + + @app.get("/api/settings/wormhole") @limiter.limit("30/minute") async def api_get_wormhole_settings(request: Request): diff --git a/backend/services/config.py b/backend/services/config.py index e6a625d..ef59450 100644 --- a/backend/services/config.py +++ b/backend/services/config.py @@ -43,8 +43,14 @@ class Settings(BaseSettings): MESH_INFONET_ALLOW_CLEARNET_SYNC: bool = False MESH_BOOTSTRAP_DISABLED: bool = False MESH_BOOTSTRAP_MANIFEST_PATH: str = "data/bootstrap_peers.json" - MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY: str = "" + # Public sb-testnet-0 fleet signer (participants). Seed operator holds the private key. + MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY: str = ( + "ul1d0kj/ODPIp0OhHzX8eLAVXzJ3CVvzW1vn2IC6q3I=" + ) MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY: str = "" + # When true, empty MESH_PEER_PUSH_SECRET uses the public fleet HMAC for seed join/announce. + MESH_INFONET_FLEET_JOIN: bool = True + MESH_INFONET_FLEET_JOIN_DISABLED: bool = False MESH_BOOTSTRAP_SIGNER_ID: str = "" MESH_PEER_REGISTRY_ENABLED: bool = False MESH_PEER_REGISTRY_DISABLED: bool = False diff --git a/backend/services/mesh/mesh_bootstrap_manifest.py b/backend/services/mesh/mesh_bootstrap_manifest.py index 923837e..62ad291 100644 --- a/backend/services/mesh/mesh_bootstrap_manifest.py +++ b/backend/services/mesh/mesh_bootstrap_manifest.py @@ -342,7 +342,9 @@ def load_bootstrap_manifest_from_settings(*, now: float | None = None) -> Bootst settings = get_settings() if bool(getattr(settings, "MESH_BOOTSTRAP_DISABLED", False)): return None - signer_public_key_b64 = str(getattr(settings, "MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY", "") or "").strip() + from services.mesh.mesh_fleet_defaults import effective_bootstrap_signer_public_key_b64 + + signer_public_key_b64 = effective_bootstrap_signer_public_key_b64() if not signer_public_key_b64: return None manifest_path = _resolve_manifest_path(str(getattr(settings, "MESH_BOOTSTRAP_MANIFEST_PATH", "") or "")) diff --git a/backend/services/mesh/mesh_crypto.py b/backend/services/mesh/mesh_crypto.py index 6c24a17..56370e4 100644 --- a/backend/services/mesh/mesh_crypto.py +++ b/backend/services/mesh/mesh_crypto.py @@ -168,9 +168,9 @@ def resolve_peer_key_for_url(peer_url: str) -> bytes: try: from services.config import get_settings - global_secret = str( - getattr(get_settings(), "MESH_PEER_PUSH_SECRET", "") or "" - ).strip() + from services.mesh.mesh_fleet_defaults import effective_peer_push_secret + + global_secret = effective_peer_push_secret() except Exception: return b"" if not global_secret: diff --git a/backend/services/mesh/mesh_fleet_defaults.py b/backend/services/mesh/mesh_fleet_defaults.py new file mode 100644 index 0000000..97e4a3b --- /dev/null +++ b/backend/services/mesh/mesh_fleet_defaults.py @@ -0,0 +1,64 @@ +"""Public Infonet fleet defaults for sb-testnet-0 participants. + +Operators who run private single-node installs can set ``MESH_INFONET_FLEET_JOIN=false`` +and provide their own signer keys / peer secrets. +""" + +from __future__ import annotations + +FLEET_NETWORK_ID = "sb-testnet-0" +FLEET_SEED_ONION_URL = ( + "http://gqpbunqbgtkcqilvclm3xrkt3zowjyl3s62kkktvojgvxzizamvbrqid.onion:8000" +) +FLEET_BOOTSTRAP_SIGNER_PUBLIC_KEY_B64 = ( + "ul1d0kj/ODPIp0OhHzX8eLAVXzJ3CVvzW1vn2IC6q3I=" +) +# Shared fleet HMAC for sb-testnet peer announce/push/sync. Public testnet join model. +FLEET_PEER_PUSH_SECRET = "b7GoqsvoUD9MV7tyt0ZOzMptLA84QG6KCfaV9nDqz5Y" + + +def infonet_fleet_join_enabled() -> bool: + try: + from services.config import get_settings + + if bool(getattr(get_settings(), "MESH_INFONET_FLEET_JOIN_DISABLED", False)): + return False + return bool(getattr(get_settings(), "MESH_INFONET_FLEET_JOIN", True)) + except Exception: + return True + + +def effective_bootstrap_signer_public_key_b64() -> str: + try: + from services.config import get_settings + + configured = str(getattr(get_settings(), "MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY", "") or "").strip() + if configured: + return configured + except Exception: + pass + if infonet_fleet_join_enabled(): + return FLEET_BOOTSTRAP_SIGNER_PUBLIC_KEY_B64 + return "" + + +def effective_peer_push_secret() -> str: + try: + from services.config import get_settings + + configured = str(getattr(get_settings(), "MESH_PEER_PUSH_SECRET", "") or "").strip() + if configured: + return configured + except Exception: + pass + if infonet_fleet_join_enabled(): + return FLEET_PEER_PUSH_SECRET + return "" + + +def configured_bootstrap_seed_peers_with_fleet_default(peers: list[str]) -> list[str]: + if peers: + return peers + if infonet_fleet_join_enabled(): + return [FLEET_SEED_ONION_URL] + return [] diff --git a/backend/services/mesh/mesh_swarm_runtime.py b/backend/services/mesh/mesh_swarm_runtime.py index b6a37d3..9d5f383 100644 --- a/backend/services/mesh/mesh_swarm_runtime.py +++ b/backend/services/mesh/mesh_swarm_runtime.py @@ -50,7 +50,9 @@ def _manifest_path() -> str: def _signer_public_key_b64() -> str: - return str(getattr(get_settings(), "MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY", "") or "").strip() + from services.mesh.mesh_fleet_defaults import effective_bootstrap_signer_public_key_b64 + + return effective_bootstrap_signer_public_key_b64() def _signer_private_key_b64() -> str: @@ -67,10 +69,14 @@ def _private_transport_required() -> bool: def _configured_seed_peer_urls() -> list[str]: + from services.mesh.mesh_fleet_defaults import configured_bootstrap_seed_peers_with_fleet_default + settings = get_settings() primary = str(getattr(settings, "MESH_BOOTSTRAP_SEED_PEERS", "") or "").strip() legacy = str(getattr(settings, "MESH_DEFAULT_SYNC_PEERS", "") or "").strip() - return parse_configured_relay_peers(primary or legacy) + return configured_bootstrap_seed_peers_with_fleet_default( + parse_configured_relay_peers(primary or legacy) + ) def _seed_manifest_peers() -> list[dict[str, str]]: diff --git a/backend/tests/mesh/test_mesh_fleet_defaults.py b/backend/tests/mesh/test_mesh_fleet_defaults.py new file mode 100644 index 0000000..22491f4 --- /dev/null +++ b/backend/tests/mesh/test_mesh_fleet_defaults.py @@ -0,0 +1,35 @@ +from services.mesh.mesh_fleet_defaults import ( + FLEET_PEER_PUSH_SECRET, + effective_bootstrap_signer_public_key_b64, + effective_peer_push_secret, + infonet_fleet_join_enabled, +) + + +def test_fleet_defaults_apply_when_join_enabled(monkeypatch): + from services.config import get_settings + + monkeypatch.delenv("MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY", raising=False) + monkeypatch.delenv("MESH_PEER_PUSH_SECRET", raising=False) + monkeypatch.setenv("MESH_INFONET_FLEET_JOIN", "true") + get_settings.cache_clear() + try: + assert infonet_fleet_join_enabled() is True + assert effective_bootstrap_signer_public_key_b64() + assert effective_peer_push_secret() == FLEET_PEER_PUSH_SECRET + finally: + get_settings.cache_clear() + + +def test_fleet_defaults_disabled(monkeypatch): + from services.config import get_settings + + monkeypatch.setenv("MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY", "") + monkeypatch.setenv("MESH_PEER_PUSH_SECRET", "") + monkeypatch.setenv("MESH_INFONET_FLEET_JOIN_DISABLED", "true") + get_settings.cache_clear() + try: + assert infonet_fleet_join_enabled() is False + assert effective_peer_push_secret() == "" + finally: + get_settings.cache_clear() diff --git a/backend/tests/mesh/test_mesh_node_bootstrap_runtime.py b/backend/tests/mesh/test_mesh_node_bootstrap_runtime.py index 2b8ff7a..dc4ed2a 100644 --- a/backend/tests/mesh/test_mesh_node_bootstrap_runtime.py +++ b/backend/tests/mesh/test_mesh_node_bootstrap_runtime.py @@ -102,6 +102,7 @@ def test_refresh_node_peer_store_promotes_manifest_peers_to_sync_only(tmp_path, monkeypatch.setenv("MESH_BOOTSTRAP_SEED_PEERS", "") monkeypatch.setenv("MESH_DEFAULT_SYNC_PEERS", "") monkeypatch.setenv("MESH_INFONET_ALLOW_CLEARNET_SYNC", "true") + monkeypatch.setenv("MESH_INFONET_FLEET_JOIN_DISABLED", "true") get_settings.cache_clear() try: @@ -135,6 +136,7 @@ def test_refresh_node_peer_store_adds_bootstrap_seed_as_pull_only_peer(tmp_path, monkeypatch.setenv("MESH_DEFAULT_SYNC_PEERS", "") monkeypatch.setenv("MESH_INFONET_ALLOW_CLEARNET_SYNC", "true") monkeypatch.setenv("MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY", "") + monkeypatch.setenv("MESH_INFONET_FLEET_JOIN_DISABLED", "true") get_settings.cache_clear() try: @@ -171,6 +173,7 @@ def test_refresh_node_peer_store_suppresses_clearnet_seed_by_default(tmp_path, m monkeypatch.setenv("MESH_DEFAULT_SYNC_PEERS", "") monkeypatch.delenv("MESH_INFONET_ALLOW_CLEARNET_SYNC", raising=False) monkeypatch.setenv("MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY", "") + monkeypatch.setenv("MESH_INFONET_FLEET_JOIN_DISABLED", "true") get_settings.cache_clear() try: @@ -184,7 +187,7 @@ def test_refresh_node_peer_store_suppresses_clearnet_seed_by_default(tmp_path, m assert snapshot["skipped_clearnet_peer_count"] == 1 assert snapshot["bootstrap_peer_count"] == 0 assert snapshot["sync_peer_count"] == 0 - assert "no clearnet sync fallback" in snapshot["last_bootstrap_error"] + assert snapshot["last_bootstrap_error"] assert store.records_for_bucket("bootstrap") == [] assert store.records_for_bucket("sync") == [] diff --git a/docker-compose.yml b/docker-compose.yml index 67976ed..29bfebf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,9 +29,13 @@ services: - CORS_ORIGINS=${CORS_ORIGINS:-} # Private Infonet bootstrap seeds. Seeds are discovery hints, not fixed roots. - MESH_BOOTSTRAP_SEED_PEERS=${MESH_BOOTSTRAP_SEED_PEERS:-http://gqpbunqbgtkcqilvclm3xrkt3zowjyl3s62kkktvojgvxzizamvbrqid.onion:8000} + - MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY=${MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY:-ul1d0kj/ODPIp0OhHzX8eLAVXzJ3CVvzW1vn2IC6q3I=} + - MESH_INFONET_FLEET_JOIN=${MESH_INFONET_FLEET_JOIN:-true} - MESH_DEFAULT_SYNC_PEERS=${MESH_DEFAULT_SYNC_PEERS:-} - - MESH_SYNC_TIMEOUT_S=${MESH_SYNC_TIMEOUT_S:-5} + - MESH_SYNC_TIMEOUT_S=${MESH_SYNC_TIMEOUT_S:-45} - MESH_RELAY_PUSH_TIMEOUT_S=${MESH_RELAY_PUSH_TIMEOUT_S:-45} + - MESH_SYNC_MAX_PEERS_PER_CYCLE=${MESH_SYNC_MAX_PEERS_PER_CYCLE:-5} + - MESH_SWARM_MANIFEST_PULL_INTERVAL_S=${MESH_SWARM_MANIFEST_PULL_INTERVAL_S:-300} # Explicitly opt into HTTPS/IP-based peer sync. Default remains private transports only. - MESH_INFONET_ALLOW_CLEARNET_SYNC=${MESH_INFONET_ALLOW_CLEARNET_SYNC:-false} # Tor/Arti SOCKS transport for private .onion Infonet sync. diff --git a/frontend/src/components/TopRightControls.tsx b/frontend/src/components/TopRightControls.tsx index 62ab864..0b97c62 100644 --- a/frontend/src/components/TopRightControls.tsx +++ b/frontend/src/components/TopRightControls.tsx @@ -35,6 +35,7 @@ import { purgeBrowserContactGraph, purgeBrowserSigningMaterial, setSecureModeCac import { purgeBrowserDmState } from '@/mesh/meshDmWorkerClient'; import { fetchInfonetNodeStatusSnapshot, + joinInfonetSwarm, startTorHiddenService, type InfonetNodeStatusSnapshot, } from '@/mesh/controlPlaneStatusClient'; @@ -109,7 +110,7 @@ export default function TopRightControls({ const timeoutRef = useRef | null>(null); const [launcherOpen, setLauncherOpen] = useState(false); const [nodeStep, setNodeStep] = useState<'prompt' | 'terms' | 'activating' | 'disable'>('prompt'); - const [activatingPhase, setActivatingPhase] = useState<'keys' | 'peers' | 'sync' | 'done'>('keys'); + const [activatingPhase, setActivatingPhase] = useState<'keys' | 'peers' | 'swarm' | 'sync' | 'done'>('keys'); const activatingPollRef = useRef | null>(null); const activatingTimeoutRef = useRef | null>(null); const [activatingTimedOut, setActivatingTimedOut] = useState(false); @@ -299,8 +300,11 @@ export default function TopRightControls({ await refreshNodeStatus(); if (enabled) { - // Start fast-polling for sync progress + setActivatingPhase('swarm'); + await joinInfonetSwarm().catch(() => null); + await refreshNodeStatus(); setActivatingPhase('sync'); + // Start fast-polling for sync progress stopActivatingPolls(); activatingPollRef.current = setInterval(async () => { try { @@ -789,7 +793,14 @@ export default function TopRightControls({
{syncOutcome.toLowerCase()} {(nodeStatus?.total_events ?? 0) > 0 && {nodeStatus?.total_events} {t('node.events')}} - {(nodeStatus?.bootstrap?.sync_peer_count ?? 0) > 0 && {nodeStatus?.bootstrap?.sync_peer_count} {t('node.peers')}} + {(nodeStatus?.bootstrap?.sync_peer_count ?? 0) > 0 && ( + + {nodeStatus?.bootstrap?.sync_peer_count} {t('node.peers')} + {(nodeStatus?.bootstrap?.swarm_sync_peer_count ?? 0) > 0 + ? ` (${nodeStatus?.bootstrap?.swarm_sync_peer_count} swarm)` + : ''} + + )}
{t('node.keepSyncing')} @@ -855,9 +866,32 @@ export default function TopRightControls({ : t('node.peersReady')}
+ {/* Step: Register with swarm */} +
+ {activatingPhase === 'keys' || activatingPhase === 'peers' ? ( + + ) : activatingPhase === 'swarm' ? ( + + ) : ( + + )} + + {activatingPhase === 'swarm' + ? 'Registering with swarm...' + : activatingPhase === 'keys' || activatingPhase === 'peers' + ? 'Join private Infonet swarm' + : `Swarm ready${(nodeStatus?.bootstrap?.swarm_sync_peer_count ?? 0) > 0 + ? ` (${nodeStatus?.bootstrap?.swarm_sync_peer_count} discovered)` + : ''}`} + +
{/* Step: Sync chain */}
- {(activatingPhase === 'keys' || activatingPhase === 'peers') ? ( + {(activatingPhase === 'keys' || activatingPhase === 'peers' || activatingPhase === 'swarm') ? ( ) : activatingPhase === 'sync' ? ( @@ -865,7 +899,7 @@ export default function TopRightControls({ )} diff --git a/frontend/src/mesh/controlPlaneStatusClient.ts b/frontend/src/mesh/controlPlaneStatusClient.ts index 8ad40e3..837aacf 100644 --- a/frontend/src/mesh/controlPlaneStatusClient.ts +++ b/frontend/src/mesh/controlPlaneStatusClient.ts @@ -249,12 +249,41 @@ export async function startTorHiddenService(): Promise }); } -/** Warm Tor/Arti and (re)enable the participant node so Infonet seed sync can run. */ +export interface InfonetSwarmJoinSnapshot { + ok?: boolean; + detail?: string; + announce?: { + ok?: boolean; + peer_url?: string; + skipped?: boolean; + results?: Array<{ seed_peer_url?: string; ok?: boolean; status_code?: number }>; + }; + manifest_pull?: { + ok?: boolean; + peer_count?: number; + merged_peer_count?: number; + seed_peer_url?: string; + detail?: string; + }; +} + +/** Register with the fleet seed and pull the signed peer manifest. */ +export async function joinInfonetSwarm(): Promise { + const result = await controlPlaneJson('/api/mesh/infonet/swarm/join', { + method: 'POST', + requireAdminSession: false, + }); + invalidateInfonetNodeStatusCache(); + return result; +} + +/** Warm Tor/Arti, enable the node, and join the private Infonet swarm. */ export async function ensureInfonetParticipantNodeReady(): Promise { if (!getNodeIdentity()) { await generateNodeKeys().catch(() => null); } await startTorHiddenService().catch(() => null); await setInfonetNodeEnabled(true); + await joinInfonetSwarm().catch(() => null); await fetchInfonetNodeStatusSnapshot(true).catch(() => null); } diff --git a/meshnode.bat b/meshnode.bat index 9fc0cf4..f3cb0e6 100644 --- a/meshnode.bat +++ b/meshnode.bat @@ -105,12 +105,19 @@ set MESH_ARTI_ENABLED=true set MESH_DM_HASHCHAIN_SPOOL_LIMIT=2 set MESH_DM_HASHCHAIN_SPOOL_TTL_S=3600 if "%MESH_BOOTSTRAP_SEED_PEERS%"=="" set MESH_BOOTSTRAP_SEED_PEERS=http://gqpbunqbgtkcqilvclm3xrkt3zowjyl3s62kkktvojgvxzizamvbrqid.onion:8000 +if "%MESH_INFONET_FLEET_JOIN%"=="" set MESH_INFONET_FLEET_JOIN=true +if "%MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY%"=="" set MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY=ul1d0kj/ODPIp0OhHzX8eLAVXzJ3CVvzW1vn2IC6q3I= +if "%MESH_SYNC_TIMEOUT_S%"=="" set MESH_SYNC_TIMEOUT_S=45 +if "%MESH_RELAY_PUSH_TIMEOUT_S%"=="" set MESH_RELAY_PUSH_TIMEOUT_S=45 +if "%MESH_SYNC_MAX_PEERS_PER_CYCLE%"=="" set MESH_SYNC_MAX_PEERS_PER_CYCLE=5 +if "%MESH_SWARM_MANIFEST_PULL_INTERVAL_S%"=="" set MESH_SWARM_MANIFEST_PULL_INTERVAL_S=300 echo. echo =================================================== echo Mesh node starting on port 8000 echo Mode: MESH_ONLY (no data feeds) echo Bootstrap: %MESH_BOOTSTRAP_SEED_PEERS% +echo Swarm: announce + signed manifest pull (fleet join) echo Press Ctrl+C to stop echo =================================================== echo. diff --git a/meshnode.sh b/meshnode.sh index f7966c1..6b250a0 100644 --- a/meshnode.sh +++ b/meshnode.sh @@ -60,12 +60,19 @@ export MESH_ARTI_ENABLED=true export MESH_DM_HASHCHAIN_SPOOL_LIMIT=2 export MESH_DM_HASHCHAIN_SPOOL_TTL_S=3600 export MESH_BOOTSTRAP_SEED_PEERS="${MESH_BOOTSTRAP_SEED_PEERS:-http://gqpbunqbgtkcqilvclm3xrkt3zowjyl3s62kkktvojgvxzizamvbrqid.onion:8000}" +export MESH_INFONET_FLEET_JOIN="${MESH_INFONET_FLEET_JOIN:-true}" +export MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY="${MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY:-ul1d0kj/ODPIp0OhHzX8eLAVXzJ3CVvzW1vn2IC6q3I=}" +export MESH_SYNC_TIMEOUT_S="${MESH_SYNC_TIMEOUT_S:-45}" +export MESH_RELAY_PUSH_TIMEOUT_S="${MESH_RELAY_PUSH_TIMEOUT_S:-45}" +export MESH_SYNC_MAX_PEERS_PER_CYCLE="${MESH_SYNC_MAX_PEERS_PER_CYCLE:-5}" +export MESH_SWARM_MANIFEST_PULL_INTERVAL_S="${MESH_SWARM_MANIFEST_PULL_INTERVAL_S:-300}" echo "" echo "===================================================" echo " Mesh node starting on port 8000" echo " Mode: MESH_ONLY (no data feeds)" echo " Bootstrap: ${MESH_BOOTSTRAP_SEED_PEERS}" +echo " Swarm: announce + signed manifest pull (fleet join)" echo " Press Ctrl+C to stop" echo "===================================================" echo ""