From 5db79c08fdd9698902499c009b220267dd687f27 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:19:02 -0400 Subject: [PATCH] bearnet: cockpit panel + auto-launch for the capture sidecar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the network-visibility subsystem VISIBLE in the browser. Adds settings/start/bearnet.html — the green-on-black BearNet panel (packaged into omni.ja at resource:///bearstart/bearnet.html, linked from the start page's new Network tile) with three live sections matching the native shell: - Packet Capture: Start/Stop/Save + live monospace stream over the /events WS. Start button = the explicit user gesture (actor=user, userGesture=true, approvalToken) that the gate requires to permit a capture. - Connection Map: live table of endpoints, category-colored (tracker/analytics/ cdn/unknown), blocked status. - Firewall: per-domain Block/Allow, reset; rules list. Auto-launch wiring: - assemble-cockpit.sh builds + stages the capture sidecar (skips gracefully if cargo absent). - bearbrowser-cockpit-up launches it on :8093 with --repo-root $RES so it finds the bridge; safe to launch blindly (it refuses to run ungoverned). Two fixes found while verifying against a live sidecar: - CORS: the panel is a resource:// page fetching the loopback service cross-origin, so the browser needs CORS headers or every request silently fails. Added a permissive CorsLayer (safe: loopback-only + gated). - Firewall keys on eTLD+1: a rule on 'ads.evil.com' now stores 'evil.com' so it matches the map's eTLD+1 blocked-check (the shell's model). Verified: block ads.evil.com -> ingest tracker.ads.evil.com shows blocked:true. Verified live: sidecar detects dumpcap; all panel endpoints return correct data with the panel's exact payloads; user-gesture+token capture-start -> HTTP 200 (gate permits); agent -> 403; CORS preflight 200; WS upgrades; panel served 200 + JS syntax-valid. --- capture-sidecar/Cargo.lock | 23 ++ capture-sidecar/Cargo.toml | 5 + capture-sidecar/src/server.rs | 21 +- scripts/assemble-cockpit.sh | 11 + scripts/bearbrowser-cockpit-up | 13 ++ scripts/bearbrowser-patches.py | 2 +- settings/start/bearbrowser-start.html | 6 +- settings/start/bearnet.html | 300 ++++++++++++++++++++++++++ 8 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 settings/start/bearnet.html diff --git a/capture-sidecar/Cargo.lock b/capture-sidecar/Cargo.lock index 281af81..bd131e7 100644 --- a/capture-sidecar/Cargo.lock +++ b/capture-sidecar/Cargo.lock @@ -98,6 +98,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + [[package]] name = "block-buffer" version = "0.10.4" @@ -136,6 +142,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tower-http", "tracing", "tracing-subscriber", "uuid", @@ -800,6 +807,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" diff --git a/capture-sidecar/Cargo.toml b/capture-sidecar/Cargo.toml index 8c5f2cf..5124bee 100644 --- a/capture-sidecar/Cargo.toml +++ b/capture-sidecar/Cargo.toml @@ -27,6 +27,11 @@ thiserror = "1" # WS fan-out of capture lines + connection records. tokio-stream = { version = "0.1", features = ["sync"] } +# CORS for the cockpit panel: bearnet.html is a resource:// page fetching this +# loopback service cross-origin, so the browser requires CORS headers. Safe to +# be permissive — the service is loopback-only and every side effect is gated. +tower-http = { version = "0.5", features = ["cors"] } + # Structured logging (stderr only; never the network). tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } diff --git a/capture-sidecar/src/server.rs b/capture-sidecar/src/server.rs index 825338f..ba2bd2c 100644 --- a/capture-sidecar/src/server.rs +++ b/capture-sidecar/src/server.rs @@ -20,6 +20,7 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::{broadcast, Mutex}; +use tower_http::cors::{Any, CorsLayer}; #[derive(Clone)] pub struct AppState { @@ -58,6 +59,15 @@ pub fn router(state: AppState) -> Router { .route("/map", get(map).post(map_ingest).delete(map_clear)) .route("/firewall", get(fw_list).post(fw_set).delete(fw_clear)) .route("/events", get(ws_upgrade)) + // The panel (resource:// origin) fetches this loopback service cross-origin. + // Permissive CORS is safe here: the socket is loopback-only and every + // side effect is gated through the bridge regardless of origin. + .layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any), + ) .with_state(state) } @@ -255,13 +265,18 @@ struct FwSetBody { } async fn fw_set(State(st): State, Json(body): Json) -> ApiResult { + // Normalize to eTLD+1 — the whole subsystem (map classifier, blocked-check) + // operates at that granularity, so a rule on "ads.evil.com" must key on + // "evil.com" or it would never match an observed connection. Matches the + // native shell, which set + checked firewall rules via etldForHost:. + let domain = crate::netmap::etld_plus_one(&body.domain); let cmd = Command { action: "firewall-set".into(), actor: body.actor, user_gesture: body.user_gesture, approval_token: body.approval_token, params: vec![ - ("domain".into(), body.domain.clone()), + ("domain".into(), domain.clone()), ("decision".into(), format!("{:?}", body.decision).to_lowercase()), ], }; @@ -269,8 +284,8 @@ async fn fw_set(State(st): State, Json(body): Json) -> ApiR if !decision.permitted() { return Err(denied(&decision)); } - st.firewall.set(&body.domain, body.decision); - Ok(Json(json!({ "set": body.domain, "decision": body.decision })).into_response()) + st.firewall.set(&domain, body.decision); + Ok(Json(json!({ "set": domain, "decision": body.decision })).into_response()) } async fn ws_upgrade(State(st): State, ws: WebSocketUpgrade) -> Response { diff --git a/scripts/assemble-cockpit.sh b/scripts/assemble-cockpit.sh index 025d96d..f039ca9 100755 --- a/scripts/assemble-cockpit.sh +++ b/scripts/assemble-cockpit.sh @@ -36,6 +36,17 @@ OUT="$STAGE/cockpit" bash "$REPO_ROOT/scripts/build-cockpit.sh" log "building agent-machine sidecar …" OUT="$STAGE/sidecars" bash "$REPO_ROOT/scripts/build-agent-machine-sidecar.sh" +# 2b. capture sidecar — cargo build → $STAGE/sidecars/…-capture-sidecar-bin (self-smoke). +# Network-visibility surface (packet capture + connection map + firewall), +# governed by the same bridge. Skipped gracefully if cargo is unavailable so a +# host without Rust can still assemble the rest of the cockpit. +if command -v cargo >/dev/null 2>&1; then + log "building capture sidecar …" + OUT="$STAGE/sidecars" bash "$REPO_ROOT/scripts/build-capture-sidecar.sh" +else + log "WARNING: cargo not found — skipping capture sidecar (BearNet panel will show offline)" +fi + # 3. Stage the runtime scripts + the enforcing contract (Lane 4 + Receipts + orchestrator). log "staging runtime scripts + policy contract …" for s in bearbrowser-agent-machine bearbrowser-agent-machine-gate.py agent-control-bridge.py \ diff --git a/scripts/bearbrowser-cockpit-up b/scripts/bearbrowser-cockpit-up index 6cad0a1..f37d3b3 100755 --- a/scripts/bearbrowser-cockpit-up +++ b/scripts/bearbrowser-cockpit-up @@ -28,6 +28,11 @@ COCKPIT_CFG="$RES/cockpit/cockpit-config.js" GATE_PORT="${BEARBROWSER_AM_GATE_PORT:-8080}" UPSTREAM_PORT="${NOETICA_AM_UPSTREAM_PORT:-8091}" RECEIPTS_PORT="${BEARBROWSER_RECEIPTS_PORT:-8092}" +# Capture sidecar (network-visibility surface): the resource:///bearstart/bearnet.html +# panel talks to it directly on this fixed loopback port. Optional — only launched +# if the binary was staged (assemble skips it when cargo is absent). +CAPTURE_PORT="${BEARBROWSER_CAPTURE_PORT:-8093}" +CAPTURE_BIN="$RES/sidecars/bearbrowser-capture-sidecar-bin" for f in "$BIN" "$GATE" "$RECEIPTS"; do [ -e "$f" ] || { echo "[cockpit-up] ERROR: missing $f (run assemble-cockpit.sh)"; exit 1; } @@ -53,6 +58,14 @@ NOETICA_AM_PORT="$UPSTREAM_PORT" NOETICA_OFFLINE="${NOETICA_OFFLINE:-1}" "$BIN" NOETICA_AM_UPSTREAM_PORT="$UPSTREAM_PORT" BEARBROWSER_AM_GATE_PORT="$GATE_PORT" python3 "$GATE" >/tmp/bb-cockpit-gate.log 2>&1 & pids+=($!) BEARBROWSER_RECEIPTS_PORT="$RECEIPTS_PORT" python3 "$RECEIPTS" >/tmp/bb-cockpit-receipts.log 2>&1 & pids+=($!) +# Capture sidecar — governed by the same bridge (--repo-root $RES resolves +# scripts/agent-control-bridge.py + policy/). It reimplements no policy and +# refuses to run if the bridge is missing, so this is safe to launch blindly. +if [ -x "$CAPTURE_BIN" ]; then + echo "[cockpit-up] capture → :$CAPTURE_PORT" + "$CAPTURE_BIN" --repo-root "$RES" --port "$CAPTURE_PORT" >/tmp/bb-cockpit-capture.log 2>&1 & pids+=($!) +fi + # 2. Bounded health summary — report, don't hang. gate_ok=000; rcpt_ok=000 for _ in $(seq 1 30); do diff --git a/scripts/bearbrowser-patches.py b/scripts/bearbrowser-patches.py index 5cb50c2..47147ae 100755 --- a/scripts/bearbrowser-patches.py +++ b/scripts/bearbrowser-patches.py @@ -398,7 +398,7 @@ def _png_size(p): _bs_dir = Path("browser/bearstart") _bs_dir.mkdir(exist_ok=True) _bs_src = Path("../settings/start") - _bs_files = ["bearbrowser-start.html", "dm-sans-latin.woff2", "dm-sans-latin-ext.woff2"] + _bs_files = ["bearbrowser-start.html", "bearnet.html", "dm-sans-latin.woff2", "dm-sans-latin-ext.woff2"] _bs_installed = [] for _bs_file in _bs_files: _src = _bs_src / _bs_file diff --git a/settings/start/bearbrowser-start.html b/settings/start/bearbrowser-start.html index e395521..8b397d8 100644 --- a/settings/start/bearbrowser-start.html +++ b/settings/start/bearbrowser-start.html @@ -124,7 +124,7 @@ .grid { display: grid; - grid-template-columns: repeat(4, 80px); + grid-template-columns: repeat(5, 80px); gap: 20px; margin-bottom: 44px; } @@ -282,6 +282,10 @@
📦
Registry + +
📡
+ Network +