Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions capture-sidecar/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions capture-sidecar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
21 changes: 18 additions & 3 deletions capture-sidecar/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -255,22 +265,27 @@ struct FwSetBody {
}

async fn fw_set(State(st): State<AppState>, Json(body): Json<FwSetBody>) -> ApiResult<Response> {
// 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()),
],
};
let decision = gate::evaluate(&st.gate, &cmd).await;
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<AppState>, ws: WebSocketUpgrade) -> Response {
Expand Down
11 changes: 11 additions & 0 deletions scripts/assemble-cockpit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
13 changes: 13 additions & 0 deletions scripts/bearbrowser-cockpit-up
Original file line number Diff line number Diff line change
Expand 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; }
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/bearbrowser-patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion settings/start/bearbrowser-start.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@

.grid {
display: grid;
grid-template-columns: repeat(4, 80px);
grid-template-columns: repeat(5, 80px);
gap: 20px;
margin-bottom: 44px;
}
Expand Down Expand Up @@ -282,6 +282,10 @@
<div class="shortcut-icon">📦</div>
<span class="shortcut-label">Registry</span>
</a>
<a class="shortcut" href="resource:///bearstart/bearnet.html">
<div class="shortcut-icon">📡</div>
<span class="shortcut-label">Network</span>
</a>
</div>

<div class="footer">
Expand Down
Loading
Loading