diff --git a/.roorules b/.roorules index 189a02b..a22a2e1 100644 --- a/.roorules +++ b/.roorules @@ -6,12 +6,15 @@ ```bash ./browser.sh start [ARGS...] # build + launch (detached, waits for MCP ready), forwards ARGS -./browser.sh stop # kill running instance +./browser.sh stop # kill ALL running instances ./browser.sh restart [ARGS...]# stop + build + start, forwards ARGS -./browser.sh status # check if running + MCP responsive -./browser.sh log # tail last 50 lines of browser log +./browser.sh status # list all running instances + MCP status +./browser.sh log # tail the browser log +./browser.sh mcp-url # print MCP endpoint URL(s) for running instance(s) ``` +**Multiple instances:** if the default MCP port (17777) is already in use by another instance, the browser automatically falls back to an OS-assigned port and records it in `/tmp/sovereign_browser_.port`. `browser.sh` reads these discovery files to find/stop/status every instance. Use `./browser.sh mcp-url` to discover the actual endpoint(s). + ## Login The browser requires Nostr login before browser tools work. The simplest way is to pass `--login-method` on the command line — this bypasses the GTK login dialog entirely, no MCP call needed: @@ -36,7 +39,7 @@ Alternatively, you can still log in at runtime via the MCP `login` tool with `{" ## MCP Server -- Endpoint: `http://localhost:17777/mcp` (Streamable HTTP) +- Endpoint: `http://localhost:17777/mcp` (Streamable HTTP) for the first instance; subsequent instances get an OS-assigned port — use `./browser.sh mcp-url` to discover the actual endpoint(s) - 100 tools available (login, navigation, snapshot, interaction, tabs, cookies, screenshots, etc.) - See `.roo/agents.md` for the full tool list and workflow diff --git a/VERSION b/VERSION index 8e0e3bc..ea7f030 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.55 +0.0.56 diff --git a/browser.sh b/browser.sh index 29deeff..0cf4dce 100755 --- a/browser.sh +++ b/browser.sh @@ -8,10 +8,16 @@ # # Usage: # ./browser.sh start [ARGS...] — build + launch (detached), forwarding ARGS -# ./browser.sh stop — kill any running instance +# ./browser.sh stop — kill ALL running instances # ./browser.sh restart [ARGS...]— stop + build + start, forwarding ARGS -# ./browser.sh status — check if running + MCP responsive +# ./browser.sh status — list all running instances + MCP status # ./browser.sh log — tail the browser log +# ./browser.sh mcp-url — print MCP endpoint URL(s) for running instance(s) +# +# Multiple instances: if the default MCP port (17777) is already in use by +# another instance, the browser automatically falls back to an OS-assigned +# port and records it in /tmp/sovereign_browser_.port. browser.sh +# reads these discovery files to find/stop/status every instance. # # Extra args after start/restart are forwarded to the binary, e.g.: # ./browser.sh start --login-method generate --url https://example.com @@ -25,7 +31,37 @@ set -euo pipefail BIN="./sovereign_browser" LOG="/tmp/sovereign_browser.log" PID_FILE="/tmp/sovereign_browser.pid" -PORT=17777 +DEFAULT_PORT=17777 +DISCOVERY_GLOB="/tmp/sovereign_browser_*.port" + +# Read the port for a given PID from its discovery file. Falls back to +# DEFAULT_PORT (17777) if no discovery file exists (e.g. older binary, or +# the first instance that successfully bound the default port and wrote a +# discovery file with 17777). Echoes the port to stdout. +port_for_pid() { + local pid="${1:-}" + local file="/tmp/sovereign_browser_${pid}.port" + if [[ -f "$file" ]]; then + local p + p=$(awk '{print $2}' "$file" 2>/dev/null | head -n1) + if [[ -n "$p" ]]; then + echo "$p" + return 0 + fi + fi + echo "$DEFAULT_PORT" +} + +# Echo the port for the currently-running instance (per get_pid), or +# DEFAULT_PORT if not running. +current_port() { + local pid + if pid=$(get_pid 2>/dev/null); then + port_for_pid "$pid" + else + echo "$DEFAULT_PORT" + fi +} # Check if a PID is actually the sovereign_browser process. # This safety guard prevents us from accidentally killing unrelated @@ -53,13 +89,24 @@ get_pid() { return 0 fi fi - # Fallback: find the process LISTENing on the port. + # Fallback 1: scan discovery files for a live sovereign_browser PID. + local f + for f in $DISCOVERY_GLOB; do + [[ -f "$f" ]] || continue + local dpid + dpid=$(awk '{print $1}' "$f" 2>/dev/null | head -n1) + if [[ -n "$dpid" ]] && is_browser_pid "$dpid"; then + echo "$dpid" + return 0 + fi + done + # Fallback 2: find the process LISTENing on the default port. # -sTCP:LISTEN matches only the listening socket, never client # connections (e.g. Roo Code's MCP client). This is critical — # without it, `lsof -ti :PORT` would also return Roo Code's PID # and we'd kill the agent driving the browser. local port_pid - port_pid=$(lsof -ti :"$PORT" -sTCP:LISTEN 2>/dev/null | head -n1 || echo "") + port_pid=$(lsof -ti :"$DEFAULT_PORT" -sTCP:LISTEN 2>/dev/null | head -n1 || echo "") if [[ -n "$port_pid" ]] && is_browser_pid "$port_pid"; then echo "$port_pid" return 0 @@ -67,13 +114,36 @@ get_pid() { return 1 } +# Echo all live sovereign_browser PIDs (one per line), discovered via the +# discovery files and the default-port listen socket. Used by cmd_stop to +# kill every running instance. +all_pids() { + local pids=() + local f + for f in $DISCOVERY_GLOB; do + [[ -f "$f" ]] || continue + local dpid + dpid=$(awk '{print $1}' "$f" 2>/dev/null | head -n1) + if [[ -n "$dpid" ]] && is_browser_pid "$dpid"; then + pids+=("$dpid") + fi + done + local pp + for pp in $(lsof -ti :"$DEFAULT_PORT" -sTCP:LISTEN 2>/dev/null || true); do + if is_browser_pid "$pp"; then + pids+=("$pp") + fi + done + # Deduplicate. + printf '%s\n' "${pids[@]}" 2>/dev/null | sort -u | grep -v '^$' || true +} + cmd_stop() { local stopped_any=0 - # Collect candidate PIDs from the PID file and from LISTENing sockets. - # We use -sTCP:LISTEN so we only match the server, never client - # connections (Roo Code / VS Code ext host connect to the MCP server - # and would be killed by a bare `lsof -ti :PORT`). + # Collect ALL running sovereign_browser PIDs — from the PID file, + # discovery files, and the default-port listen socket. This kills + # every instance when multiple are running on different ports. local candidates=() if [[ -f "$PID_FILE" ]]; then while IFS= read -r pid; do @@ -82,7 +152,7 @@ cmd_stop() { fi while IFS= read -r pid; do [[ -n "$pid" ]] && candidates+=("$pid") - done < <(lsof -ti :"$PORT" -sTCP:LISTEN 2>/dev/null || true) + done < <(all_pids) # Deduplicate and kill only verified sovereign_browser PIDs. for pid in $(printf '%s\n' "${candidates[@]}" 2>/dev/null | sort -u | grep -v '^$'); do @@ -96,7 +166,7 @@ cmd_stop() { # Wait for graceful shutdown, then force-kill survivors (verified only). if [[ "$stopped_any" -eq 1 ]]; then sleep 1 - for pid in $(lsof -ti :"$PORT" -sTCP:LISTEN 2>/dev/null || true); do + for pid in $(all_pids); do if is_browser_pid "$pid"; then echo "[browser] Force killing PID $pid..." kill -9 "$pid" 2>/dev/null || true @@ -106,10 +176,21 @@ cmd_stop() { fi rm -f "$PID_FILE" + # Clean up any leftover discovery files (the browser removes its own on + # clean shutdown, but force-killed instances leave them behind). + local f + for f in $DISCOVERY_GLOB; do + [[ -f "$f" ]] || continue + local dpid + dpid=$(awk '{print $1}' "$f" 2>/dev/null | head -n1) + if [[ -z "$dpid" ]] || ! kill -0 "$dpid" 2>/dev/null; then + rm -f "$f" + fi + done - # Final check: is anything still LISTENing on the port? - if lsof -ti :"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then - echo "[browser] Warning: process still listening on port $PORT." + # Final check: is anything still running? + if [[ -n "$(all_pids)" ]]; then + echo "[browser] Warning: some instances still running." else echo "[browser] Stopped." fi @@ -187,13 +268,18 @@ cmd_start() { disown 2>/dev/null || true echo "$pid" > "$PID_FILE" - # Wait for the MCP server to be ready (up to 10 seconds) - echo "[browser] Waiting for MCP server on port $PORT..." + # Wait for the MCP server to be ready (up to 10 seconds). The actual + # port may differ from DEFAULT_PORT if it was in use (the browser falls + # back to an OS-assigned port and writes it to a discovery file), so + # read the port from the discovery file each iteration. + echo "[browser] Waiting for MCP server (PID $pid)..." + local mcp_port="" for i in $(seq 1 100); do - if curl -s -o /dev/null -X POST "http://localhost:$PORT/mcp" \ + mcp_port=$(port_for_pid "$pid") + if curl -s -o /dev/null -X POST "http://localhost:$mcp_port/mcp" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' 2>/dev/null; then - echo "[browser] MCP server is ready (PID $pid)." + echo "[browser] MCP server is ready (PID $pid, port $mcp_port)." return 0 fi sleep 0.1 @@ -211,22 +297,51 @@ cmd_restart() { } cmd_status() { + local pids + pids=$(all_pids) + if [[ -z "$pids" ]]; then + echo "[browser] Not running." + return 0 + fi + local count + count=$(echo "$pids" | wc -l) local pid - if pid=$(get_pid 2>/dev/null); then - echo "[browser] Running (PID $pid)." - # Check MCP - if curl -s -o /dev/null -X POST "http://localhost:$PORT/mcp" \ + for pid in $pids; do + local p + p=$(port_for_pid "$pid") + echo "[browser] Running (PID $pid, port $p)." + if curl -s -o /dev/null -X POST "http://localhost:$p/mcp" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' 2>/dev/null; then - echo "[browser] MCP server: responsive" + echo "[browser] MCP server: responsive" else - echo "[browser] MCP server: NOT responding" + echo "[browser] MCP server: NOT responding" fi - else - echo "[browser] Not running." + done + if [[ "$count" -gt 1 ]]; then + echo "[browser] $count instances running." fi } +# Print the MCP endpoint URL for the running instance (or all instances +# if multiple are running). Useful for external clients that need to +# discover the port programmatically: +# URL=$(./browser.sh mcp-url) +cmd_mcp_url() { + local pids + pids=$(all_pids) + if [[ -z "$pids" ]]; then + echo "[browser] Not running." >&2 + return 1 + fi + local pid + for pid in $pids; do + local p + p=$(port_for_pid "$pid") + echo "http://localhost:$p/mcp" + done +} + cmd_log() { if [[ -f "$LOG" ]]; then tail -50 "$LOG" @@ -241,9 +356,11 @@ case "${1:-}" in restart) shift; cmd_restart "$@" ;; status) cmd_status ;; log) cmd_log ;; + mcp-url) cmd_mcp_url ;; *) - echo "Usage: $0 {start|stop|restart|status|log} [ARGS...]" >&2 + echo "Usage: $0 {start|stop|restart|status|log|mcp-url} [ARGS...]" >&2 echo " start/restart forward extra args to the binary." >&2 + echo " mcp-url prints the MCP endpoint URL(s) for running instance(s)." >&2 exit 1 ;; esac diff --git a/src/agent_server.c b/src/agent_server.c index e3292b6..4e1810e 100644 --- a/src/agent_server.c +++ b/src/agent_server.c @@ -14,10 +14,50 @@ #include #include #include +#include +#include +#include /* Forward declarations from agent_tools.c */ extern cJSON *agent_tools_dispatch(cJSON *request, SoupWebsocketConnection *conn); +/* ── Discovery file ───────────────────────────────────────────────── * + * When the configured port (default 17777) is already in use by another + * browser instance, we fall back to an OS-assigned port (bind to 0). So + * that external clients and browser.sh can find the actual port, we write + * a per-instance discovery file at /tmp/sovereign_browser_.port + * containing the PID and port. browser.sh globs these files to enumerate + * running instances. The file is removed on clean shutdown. */ +static char g_discovery_path[128] = {0}; + +static void build_discovery_path(char *out, size_t out_sz) { + snprintf(out, out_sz, "/tmp/sovereign_browser_%d.port", (int)getpid()); +} + +static void write_discovery_file(int port) { + char path[128]; + FILE *fp; + build_discovery_path(path, sizeof(path)); + fp = fopen(path, "w"); + if (fp == NULL) { + g_printerr("[agent] Failed to write discovery file %s\n", path); + return; + } + /* Format: "PID PORT\n" — easy to parse from shell with `read`. */ + fprintf(fp, "%d %d\n", (int)getpid(), port); + fclose(fp); + snprintf(g_discovery_path, sizeof(g_discovery_path), "%s", path); + g_print("[agent] Discovery file: %s (pid=%d port=%d)\n", + path, (int)getpid(), port); +} + +static void remove_discovery_file(void) { + if (g_discovery_path[0] != '\0') { + unlink(g_discovery_path); + g_discovery_path[0] = '\0'; + } +} + /* ── Static state ─────────────────────────────────────────────────── */ static SoupServer *g_server = NULL; @@ -210,8 +250,20 @@ int agent_server_start(int port) { /* Add MCP handler at /mcp. */ agent_mcp_register(g_server); - /* Listen on the specified port (0 = auto-assign). */ + /* Listen on the specified port. If it's already in use (another browser + * instance is running), fall back to port 0 so the OS picks a free port. + * The actual bound port is written to a discovery file so browser.sh and + * external MCP clients can find it. */ soup_server_listen_local(g_server, port, 0, &error); + if (error != NULL) { + if (port != 0) { + g_printerr("[agent] Port %d in use (%s) — falling back to OS auto-assign.\n", + port, error->message); + g_error_free(error); + error = NULL; + soup_server_listen_local(g_server, 0, 0, &error); + } + } if (error != NULL) { g_printerr("[agent] Failed to listen on port %d: %s\n", port, error->message); g_error_free(error); @@ -233,6 +285,11 @@ int agent_server_start(int port) { g_running = TRUE; g_print("[agent] WebSocket server listening on ws://localhost:%d/agent\n", g_port); g_print("[agent] Status endpoint: http://localhost:%d/\n", g_port); + if (g_port != port) { + g_print("[agent] Note: using auto-assigned port %d (configured port %d was in use).\n", + g_port, port); + } + write_discovery_file(g_port); return 0; } @@ -258,6 +315,7 @@ void agent_server_stop(void) { g_running = FALSE; g_port = 0; + remove_discovery_file(); g_print("[agent] Server stopped\n"); } diff --git a/src/version.h b/src/version.h index 11149ac..01da703 100644 --- a/src/version.h +++ b/src/version.h @@ -11,9 +11,9 @@ #ifndef SOVEREIGN_BROWSER_VERSION_H #define SOVEREIGN_BROWSER_VERSION_H -#define SB_VERSION "v0.0.55" +#define SB_VERSION "v0.0.56" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 55 +#define SB_VERSION_PATCH 56 #endif /* SOVEREIGN_BROWSER_VERSION_H */