Enable zero-config Infonet fleet join for all participant nodes.

Ship sb-testnet fleet defaults, swarm/join API, NODE launcher registration step, and meshnode script defaults so users discover peers via the signed seed manifest without manual peer lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-06-11 10:25:48 -06:00
parent 776c89bfcf
commit df76f6f147
14 changed files with 247 additions and 17 deletions
+4 -1
View File
@@ -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)
+31 -1
View File
@@ -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):
+7 -1
View File
@@ -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
@@ -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 ""))
+3 -3
View File
@@ -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:
@@ -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 []
+8 -2
View File
@@ -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]]:
@@ -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()
@@ -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") == []
+5 -1
View File
@@ -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.
+39 -5
View File
@@ -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<ReturnType<typeof setTimeout> | 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<ReturnType<typeof setInterval> | null>(null);
const activatingTimeoutRef = useRef<ReturnType<typeof setTimeout> | 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({
<div className="mt-2 text-[9px] text-cyan-200/70 normal-case tracking-normal flex flex-wrap gap-x-3">
<span>{syncOutcome.toLowerCase()}</span>
{(nodeStatus?.total_events ?? 0) > 0 && <span>{nodeStatus?.total_events} {t('node.events')}</span>}
{(nodeStatus?.bootstrap?.sync_peer_count ?? 0) > 0 && <span>{nodeStatus?.bootstrap?.sync_peer_count} {t('node.peers')}</span>}
{(nodeStatus?.bootstrap?.sync_peer_count ?? 0) > 0 && (
<span>
{nodeStatus?.bootstrap?.sync_peer_count} {t('node.peers')}
{(nodeStatus?.bootstrap?.swarm_sync_peer_count ?? 0) > 0
? ` (${nodeStatus?.bootstrap?.swarm_sync_peer_count} swarm)`
: ''}
</span>
)}
</div>
<div className="mt-3 text-[11px] text-[var(--text-muted)] normal-case tracking-normal leading-[1.8]">
{t('node.keepSyncing')}
@@ -855,9 +866,32 @@ export default function TopRightControls({
: t('node.peersReady')}
</span>
</div>
{/* Step: Register with swarm */}
<div className="flex items-center gap-3 text-[10px] font-mono">
{activatingPhase === 'keys' || activatingPhase === 'peers' ? (
<span className="w-[11px] h-[11px] shrink-0" />
) : activatingPhase === 'swarm' ? (
<RefreshCw size={11} className="text-cyan-400 animate-spin shrink-0" />
) : (
<CheckCircle2 size={11} className="text-green-400 shrink-0" />
)}
<span className={
activatingPhase === 'keys' || activatingPhase === 'peers' ? 'text-[var(--text-muted)]'
: activatingPhase === 'swarm' ? 'text-cyan-300'
: 'text-green-300'
}>
{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)`
: ''}`}
</span>
</div>
{/* Step: Sync chain */}
<div className="flex items-center gap-3 text-[10px] font-mono">
{(activatingPhase === 'keys' || activatingPhase === 'peers') ? (
{(activatingPhase === 'keys' || activatingPhase === 'peers' || activatingPhase === 'swarm') ? (
<span className="w-[11px] h-[11px] shrink-0" />
) : activatingPhase === 'sync' ? (
<RefreshCw size={11} className="text-cyan-400 animate-spin shrink-0" />
@@ -865,7 +899,7 @@ export default function TopRightControls({
<CheckCircle2 size={11} className="text-green-400 shrink-0" />
)}
<span className={
(activatingPhase === 'keys' || activatingPhase === 'peers') ? 'text-[var(--text-muted)]'
(activatingPhase === 'keys' || activatingPhase === 'peers' || activatingPhase === 'swarm') ? 'text-[var(--text-muted)]'
: activatingPhase === 'sync' ? 'text-cyan-300'
: 'text-green-300'
}>
+30 -1
View File
@@ -249,12 +249,41 @@ export async function startTorHiddenService(): Promise<TorHiddenServiceSnapshot>
});
}
/** 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<InfonetSwarmJoinSnapshot> {
const result = await controlPlaneJson<InfonetSwarmJoinSnapshot>('/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<void> {
if (!getNodeIdentity()) {
await generateNodeKeys().catch(() => null);
}
await startTorHiddenService().catch(() => null);
await setInfonetNodeEnabled(true);
await joinInfonetSwarm().catch(() => null);
await fetchInfonetNodeStatusSnapshot(true).catch(() => null);
}
+7
View File
@@ -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.
+7
View File
@@ -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 ""