mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-10 20:50:25 +02:00
Fix encoding corruption from AISHub secret/config wiring.
Restore main.py and config.py to clean UTF-8 and re-apply only the AISHUB_USERNAME additions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+81
-81
@@ -1,4 +1,4 @@
|
||||
import os
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
@@ -139,8 +139,8 @@ def _check_explicit_scoped_auth_local(
|
||||
if not admin_key and not scoped_tokens:
|
||||
if _allow_insecure_admin() or (_debug_mode_enabled() and host == "test"):
|
||||
return True, "ok", "debug_override"
|
||||
return False, "Forbidden ΓÇö admin key not configured", ""
|
||||
return False, "Forbidden ΓÇö invalid or missing admin key", ""
|
||||
return False, "Forbidden — admin key not configured", ""
|
||||
return False, "Forbidden — invalid or missing admin key", ""
|
||||
|
||||
|
||||
def _gate_privileged_access_status_snapshot_local() -> dict[str, Any]:
|
||||
@@ -164,7 +164,7 @@ def _gate_privileged_access_status_snapshot_local() -> dict[str, Any]:
|
||||
# Docker Swarm Secrets support
|
||||
# For each VAR below, if VAR_FILE is set (e.g. AIS_API_KEY_FILE=/run/secrets/AIS_API_KEY),
|
||||
# the file is read and its trimmed content is placed into VAR.
|
||||
# This MUST run before service imports — modules read os.environ at import time.
|
||||
# This MUST run before service imports — modules read os.environ at import time.
|
||||
# ---------------------------------------------------------------------------
|
||||
_SECRET_VARS = [
|
||||
"AIS_API_KEY",
|
||||
@@ -213,7 +213,7 @@ if not _MESH_ONLY:
|
||||
from services.ais_stream import start_ais_stream, stop_ais_stream
|
||||
from services.carrier_tracker import start_carrier_tracker, stop_carrier_tracker
|
||||
else:
|
||||
# Lean mesh/wormhole process ΓÇö avoid importing the OSINT fetcher graph.
|
||||
# Lean mesh/wormhole process — avoid importing the OSINT fetcher graph.
|
||||
def start_scheduler(*_a, **_k): # type: ignore[misc]
|
||||
return None
|
||||
|
||||
@@ -1643,7 +1643,7 @@ def _peer_sync_response(peer_url: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
# HTTP 429 must be surfaced as a typed exception carrying the
|
||||
# Retry-After value, so finish_sync can honor it and stop hammering
|
||||
# the upstream. Pre-fix this path just stringified the status into
|
||||
# a ValueError, which finish_sync then ignored ΓÇö keeping the
|
||||
# a ValueError, which finish_sync then ignored — keeping the
|
||||
# upstream's rate-limit bucket full indefinitely.
|
||||
if response.status_code == 429:
|
||||
from services.mesh.mesh_infonet_sync_support import (
|
||||
@@ -1680,8 +1680,8 @@ def _hydrate_gate_store_from_chain(events: list[dict]) -> int:
|
||||
"""Copy any gate_message chain events into the local gate_store for read/decrypt.
|
||||
|
||||
Only events that are resident in the local infonet (accepted or already
|
||||
present) are hydrated. The canonical infonet-resident event is used —
|
||||
never the raw batch event — so a forged batch entry carrying a valid
|
||||
present) are hydrated. The canonical infonet-resident event is used —
|
||||
never the raw batch event — so a forged batch entry carrying a valid
|
||||
event_id but attacker-chosen payload cannot pollute gate_store.
|
||||
"""
|
||||
import copy
|
||||
@@ -1766,7 +1766,7 @@ def _sync_from_peer(
|
||||
|
||||
Returns ``(ok, error, forked, retry_after_s)``. The fourth tuple
|
||||
element is non-zero only when the peer responded with HTTP 429
|
||||
and supplied a parseable ``Retry-After`` header ΓÇö see the typed
|
||||
and supplied a parseable ``Retry-After`` header — see the typed
|
||||
``PeerSyncRateLimited`` exception in mesh_infonet_sync_support.py.
|
||||
Callers should pass that value to ``finish_sync(retry_after_s=...)``
|
||||
so the next attempt actually waits.
|
||||
@@ -1909,7 +1909,7 @@ def _run_public_sync_cycle() -> SyncWorkerState:
|
||||
except PeerSyncHTTPError as exc:
|
||||
# _sync_from_peer catches PeerSyncRateLimited internally (4-tuple
|
||||
# path for 429 with Retry-After). Other non-200 statuses surface
|
||||
# here as PeerSyncHTTPError ΓÇö pull retry_after_s + status off it
|
||||
# here as PeerSyncHTTPError — pull retry_after_s + status off it
|
||||
# so the cooldown calculation below can honor server hints even
|
||||
# for non-429 throttling responses.
|
||||
ok = False
|
||||
@@ -1974,7 +1974,7 @@ def _run_public_sync_cycle() -> SyncWorkerState:
|
||||
failure_backoff_s=failure_backoff_s,
|
||||
# 429 retry-storm fix: when the peer returned HTTP 429 with
|
||||
# a Retry-After header, finish_sync uses max(exponential,
|
||||
# retry_after) for next_sync_due_at ΓÇö so we actually wait
|
||||
# retry_after) for next_sync_due_at — so we actually wait
|
||||
# the time the upstream asked for instead of hammering
|
||||
# every 60s and keeping its rate-limit bucket full forever.
|
||||
retry_after_s=retry_after_s,
|
||||
@@ -2148,13 +2148,13 @@ def _start_infonet_node_runtime(reason: str = "startup") -> None:
|
||||
logger.warning(f"Node bootstrap runtime failed to initialize: {e}")
|
||||
|
||||
|
||||
# ─── Background HTTP Peer Push Worker ────────────────────────────────────
|
||||
# ─── Background HTTP Peer Push Worker ────────────────────────────────────
|
||||
# Runs alongside the sync loop. Every PUSH_INTERVAL seconds, batches new
|
||||
# Infonet events and sends them via HMAC-authenticated POST to push peers.
|
||||
|
||||
_PEER_PUSH_INTERVAL_S = 10
|
||||
_PEER_PUSH_BATCH_SIZE = 50
|
||||
_peer_push_last_index: dict[str, int] = {} # peer_url → last pushed event index
|
||||
_peer_push_last_index: dict[str, int] = {} # peer_url → last pushed event index
|
||||
_INFONET_SYNC_RATE_LIMIT = "600/minute"
|
||||
|
||||
|
||||
@@ -2174,7 +2174,7 @@ def _http_peer_push_loop() -> None:
|
||||
# legacy global MESH_PEER_PUSH_SECRET path and the per-peer
|
||||
# MESH_PEER_SECRETS map. The per-peer skip happens below
|
||||
# ("if not peer_key: continue"), so we don't gate the whole
|
||||
# loop on the global secret being set ΓÇö an install that only
|
||||
# loop on the global secret being set — an install that only
|
||||
# configures per-peer secrets is now valid.
|
||||
|
||||
peers = _filter_infonet_peer_urls(authenticated_push_peer_urls())
|
||||
@@ -2234,7 +2234,7 @@ def _http_peer_push_loop() -> None:
|
||||
_peer_push_last_index[normalized] = last_idx + len(batch)
|
||||
logger.info(
|
||||
f"Pushed {len(batch)} event(s) to {normalized[:40]} "
|
||||
f"(idx {last_idx}→{last_idx + len(batch)})"
|
||||
f"(idx {last_idx}→{last_idx + len(batch)})"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Peer push to {normalized[:40]} returned {resp.status_code}")
|
||||
@@ -2262,13 +2262,13 @@ def _swarm_manifest_pull_loop() -> None:
|
||||
_NODE_SYNC_STOP.wait(max(30, interval_s))
|
||||
|
||||
|
||||
# ─── Background Gate Message Pull Worker ─────────────────────────────────
|
||||
# ─── Background Gate Message Pull Worker ─────────────────────────────────
|
||||
# Periodically pulls gate events from relay peers that this node is missing.
|
||||
# Complements the push loop: push sends OUR events to peers, pull fetches
|
||||
# THEIR events from peers (needed when this node is behind NAT).
|
||||
|
||||
_GATE_PULL_INTERVAL_S = 10
|
||||
_gate_pull_last_count: dict[str, dict[str, int]] = {} # peer → {gate_id → known count}
|
||||
_gate_pull_last_count: dict[str, dict[str, int]] = {} # peer → {gate_id → known count}
|
||||
|
||||
|
||||
def _http_gate_pull_loop() -> None:
|
||||
@@ -2405,9 +2405,9 @@ def _http_gate_pull_loop() -> None:
|
||||
|
||||
|
||||
|
||||
# ─── Background Gate Message Push Worker ─────────────────────────────────
|
||||
# ─── Background Gate Message Push Worker ─────────────────────────────────
|
||||
|
||||
_gate_push_last_count: dict[str, dict[str, int]] = {} # peer → {gate_id → count}
|
||||
_gate_push_last_count: dict[str, dict[str, int]] = {} # peer → {gate_id → count}
|
||||
|
||||
|
||||
def _http_gate_push_loop() -> None:
|
||||
@@ -2750,9 +2750,9 @@ async def lifespan(app: FastAPI):
|
||||
validate_env(strict=not _MESH_ONLY)
|
||||
|
||||
if _MESH_ONLY:
|
||||
logger.info("MESH_ONLY enabled — skipping global data fetchers/schedulers.")
|
||||
logger.info("MESH_ONLY enabled — skipping global data fetchers/schedulers.")
|
||||
else:
|
||||
# Start AIS stream first — it loads the disk cache (instant ships) then
|
||||
# Start AIS stream first — it loads the disk cache (instant ships) then
|
||||
# begins accumulating live vessel data via WebSocket in the background.
|
||||
start_ais_stream()
|
||||
|
||||
@@ -2760,7 +2760,7 @@ async def lifespan(app: FastAPI):
|
||||
# in _scheduler_loop, so we do NOT call it again in the preload thread.
|
||||
start_carrier_tracker()
|
||||
|
||||
# Start SIGINT grid eagerly — APRS-IS TCP + Meshtastic MQTT connections
|
||||
# Start SIGINT grid eagerly — APRS-IS TCP + Meshtastic MQTT connections
|
||||
# take a few seconds to handshake and start receiving packets. By starting
|
||||
# now, the bridges are already accumulating signals by the time the first
|
||||
# fetch_sigint() reads them during the preload cycle.
|
||||
@@ -2869,7 +2869,7 @@ async def lifespan(app: FastAPI):
|
||||
delay_s = float(os.environ.get("SHADOWBROKER_STARTUP_PRELOAD_DELAY_S", "2.0") or 0)
|
||||
if delay_s > 0:
|
||||
time.sleep(delay_s)
|
||||
logger.info("=== PRELOADING DATA (background — server already accepting requests) ===")
|
||||
logger.info("=== PRELOADING DATA (background — server already accepting requests) ===")
|
||||
try:
|
||||
update_all_data(startup_mode=True)
|
||||
logger.info("=== PRELOAD COMPLETE ===")
|
||||
@@ -2883,7 +2883,7 @@ async def lifespan(app: FastAPI):
|
||||
try:
|
||||
from services.tor_hidden_service import tor_service, HOSTNAME_PATH
|
||||
if HOSTNAME_PATH.exists():
|
||||
logger.info("Previous Tor hidden service detected — auto-restarting...")
|
||||
logger.info("Previous Tor hidden service detected — auto-restarting...")
|
||||
threading.Thread(
|
||||
target=tor_service.start, daemon=True
|
||||
).start()
|
||||
@@ -3196,9 +3196,9 @@ def _trusted_gate_reply_to(event: dict) -> str:
|
||||
def _derive_anon_handle(node_id: str, gate_id: str) -> str:
|
||||
"""Derive a stable per-session, per-gate anonymous display handle.
|
||||
|
||||
Same node_id + same gate → same handle for every message that session
|
||||
Same node_id + same gate → same handle for every message that session
|
||||
posts (lets other members follow a conversation thread). Different
|
||||
session (anon re-enters → new node_id) → new handle. Different gate →
|
||||
session (anon re-enters → new node_id) → new handle. Different gate →
|
||||
different handle for the same session (prevents cross-gate linking).
|
||||
Not reversible: the handle is HMAC-SHA256(node_id, gate_id) truncated
|
||||
to 4 hex chars (~16 bits), which is enough to tell sessions apart in
|
||||
@@ -3239,7 +3239,7 @@ def _strip_gate_identity_member(event: dict, *, envelope_policy: str = "envelope
|
||||
"transport_lock": str(payload.get("transport_lock", "") or ""),
|
||||
# gate_envelope is AES-256-GCM ciphertext encrypted under the gate's
|
||||
# domain key (gate_secret). Only members who hold the gate_secret
|
||||
# can decrypt it ΓÇö so exposing the ciphertext itself to members is
|
||||
# can decrypt it — so exposing the ciphertext itself to members is
|
||||
# safe, and it's REQUIRED for the envelope_always decrypt path that
|
||||
# gives members durable re-readable history. envelope_hash is the
|
||||
# cryptographic binding (SHA-256 of gate_envelope) the decrypt path
|
||||
@@ -3302,7 +3302,7 @@ def _strip_gate_identity_privileged(event: dict) -> dict:
|
||||
|
||||
|
||||
def _strip_gate_identity(event: dict) -> dict:
|
||||
"""Legacy alias — defaults to member (narrowed) view."""
|
||||
"""Legacy alias — defaults to member (narrowed) view."""
|
||||
return _strip_gate_identity_member(event)
|
||||
|
||||
|
||||
@@ -3484,7 +3484,7 @@ def _verify_gate_access(request: Request, gate_id: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
# ── Non-hostile transport auto-upgrade ────────────────────────────────
|
||||
# ── Non-hostile transport auto-upgrade ────────────────────────────────
|
||||
#
|
||||
# The mesh/wormhole middleware can try to bring the wormhole supervisor
|
||||
# up in the background when a user hits a tier-gated route on a weak
|
||||
@@ -3751,8 +3751,8 @@ async def enforce_high_privacy_mesh(request: Request, call_next):
|
||||
# background and return an ok:True "preparing" response
|
||||
# (202 Accepted) so the client shows a spinner rather
|
||||
# than an approval dialog. The request itself is NOT
|
||||
# forwarded to the handler ΓÇö the tier is too low for the
|
||||
# route's required privacy ΓÇö but the client can poll and
|
||||
# forwarded to the handler — the tier is too low for the
|
||||
# route's required privacy — but the client can poll and
|
||||
# retry transparently once the lane warms up.
|
||||
try:
|
||||
upgraded = await _try_transparent_transport_upgrade()
|
||||
@@ -3785,7 +3785,7 @@ async def enforce_high_privacy_mesh(request: Request, call_next):
|
||||
data = read_wormhole_settings()
|
||||
# Tor-style: if the user selected high privacy but Wormhole
|
||||
# isn't enabled yet, just turn it on and kick off warmup.
|
||||
# Don't block the request on the upgrade ΓÇö the transport
|
||||
# Don't block the request on the upgrade — the transport
|
||||
# manager will converge in the background.
|
||||
if (
|
||||
private_mesh_path
|
||||
@@ -3808,7 +3808,7 @@ async def enforce_high_privacy_mesh(request: Request, call_next):
|
||||
or _is_anonymous_dm_action_path(path, request.method)
|
||||
or _is_anonymous_wormhole_gate_admin_path(path, request.method)
|
||||
):
|
||||
# Tor-style: anonymous mode is on → do whatever is required for
|
||||
# Tor-style: anonymous mode is on → do whatever is required for
|
||||
# it to function. Auto-enable Wormhole if off, and schedule
|
||||
# hidden-transport warmup WITHOUT blocking this request. The
|
||||
# transport manager converges in the background; the user sees
|
||||
@@ -3972,7 +3972,7 @@ def _queue_viirs_change_refresh() -> None:
|
||||
@limiter.limit("60/minute")
|
||||
async def update_viewport(vp: ViewportUpdate, request: Request): # noqa: ARG001
|
||||
"""Receive frontend map bounds. AIS stream stays global so open-ocean
|
||||
vessels are never dropped — the frontend worker handles viewport culling."""
|
||||
vessels are never dropped — the frontend worker handles viewport culling."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -4140,7 +4140,7 @@ async def nearest_sdr(
|
||||
return find_nearest_kiwisdr(lat, lng, kiwisdr_data)
|
||||
|
||||
|
||||
# ─── Per-Identity Throttle State ──────────────────────────────────────────
|
||||
# ─── Per-Identity Throttle State ──────────────────────────────────────────
|
||||
# In-memory: {node_id: {"last_send": timestamp, "daily_count": int, "daily_reset": timestamp}}
|
||||
# Bounded to 10000 entries with 24hr TTL to prevent unbounded memory growth
|
||||
_node_throttle: TTLCache = TTLCache(maxsize=10000, ttl=86400)
|
||||
@@ -4224,7 +4224,7 @@ def _check_throttle(
|
||||
|
||||
|
||||
def _check_gate_post_cooldown(sender_id: str, gate_id: str) -> tuple[bool, str]:
|
||||
"""Check cooldown — does NOT record it. Call _record_gate_post_cooldown() after success."""
|
||||
"""Check cooldown — does NOT record it. Call _record_gate_post_cooldown() after success."""
|
||||
gate_key = str(gate_id or "").strip().lower()
|
||||
sender_key = str(sender_id or "").strip()
|
||||
if not gate_key or not sender_key:
|
||||
@@ -4426,7 +4426,7 @@ def _prepared_signed_write(request: Request):
|
||||
@limiter.limit("10/minute")
|
||||
@requires_signed_write(kind=SignedWriteKind.MESH_SEND)
|
||||
async def mesh_send(request: Request):
|
||||
"""Unified mesh message endpoint — auto-routes via optimal transport.
|
||||
"""Unified mesh message endpoint — auto-routes via optimal transport.
|
||||
|
||||
Body: { destination, message, priority?, channel?, node_id?, credentials? }
|
||||
The router picks APRS, Meshtastic, or Internet based on gate logic.
|
||||
@@ -4438,7 +4438,7 @@ async def mesh_send(request: Request):
|
||||
if not destination or not message:
|
||||
return {"ok": False, "detail": "Missing required fields: destination, message"}
|
||||
|
||||
# ─── Byte limit enforcement ───────────────────────────────────
|
||||
# ─── Byte limit enforcement ───────────────────────────────────
|
||||
payload_bytes = len(message.encode("utf-8"))
|
||||
payload_type = body.get("payload_type", "text")
|
||||
max_bytes = _BYTE_LIMITS.get(payload_type, 200)
|
||||
@@ -4448,7 +4448,7 @@ async def mesh_send(request: Request):
|
||||
"detail": f"Message too long ({payload_bytes} bytes). Maximum: {max_bytes} bytes for {payload_type} messages.",
|
||||
}
|
||||
|
||||
# ─── Signature verification & node registration ──────────────
|
||||
# ─── Signature verification & node registration ──────────────
|
||||
node_id = body.get("node_id", body.get("sender_id", "anonymous"))
|
||||
public_key = body.get("public_key", "")
|
||||
public_key_algo = body.get("public_key_algo", "")
|
||||
@@ -4471,9 +4471,9 @@ async def mesh_send(request: Request):
|
||||
|
||||
reputation_ledger.register_node(node_id, public_key, public_key_algo)
|
||||
except Exception:
|
||||
pass # Non-critical — don't block sends if reputation module fails
|
||||
pass # Non-critical — don't block sends if reputation module fails
|
||||
|
||||
# ─── Per-identity throttle ────────────────────────────────────
|
||||
# ─── Per-identity throttle ────────────────────────────────────
|
||||
priority_str = signed_payload["priority"]
|
||||
transport_lock = str(body.get("transport_lock", "") or "").lower()
|
||||
throttle_ok, throttle_reason = _check_throttle(node_id, priority_str, transport_lock)
|
||||
@@ -4496,7 +4496,7 @@ async def mesh_send(request: Request):
|
||||
}
|
||||
priority = priority_map.get(priority_str, Priority.NORMAL)
|
||||
|
||||
# ─── C-1 fix: compute trust_tier from Wormhole state ───────
|
||||
# ─── C-1 fix: compute trust_tier from Wormhole state ───────
|
||||
from services.wormhole_supervisor import get_transport_tier
|
||||
|
||||
computed_tier = get_transport_tier()
|
||||
@@ -4512,7 +4512,7 @@ async def mesh_send(request: Request):
|
||||
)
|
||||
|
||||
credentials = body.get("credentials", {})
|
||||
# ─── C-2 fix: enforce tier before transport_lock dispatch ──
|
||||
# ─── C-2 fix: enforce tier before transport_lock dispatch ──
|
||||
private_tier = str(envelope.trust_tier or "").startswith("private_")
|
||||
if transport_lock == "meshtastic":
|
||||
if private_tier:
|
||||
@@ -4548,7 +4548,7 @@ async def mesh_send(request: Request):
|
||||
results = mesh_router.route(envelope, credentials)
|
||||
any_ok = any(r.ok for r in results)
|
||||
|
||||
# ─── Mirror to Meshtastic bridge feed ────────────────────────
|
||||
# ─── Mirror to Meshtastic bridge feed ────────────────────────
|
||||
# The MQTT broker won't echo our own publishes back to our subscriber, so
|
||||
# inject successfully-sent channel broadcasts into the bridge directly.
|
||||
# Node-targeted packets must not appear in the public channel feed.
|
||||
@@ -4720,14 +4720,14 @@ async def mesh_messages(
|
||||
@app.get("/api/mesh/channels")
|
||||
@limiter.limit("30/minute")
|
||||
async def mesh_channels(request: Request):
|
||||
"""Get Meshtastic channel population stats — nodes per region/channel."""
|
||||
"""Get Meshtastic channel population stats — nodes per region/channel."""
|
||||
stats = get_latest_data().get("mesh_channel_stats", {})
|
||||
return stats
|
||||
|
||||
|
||||
# ─── Reputation Endpoints ─────────────────────────────────────────────────
|
||||
# ─── Reputation Endpoints ─────────────────────────────────────────────────
|
||||
|
||||
# Cached root node_id — avoids 5 encrypted disk reads per vote.
|
||||
# Cached root node_id — avoids 5 encrypted disk reads per vote.
|
||||
_root_node_id_cache: dict[str, object] = {"value": None, "ts": 0.0}
|
||||
_ROOT_NODE_ID_TTL = 30.0 # seconds
|
||||
|
||||
@@ -4784,7 +4784,7 @@ async def mesh_vote(request: Request):
|
||||
vote_payload = {"target_id": target_id, "vote": vote, "gate": gate}
|
||||
|
||||
# Resolve stable local operator ID for duplicate-vote prevention.
|
||||
# Personas generate unique keypairs, so voter_id alone is insufficient —
|
||||
# Personas generate unique keypairs, so voter_id alone is insufficient —
|
||||
# use the root identity's node_id as a stable anchor so switching personas
|
||||
# doesn't let the same operator vote multiple times on the same post.
|
||||
stable_voter_id = voter_id
|
||||
@@ -5073,7 +5073,7 @@ async def mesh_identity_revoke(request: Request):
|
||||
return {"ok": True, "detail": "Identity revoked"}
|
||||
|
||||
|
||||
# ─── Gate Endpoints ───────────────────────────────────────────────────────
|
||||
# ─── Gate Endpoints ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/api/mesh/gate/create")
|
||||
@@ -5144,7 +5144,7 @@ async def gate_create(request: Request):
|
||||
@app.get("/api/mesh/gate/list")
|
||||
@limiter.limit("30/minute")
|
||||
async def gate_list(request: Request):
|
||||
"""List all known gates (public catalog — secrets are never included)."""
|
||||
"""List all known gates (public catalog — secrets are never included)."""
|
||||
from services.mesh.mesh_reputation import gate_manager
|
||||
|
||||
return {"gates": gate_manager.list_gates()}
|
||||
@@ -5247,7 +5247,7 @@ def _submit_gate_message_envelope(request: Request, gate_id: str, body: dict[str
|
||||
payload_ok, payload_reason = validate_event_payload("gate_message", gate_payload)
|
||||
if not payload_ok:
|
||||
return {"ok": False, "detail": payload_reason}
|
||||
# gate_envelope is not part of the signed payload — envelope_hash binds it.
|
||||
# gate_envelope is not part of the signed payload — envelope_hash binds it.
|
||||
# reply_to is signed for new compose flows; if only the legacy no-reply_to
|
||||
# signature verifies, strip it rather than accepting unauthenticated
|
||||
# threading metadata.
|
||||
@@ -5397,13 +5397,13 @@ def _submit_gate_message_envelope(request: Request, gate_id: str, body: dict[str
|
||||
)
|
||||
|
||||
|
||||
# ─── Infonet Endpoints ───────────────────────────────────────────────────
|
||||
# ─── Infonet Endpoints ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.get("/api/mesh/infonet/status")
|
||||
@limiter.limit("30/minute")
|
||||
async def infonet_status(request: Request, verify_signatures: bool = False):
|
||||
"""Get Infonet metadata — event counts, head hash, chain size."""
|
||||
"""Get Infonet metadata — event counts, head hash, chain size."""
|
||||
from services.mesh.mesh_hashchain import infonet
|
||||
from services.wormhole_supervisor import get_wormhole_state
|
||||
|
||||
@@ -5582,7 +5582,7 @@ async def mesh_metrics(request: Request):
|
||||
ok, detail = _check_scoped_auth(request, "mesh.audit")
|
||||
if not ok:
|
||||
if detail == "insufficient scope":
|
||||
raise HTTPException(status_code=403, detail="Forbidden — insufficient scope")
|
||||
raise HTTPException(status_code=403, detail="Forbidden — insufficient scope")
|
||||
raise HTTPException(status_code=403, detail=detail)
|
||||
return snapshot()
|
||||
|
||||
@@ -5982,7 +5982,7 @@ async def gate_peer_pull(request: Request):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Peer Management API — operator endpoints for adding / removing / listing
|
||||
# Peer Management API — operator endpoints for adding / removing / listing
|
||||
# peers without editing peer_store.json by hand.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -6039,7 +6039,7 @@ async def add_peer(request: Request):
|
||||
if not transport:
|
||||
transport = peer_transport_kind(peer_url)
|
||||
if not transport:
|
||||
return {"ok": False, "detail": "Cannot determine transport for peer_url — provide transport explicitly"}
|
||||
return {"ok": False, "detail": "Cannot determine transport for peer_url — provide transport explicitly"}
|
||||
|
||||
label = str(body.get("label", "") or "").strip()
|
||||
role = str(body.get("role", "") or "").strip().lower() or "relay"
|
||||
@@ -6416,7 +6416,7 @@ async def infonet_events_by_type(
|
||||
}
|
||||
|
||||
|
||||
# ─── Oracle Endpoints ─────────────────────────────────────────────────────
|
||||
# ─── Oracle Endpoints ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/api/mesh/oracle/predict")
|
||||
@@ -6426,8 +6426,8 @@ async def oracle_predict(request: Request):
|
||||
"""Place a prediction on a market outcome. FINAL decision.
|
||||
|
||||
Body: {node_id, market_title, side, stake_amount?: number}
|
||||
- stake_amount = 0 or omitted → FREE PICK (earn rep if correct)
|
||||
- stake_amount > 0 → STAKE REP (risk rep, split loser pool if correct)
|
||||
- stake_amount = 0 or omitted → FREE PICK (earn rep if correct)
|
||||
- stake_amount > 0 → STAKE REP (risk rep, split loser pool if correct)
|
||||
- side can be "yes"/"no" or an outcome name for multi-outcome markets
|
||||
"""
|
||||
from services.mesh.mesh_oracle import oracle_ledger
|
||||
@@ -6466,7 +6466,7 @@ async def oracle_predict(request: Request):
|
||||
if m.get("title", "").lower() == market_title.lower():
|
||||
matched = m
|
||||
break
|
||||
# Fuzzy fallback — partial match
|
||||
# Fuzzy fallback — partial match
|
||||
if not matched:
|
||||
for m in markets:
|
||||
if market_title.lower() in m.get("title", "").lower():
|
||||
@@ -6497,13 +6497,13 @@ async def oracle_predict(request: Request):
|
||||
probability = 100.0 - probability
|
||||
|
||||
if stake_amount > 0:
|
||||
# STAKED prediction — risk rep for bigger reward
|
||||
# STAKED prediction — risk rep for bigger reward
|
||||
ok, detail = oracle_ledger.place_market_stake(
|
||||
node_id, matched["title"], side, stake_amount, probability
|
||||
)
|
||||
mode = "staked"
|
||||
else:
|
||||
# FREE prediction — no rep risked
|
||||
# FREE prediction — no rep risked
|
||||
ok, detail = oracle_ledger.place_prediction(node_id, matched["title"], side, probability)
|
||||
mode = "free"
|
||||
|
||||
@@ -6726,7 +6726,7 @@ async def oracle_resolve(request: Request):
|
||||
@app.get("/api/mesh/oracle/consensus")
|
||||
@limiter.limit("30/minute")
|
||||
async def oracle_consensus(request: Request, market_title: str = ""):
|
||||
"""Get network consensus for a market — picks + staked rep per side."""
|
||||
"""Get network consensus for a market — picks + staked rep per side."""
|
||||
from services.mesh.mesh_oracle import oracle_ledger
|
||||
|
||||
if not market_title:
|
||||
@@ -6815,7 +6815,7 @@ async def oracle_stakes_for_message(request: Request, message_id: str):
|
||||
@app.get("/api/mesh/oracle/profile")
|
||||
@limiter.limit("30/minute")
|
||||
async def oracle_profile(request: Request, node_id: str = ""):
|
||||
"""Get full oracle profile — rep, prediction history, win rate, farming score."""
|
||||
"""Get full oracle profile — rep, prediction history, win rate, farming score."""
|
||||
from services.mesh.mesh_oracle import oracle_ledger
|
||||
|
||||
if not node_id:
|
||||
@@ -6853,7 +6853,7 @@ async def oracle_resolve_stakes(request: Request):
|
||||
return {"ok": True, "resolutions": resolutions, "count": len(resolutions)}
|
||||
|
||||
|
||||
# ─── Encrypted DM Relay (Dead Drop) ───────────────────────────────────────
|
||||
# ─── Encrypted DM Relay (Dead Drop) ───────────────────────────────────────
|
||||
|
||||
|
||||
def _secure_dm_enabled() -> bool:
|
||||
@@ -6887,7 +6887,7 @@ def _anonymous_dm_hidden_transport_requested() -> bool:
|
||||
is *ready* yet.
|
||||
|
||||
Use this (not the ``_enforced`` variant) for *protective* logic that must
|
||||
keep stated privacy intent honored during warmup ΓÇö e.g., skipping direct
|
||||
keep stated privacy intent honored during warmup — e.g., skipping direct
|
||||
RNS metadata lookups. ``_enforced`` is for claim/telemetry paths that
|
||||
report what is currently being honored.
|
||||
"""
|
||||
@@ -7165,7 +7165,7 @@ async def _dm_send_from_signed_request(request: Request):
|
||||
return {"ok": False, "detail": "DM timestamp is too far from current time"}
|
||||
if delivery_class not in ("request", "shared"):
|
||||
return {"ok": False, "detail": "delivery_class must be request or shared"}
|
||||
# Contact requests are the first-contact handshake ΓÇö do not require prior verification.
|
||||
# Contact requests are the first-contact handshake — do not require prior verification.
|
||||
if delivery_class == "shared":
|
||||
try:
|
||||
from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement
|
||||
@@ -7493,7 +7493,7 @@ async def _dm_count_secure_from_signed_request(request: Request):
|
||||
mailbox_keys = dm_relay.claim_mailbox_keys(agent_id, claims)
|
||||
relay_ids = dm_relay.claim_message_ids(agent_id, claims)
|
||||
direct_ids = set()
|
||||
# Rec #9: requested (not merely enforced) ΓÇö skip direct-lane count probe
|
||||
# Rec #9: requested (not merely enforced) — skip direct-lane count probe
|
||||
# as soon as anonymous mode is requested, even before ready converges.
|
||||
if not _anonymous_dm_hidden_transport_requested():
|
||||
try:
|
||||
@@ -7594,7 +7594,7 @@ async def dm_get_pubkey(
|
||||
if key_bundle is None:
|
||||
# Invite handles are minted on the owner's node. When a remote peer
|
||||
# pastes a short address, resolve it across the private fleet before
|
||||
# failing ΓÇö same path as prekey-bundle import.
|
||||
# failing — same path as prekey-bundle import.
|
||||
from services.mesh.mesh_wormhole_prekey import fetch_dm_prekey_bundle
|
||||
|
||||
preferred_lookup_peer = str(lookup_peer_url or "").strip().rstrip("/")
|
||||
@@ -8161,7 +8161,7 @@ async def debug_latest_data(request: Request):
|
||||
return list(get_latest_data().keys())
|
||||
|
||||
|
||||
# ── CCTV media proxy (bypass CORS for cross-origin video/image streams) ───
|
||||
# ── CCTV media proxy (bypass CORS for cross-origin video/image streams) ───
|
||||
_CCTV_PROXY_ALLOWED_HOSTS = {
|
||||
"s3-eu-west-1.amazonaws.com", # TfL JamCams
|
||||
"jamcams.tfl.gov.uk",
|
||||
@@ -8810,12 +8810,12 @@ def api_region_dossier(
|
||||
lat: float = Query(..., ge=-90, le=90),
|
||||
lng: float = Query(..., ge=-180, le=180),
|
||||
):
|
||||
"""Sync def so FastAPI runs it in a threadpool — prevents blocking the event loop."""
|
||||
"""Sync def so FastAPI runs it in a threadpool — prevents blocking the event loop."""
|
||||
return get_region_dossier(lat, lng)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Geocoding — proxy to Nominatim with caching and proper headers
|
||||
# Geocoding — proxy to Nominatim with caching and proper headers
|
||||
# ---------------------------------------------------------------------------
|
||||
from services.geocode import search_geocode, reverse_geocode
|
||||
|
||||
@@ -8988,7 +8988,7 @@ async def api_sentinel_tile(request: Request):
|
||||
evalscript = evalscripts.get(preset, evalscripts["TRUE-COLOR"])
|
||||
|
||||
# Adaptive time range: wider window at lower zoom for better coverage.
|
||||
# Sentinel-2 has 5-day revisit — a single day often has gaps.
|
||||
# Sentinel-2 has 5-day revisit — a single day often has gaps.
|
||||
# At low zoom we mosaic over more days to fill gaps.
|
||||
from datetime import datetime as _dt, timedelta as _td
|
||||
|
||||
@@ -9058,7 +9058,7 @@ async def api_sentinel_tile(request: Request):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API Settings — key registry & management
|
||||
# API Settings — key registry & management
|
||||
# ---------------------------------------------------------------------------
|
||||
from services.api_settings import get_api_keys, get_env_path_info
|
||||
from services.shodan_connector import (
|
||||
@@ -9133,7 +9133,7 @@ async def api_shodan_host(request: Request, body: ShodanHostRequest):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Finnhub — free market intelligence (quotes, congress trades, insider txns)
|
||||
# Finnhub — free market intelligence (quotes, congress trades, insider txns)
|
||||
# ---------------------------------------------------------------------------
|
||||
from services.unusual_whales_connector import (
|
||||
FinnhubConnectorError,
|
||||
@@ -9223,7 +9223,7 @@ async def api_reset_news_feeds(request: Request):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wormhole Settings — local agent toggle
|
||||
# Wormhole Settings — local agent toggle
|
||||
# ---------------------------------------------------------------------------
|
||||
from services.wormhole_settings import read_wormhole_settings, write_wormhole_settings
|
||||
from services.wormhole_status import read_wormhole_status
|
||||
@@ -9394,7 +9394,7 @@ class NodeSettingsUpdate(BaseModel):
|
||||
@limiter.limit("30/minute")
|
||||
async def api_get_node_settings(request: Request):
|
||||
"""Issue #243 (tg12): node mode and participant state are
|
||||
operational posture. Anonymous callers receive an empty stub ΓÇö
|
||||
operational posture. Anonymous callers receive an empty stub —
|
||||
enough for the UI to know the endpoint exists but nothing
|
||||
fingerprintable. Authenticated callers see the full state.
|
||||
|
||||
@@ -10098,7 +10098,7 @@ def decrypt_wormhole_dm_envelope(
|
||||
if str(current_tier or "").startswith("private_"):
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": "MLS format required in private transport mode — legacy DM decrypt blocked",
|
||||
"detail": "MLS format required in private transport mode — legacy DM decrypt blocked",
|
||||
}
|
||||
if not _legacy_dm1_allowed():
|
||||
return {
|
||||
@@ -10252,7 +10252,7 @@ async def api_wormhole_join(request: Request):
|
||||
)
|
||||
|
||||
# Enable node participation so the sync/push workers connect to peers.
|
||||
# This is the voluntary opt-in — the node only joins the network when
|
||||
# This is the voluntary opt-in — the node only joins the network when
|
||||
# the user explicitly opens the Wormhole.
|
||||
from services.node_settings import write_node_settings
|
||||
|
||||
@@ -12101,7 +12101,7 @@ async def api_set_privacy_profile(request: Request, body: PrivacyProfileUpdate):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# System — self-update
|
||||
# System — self-update
|
||||
# ---------------------------------------------------------------------------
|
||||
from pathlib import Path
|
||||
from services.updater import perform_update, schedule_restart
|
||||
@@ -12126,7 +12126,7 @@ async def system_update(request: Request):
|
||||
status_code=500,
|
||||
media_type="application/json",
|
||||
)
|
||||
# Docker: skip restart — user must pull new images manually
|
||||
# Docker: skip restart — user must pull new images manually
|
||||
if result.get("status") == "docker":
|
||||
return result
|
||||
# Schedule restart AFTER response flushes (2s delay)
|
||||
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
"""Typed configuration via pydantic-settings."""
|
||||
"""Typed configuration via pydantic-settings."""
|
||||
|
||||
from functools import lru_cache
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
@@ -32,7 +32,7 @@ class Settings(BaseSettings):
|
||||
MESH_RNS_ENABLED: bool = False
|
||||
MESH_ARTI_ENABLED: bool = False
|
||||
# When true, trust wormhole_status.json ready bit if the child process is
|
||||
# alive ΓÇö avoids transport-tier flapping when /api/health probes time out
|
||||
# alive — avoids transport-tier flapping when /api/health probes time out
|
||||
# under Tor load (common during live DM E2E).
|
||||
MESH_WORMHOLE_TRUST_FILE_READY: bool = False
|
||||
MESH_ARTI_SOCKS_PORT: int = 9050
|
||||
@@ -81,7 +81,7 @@ class Settings(BaseSettings):
|
||||
MESH_PEER_PUSH_SECRET: str = ""
|
||||
# Issue #256 (tg12): optional per-peer HMAC secret map. Comma-separated
|
||||
# `url=secret` pairs. When a peer URL appears here, only that per-peer
|
||||
# secret is accepted for it ΓÇö the global MESH_PEER_PUSH_SECRET above is
|
||||
# secret is accepted for it — the global MESH_PEER_PUSH_SECRET above is
|
||||
# ignored for that specific URL. Single-peer installs and unmigrated
|
||||
# multi-peer installs leave this empty and behavior is unchanged.
|
||||
MESH_PEER_SECRETS: str = ""
|
||||
@@ -123,7 +123,7 @@ class Settings(BaseSettings):
|
||||
MESH_RNS_IBF_FAIL_THRESHOLD: int = 3
|
||||
MESH_RNS_IBF_COOLDOWN_S: int = 120
|
||||
MESH_VERIFY_INTERVAL_S: int = 600
|
||||
# MESH_VERIFY_SIGNATURES is intentionally removed ΓÇö the audit loop in main.py
|
||||
# MESH_VERIFY_SIGNATURES is intentionally removed — the audit loop in main.py
|
||||
# always calls validate_chain_incremental(verify_signatures=True). Any value
|
||||
# set in the environment is ignored.
|
||||
MESH_DM_SECURE_MODE: bool = True
|
||||
@@ -145,14 +145,14 @@ class Settings(BaseSettings):
|
||||
# Anti-spam: cap on distinct UNACKED messages a single sender can have
|
||||
# parked in a single recipient's mailbox at any one time. Once the
|
||||
# recipient pulls (acks) a message, the sender's quota for that pair
|
||||
# frees up. Default 2 ΓÇö a sender who wants to deliver more must wait
|
||||
# frees up. Default 2 — a sender who wants to deliver more must wait
|
||||
# for the recipient to actually read the prior messages.
|
||||
#
|
||||
# This cap is enforced TWICE: once on the local deposit path (the
|
||||
# sender's own node refuses to spool the 3rd message) AND once on
|
||||
# the replication-acceptance path (honest peer relays refuse to
|
||||
# accept inbound replicas that would put them over the cap). The
|
||||
# double enforcement makes the rule a NETWORK rule ΓÇö patching out
|
||||
# double enforcement makes the rule a NETWORK rule — patching out
|
||||
# the local check on a hostile sender's relay doesn't let extras
|
||||
# propagate, because every honest peer enforces the same cap on
|
||||
# inbound replication.
|
||||
@@ -169,7 +169,7 @@ class Settings(BaseSettings):
|
||||
MESH_VOTER_BLIND_SALT_GRACE_DAYS: int = 30
|
||||
MESH_DM_MAX_MSG_BYTES: int = 8192
|
||||
MESH_DM_ALLOW_SENDER_SEAL: bool = False
|
||||
# TTL for DH key and prekey bundle registrations ΓÇö stale entries are pruned.
|
||||
# TTL for DH key and prekey bundle registrations — stale entries are pruned.
|
||||
MESH_DM_KEY_TTL_DAYS: int = 30
|
||||
# TTL for invite-scoped prekey lookup aliases; shorter windows reduce
|
||||
# long-lived relay linkage between opaque lookup handles and agent IDs.
|
||||
@@ -177,7 +177,7 @@ class Settings(BaseSettings):
|
||||
# TTL for relay witness history; keep continuity metadata bounded instead
|
||||
# of relying on a hidden hardcoded retention window.
|
||||
MESH_DM_WITNESS_TTL_DAYS: int = 14
|
||||
# TTL for mailbox binding metadata ΓÇö shorter = smaller metadata footprint on disk.
|
||||
# TTL for mailbox binding metadata — shorter = smaller metadata footprint on disk.
|
||||
MESH_DM_BINDING_TTL_DAYS: int = 3
|
||||
# When False, mailbox bindings are memory-only (agents re-register on restart).
|
||||
# Enable explicitly only if restart continuity is worth persisting DM graph metadata.
|
||||
@@ -320,7 +320,7 @@ class Settings(BaseSettings):
|
||||
# Second explicit opt-in for private-tier clearnet fallback. Without this
|
||||
# acknowledgement, "allow" remains requested but not effective.
|
||||
MESH_PRIVATE_CLEARNET_FALLBACK_ACKNOWLEDGE: bool = False
|
||||
# Meshtastic MQTT bridge ΓÇö disabled by default to avoid hammering the
|
||||
# Meshtastic MQTT bridge — disabled by default to avoid hammering the
|
||||
# public broker. Users opt in explicitly.
|
||||
MESH_MQTT_ENABLED: bool = False
|
||||
# Meshtastic MQTT broker credentials (defaults match public firmware).
|
||||
@@ -328,7 +328,7 @@ class Settings(BaseSettings):
|
||||
MESH_MQTT_PORT: int = 1883
|
||||
MESH_MQTT_USER: str = "meshdev"
|
||||
MESH_MQTT_PASS: str = "large4cats"
|
||||
# Hex-encoded PSK ΓÇö empty string means use the default LongFast key.
|
||||
# Hex-encoded PSK — empty string means use the default LongFast key.
|
||||
# Must decode to exactly 16 or 32 bytes when set.
|
||||
MESH_MQTT_PSK: str = ""
|
||||
# Optional operator-provided Meshtastic node ID (e.g. "!abcd1234") included
|
||||
@@ -351,16 +351,16 @@ class Settings(BaseSettings):
|
||||
OPERATOR_HANDLE: str = ""
|
||||
|
||||
# SAR (Synthetic Aperture Radar) data layer
|
||||
# Mode A ΓÇö free catalog metadata, no account, default-on
|
||||
# Mode A — free catalog metadata, no account, default-on
|
||||
MESH_SAR_CATALOG_ENABLED: bool = True
|
||||
# Mode B ΓÇö free pre-processed anomalies (OPERA / EGMS / GFM / EMS / UNOSAT)
|
||||
# Mode B — free pre-processed anomalies (OPERA / EGMS / GFM / EMS / UNOSAT)
|
||||
# Two-step opt-in: must be "allow" AND _ACKNOWLEDGE must be true
|
||||
MESH_SAR_PRODUCTS_FETCH: str = "block"
|
||||
MESH_SAR_PRODUCTS_FETCH_ACKNOWLEDGE: bool = False
|
||||
# NASA Earthdata Login (free) ΓÇö required for OPERA products
|
||||
# NASA Earthdata Login (free) — required for OPERA products
|
||||
MESH_SAR_EARTHDATA_USER: str = ""
|
||||
MESH_SAR_EARTHDATA_TOKEN: str = ""
|
||||
# Copernicus Data Space (free) ΓÇö required for EGMS / EMS products
|
||||
# Copernicus Data Space (free) — required for EGMS / EMS products
|
||||
MESH_SAR_COPERNICUS_USER: str = ""
|
||||
MESH_SAR_COPERNICUS_TOKEN: str = ""
|
||||
# Whether OpenClaw agents may read/act on the SAR layer
|
||||
|
||||
Reference in New Issue
Block a user