diff --git a/README.md b/README.md index d442bd79..27136e11 100644 --- a/README.md +++ b/README.md @@ -12,34 +12,30 @@ It ships as a single binary; the identity and access layer is **AuthBridge**, an ## Quick start (local, no Kubernetes) -See an AI agent's egress — LLM, MCP, and A2A calls — decrypted and parsed live on your laptop. No cluster, no Keycloak, no SPIRE. +Watch an AI agent's traffic — its model, tool, and agent-to-agent calls — decrypted and parsed live on your laptop. -1. **Install and start it.** One line installs the `abctl` and `authbridge-proxy` binaries (macOS/Linux) and starts the local demo. On first run it mints a demo CA under `./cortex-ca` and logs the `NODE_EXTRA_CA_CERTS=…` line to trust it: +1. **Install and start the demo** (macOS/Linux). Downloads two small binaries and starts the proxy in the background: ```sh curl -fsSL https://raw.githubusercontent.com/rossoctl/cortex/main/authbridge/install-demo.sh | sh ``` - _(Prefer to inspect first? Read [`install-demo.sh`](./authbridge/install-demo.sh) — or build from source: `cd authbridge/cmd/abctl && go build .` then `cd ../authbridge-proxy && go build . && ./authbridge-proxy --demo`.)_ - -2. **Open the session viewer** in another terminal: +2. **Open the live viewer** in another terminal: ```sh - abctl --endpoint http://localhost:9094 + abctl --endpoint http://localhost:47601 ``` -3. **Run your agent through it** — e.g. Claude Code (from the same directory, so `./cortex-ca` resolves — or use the absolute path the proxy logged): +3. **Send an agent's traffic through it** — e.g. Claude Code, from the directory where you started the demo: ```sh - HTTPS_PROXY=http://localhost:8081 \ + HTTPS_PROXY=http://localhost:47600 \ NODE_EXTRA_CA_CERTS="$PWD/cortex-ca/ca.crt" \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ claude ``` - Its LLM, MCP, and A2A calls appear live in `abctl`, decrypted and parsed. - -> Observe-only: the parsers *observe* traffic; nothing is enforced. The self-signed demo CA is for local use — in Kubernetes the CA is a mounted cert-manager Secret. + Its calls stream into `abctl`, decrypted and parsed. ## Running on Kubernetes diff --git a/authbridge/cmd/authbridge-proxy/demo.go b/authbridge/cmd/authbridge-proxy/demo.go index 9865a11d..a05905d5 100644 --- a/authbridge/cmd/authbridge-proxy/demo.go +++ b/authbridge/cmd/authbridge-proxy/demo.go @@ -16,12 +16,15 @@ const demoCADirDefault = "cortex-ca" // the LLM / MCP / A2A parsers, so an agent's egress is decrypted and parsed. // Kept in sync with the root README. // -// The listeners the demo actually uses are pinned to loopback: this runs on a -// laptop, so a wildcard bind would expose an open forward proxy and the -// unauthenticated session API (which carries decrypted bodies and any injected -// tokens) to the LAN. The preset only fills empty addresses, so these explicit -// values win. The enforce-redirect transparent listener isn't used here (no -// iptables) and main.go skips starting it under --demo. +// Every listener the demo uses is pinned to loopback on an uncommon port. This +// runs on a laptop, so (a) a wildcard bind would expose an open forward proxy, +// the stats endpoint, and the unauthenticated session API (which carries +// decrypted bodies and any injected tokens) to the LAN, and (b) the usual +// 8081/909x ports collide with common dev tools. The preset only fills empty +// addresses, so these explicit values win — keep them in sync with the ports +// the installer probes and prints (authbridge/install-demo.sh). The +// enforce-redirect transparent listener isn't used here (no iptables) and +// main.go skips starting it under --demo. // // The YAML body is flush-left on purpose — a raw string literal preserves // leading whitespace, so indenting these lines in source would corrupt the YAML. @@ -32,8 +35,10 @@ func demoConfigYAML(caDir string) string { mode: proxy-sidecar listener: roles: [forward] - forward_proxy_addr: 127.0.0.1:8081 - session_api_addr: 127.0.0.1:9094 + forward_proxy_addr: 127.0.0.1:47600 + session_api_addr: 127.0.0.1:47601 +stats: + address: 127.0.0.1:47602 tls_bridge: mode: enabled ca_dir: "` + caDir + `" diff --git a/authbridge/cmd/authbridge-proxy/demo_test.go b/authbridge/cmd/authbridge-proxy/demo_test.go index c3092ba5..bb1a6faf 100644 --- a/authbridge/cmd/authbridge-proxy/demo_test.go +++ b/authbridge/cmd/authbridge-proxy/demo_test.go @@ -41,15 +41,19 @@ func TestDemoConfig_WriteLoadsAndValidates(t *testing.T) { t.Errorf("expected forward-only roles, got %v", roles) } - // The listeners the demo uses must bind loopback, never a wildcard that - // would expose an open forward proxy or the unauthenticated session API - // (decrypted bodies + injected tokens) to the LAN. The transparent listener - // isn't started under --demo (main.go gates it), so it's not asserted here. - if got := cfg.Listener.ForwardProxyAddr; got != "127.0.0.1:8081" { - t.Errorf("ForwardProxyAddr = %q, want loopback 127.0.0.1:8081", got) + // The listeners the demo uses must bind loopback on the uncommon ports the + // installer probes/prints, never a wildcard that would expose an open forward + // proxy, the stats endpoint, or the unauthenticated session API (decrypted + // bodies + injected tokens) to the LAN. The transparent listener isn't started + // under --demo (main.go gates it), so it's not asserted here. + if got := cfg.Listener.ForwardProxyAddr; got != "127.0.0.1:47600" { + t.Errorf("ForwardProxyAddr = %q, want loopback 127.0.0.1:47600", got) } - if got := cfg.Listener.SessionAPIAddr; got != "127.0.0.1:9094" { - t.Errorf("SessionAPIAddr = %q, want loopback 127.0.0.1:9094", got) + if got := cfg.Listener.SessionAPIAddr; got != "127.0.0.1:47601" { + t.Errorf("SessionAPIAddr = %q, want loopback 127.0.0.1:47601", got) + } + if got := cfg.Stats.StatsAddress; got != "127.0.0.1:47602" { + t.Errorf("Stats.StatsAddress = %q, want loopback 127.0.0.1:47602", got) } if cfg.TLSBridge == nil { diff --git a/authbridge/install-demo.sh b/authbridge/install-demo.sh index ea1dbd5c..ad31ebbc 100755 --- a/authbridge/install-demo.sh +++ b/authbridge/install-demo.sh @@ -5,7 +5,8 @@ # # Detects your OS/arch, downloads the prebuilt `abctl` and `authbridge-proxy` # binaries for the newest release, verifies their SHA-256 checksums, installs -# them to ~/.local/bin, and starts `authbridge-proxy --demo` (Ctrl-C to stop). +# them to ~/.local/bin, and starts the demo in the background — then prints the +# commands to watch traffic and point an agent at it, plus how to stop it. # macOS + Linux, amd64 + arm64. No cluster, Keycloak, or SPIRE needed. # # Environment: @@ -37,6 +38,25 @@ sha_check() { fi } +# Demo listener ports — loopback, and deliberately uncommon to avoid colliding +# with common dev tools. Keep in sync with the demo config in +# authbridge/cmd/authbridge-proxy/demo.go. +DEMO_FORWARD_PORT=47600 +DEMO_SESSION_PORT=47601 +DEMO_STATS_PORT=47602 + +# port_in_use exits 0 if something is already listening on the given loopback +# port. Best-effort: uses lsof, then nc; if neither exists, it assumes free. +port_in_use() { + if command -v lsof >/dev/null 2>&1; then + lsof -nP -iTCP@127.0.0.1:"$1" -sTCP:LISTEN >/dev/null 2>&1 + elif command -v nc >/dev/null 2>&1; then + nc -z 127.0.0.1 "$1" >/dev/null 2>&1 + else + return 1 + fi +} + # --- detect platform --- os=$(uname -s) case "$os" in @@ -52,6 +72,15 @@ case "$arch" in *) die "unsupported architecture: $arch (supported: amd64, arm64)" ;; esac +# --- preflight: fail early (before downloading) if a demo port is taken --- +if [ "${AUTHBRIDGE_INSTALL_ONLY:-}" != "1" ]; then + for p in "$DEMO_FORWARD_PORT" "$DEMO_SESSION_PORT" "$DEMO_STATS_PORT"; do + if port_in_use "$p"; then + die "port ${p} is already in use. Is the demo already running (see ./cortex-ca/demo.pid)? Otherwise free the port, or change the ports in ./cortex-ca/demo.yaml, then re-run." + fi + done +fi + # --- resolve the release tag --- # `releases/latest` excludes prereleases, and the project ships prereleases, so # list releases (newest first) and take the first tag_name instead. @@ -104,8 +133,9 @@ fi rm -rf "$tmp" trap - EXIT -# --- report + next steps --- +# --- report --- proxy="${BIN_DIR}/authbridge-proxy" +ca_dir="$(pwd)/cortex-ca" # matches demoCADirDefault in demo.go case ":${PATH}:" in *":${BIN_DIR}:"*) abctl_cmd="abctl" proxy_cmd="authbridge-proxy" ;; *) abctl_cmd="${BIN_DIR}/abctl" proxy_cmd="$proxy" ;; @@ -116,25 +146,61 @@ info "Installed abctl and authbridge-proxy (${version}) to ${BIN_DIR}" case ":${PATH}:" in *":${BIN_DIR}:"*) ;; *) - warn "${BIN_DIR} is not on your PATH; the commands below use full paths." + warn "${BIN_DIR} is not on your PATH." warn "Add it for future sessions: export PATH=\"${BIN_DIR}:\$PATH\"" ;; esac -info "" -info "In two more terminals (from this directory, so ./cortex-ca resolves):" -info " View the live session: ${abctl_cmd} --endpoint http://localhost:9094" -info ' Run your agent, e.g. Claude Code:' -# $PWD is intentionally literal here — printed for the user's shell to expand -# when they paste the command, not expanded by this script. -# shellcheck disable=SC2016 -info ' HTTPS_PROXY=http://localhost:8081 NODE_EXTRA_CA_CERTS="$PWD/cortex-ca/ca.crt" CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 claude' -info "" if [ "${AUTHBRIDGE_INSTALL_ONLY:-}" = "1" ]; then + info "" info "Install-only mode. Start the demo with: ${proxy_cmd} --demo" exit 0 fi -info "Starting the demo proxy (Ctrl-C to stop)..." +# --- start in the background, then wait until it's actually listening --- +info "" +info "Starting the demo in the background..." +mkdir -p "$ca_dir" +log="${ca_dir}/demo.log" +pidfile="${ca_dir}/demo.pid" +nohup "$proxy" --demo "$log" 2>&1 & +demo_pid=$! +echo "$demo_pid" >"$pidfile" + +# Confirm readiness from real signals, not the "listening" log line — that line is +# emitted just *before* the socket is bound, so a bind failure could look ready. +# A bind failure exits within ms (the proxy Fatalf's), so watch for early exit; +# and probe the forward port for a true post-bind signal. +ready=0 +i=0 +while [ "$i" -lt 50 ]; do + if ! kill -0 "$demo_pid" 2>/dev/null; then + warn "the demo exited during startup — last log lines:" + tail -n 15 "$log" >&2 || true + die "demo failed to start (full log: ${log})" + fi + if port_in_use "$DEMO_FORWARD_PORT"; then + ready=1 + break + fi + sleep 0.2 + i=$((i + 1)) +done + +info "" +if [ "$ready" -eq 1 ]; then + info "Cortex demo is running (pid ${demo_pid}). Logs: ${log}" +else + # It didn't exit during the startup window (a bind failure would have killed + # it), but no probe tool confirmed the port — most likely up. Say so honestly. + info "Cortex demo started (pid ${demo_pid}); couldn't confirm it's listening (install lsof or nc to verify). Logs: ${log}" +fi +info "" +info " Watch traffic: ${abctl_cmd} --endpoint http://localhost:${DEMO_SESSION_PORT}" +info " Send traffic through it (e.g. Claude Code):" +info " HTTPS_PROXY=http://localhost:${DEMO_FORWARD_PORT} \\" +info " NODE_EXTRA_CA_CERTS=${ca_dir}/ca.crt \\" +info " CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 claude" +info "" +info " Stop the demo: kill ${demo_pid} (or: kill \$(cat ${pidfile}))" info "" -exec "$proxy" --demo