Files
sovereign_browser/src/agent_server.c
T

365 lines
13 KiB
C

/*
* agent_server.c — WebSocket server for agent tool commands
*
* Uses libsoup's SoupServer to provide a WebSocket endpoint at /agent.
* External AI agents connect, send JSON tool commands, and receive
* JSON responses. The server also supports plain HTTP GET to / for
* status discovery (curl-testable without a WebSocket client).
*/
#include "agent_server.h"
#include "agent_mcp.h"
#include "settings.h"
#include <libsoup/soup.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
/* 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_<pid>.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;
static int g_port = 0;
static gboolean g_running = FALSE;
static GPtrArray *g_clients = NULL; /* array of SoupWebsocketConnection* */
static agent_login_callback_t g_login_cb = NULL;
/* ── Client management ────────────────────────────────────────────── */
static void on_ws_message(SoupWebsocketConnection *conn,
SoupWebsocketDataType type,
GBytes *message,
gpointer user_data);
static void on_ws_closed(SoupWebsocketConnection *conn, gpointer user_data);
static void on_ws_error(SoupWebsocketConnection *conn, gpointer user_data);
/* Forward declarations */
static void on_websocket_handler(SoupServer *server,
SoupServerMessage *msg,
const char *path,
SoupWebsocketConnection *conn,
gpointer user_data);
static void track_client(SoupWebsocketConnection *conn) {
if (g_clients == NULL) {
g_clients = g_ptr_array_new();
}
/* Take an extra reference so the connection isn't destroyed when
* the websocket handler callback returns. */
g_object_ref(conn);
g_ptr_array_add(g_clients, conn);
g_signal_connect(conn, "message", G_CALLBACK(on_ws_message), NULL);
g_signal_connect(conn, "closed", G_CALLBACK(on_ws_closed), NULL);
g_signal_connect(conn, "error", G_CALLBACK(on_ws_error), NULL);
g_print("[agent] Client connected (%d total)\n", g_clients->len);
}
static void untrack_client(SoupWebsocketConnection *conn) {
if (g_clients == NULL) return;
/* Remove from array and release our reference. */
g_ptr_array_remove_fast(g_clients, conn);
g_object_unref(conn);
g_print("[agent] Client disconnected (%d remaining)\n",
g_clients ? g_clients->len : 0);
}
/* ── Send helpers ─────────────────────────────────────────────────── */
static void send_json_to_client(SoupWebsocketConnection *conn, const char *json_str) {
if (soup_websocket_connection_get_state(conn) != SOUP_WEBSOCKET_STATE_OPEN) {
return;
}
soup_websocket_connection_send_text(conn, json_str);
}
static void send_json_to_all(const char *json_str) {
if (g_clients == NULL) return;
for (guint i = 0; i < g_clients->len; i++) {
SoupWebsocketConnection *conn = g_ptr_array_index(g_clients, i);
send_json_to_client(conn, json_str);
}
}
/* ── WebSocket callbacks ──────────────────────────────────────────── */
static void on_ws_message(SoupWebsocketConnection *conn,
SoupWebsocketDataType type,
GBytes *message,
gpointer user_data) {
(void)user_data;
(void)type;
gsize size = 0;
const gchar *data = g_bytes_get_data(message, &size);
if (data == NULL || size == 0) return;
/* Parse the JSON request. */
cJSON *request = cJSON_ParseWithLength(data, size);
if (request == NULL) {
const char *err_json = "{\"success\":false,\"error\":{\"code\":\"INVALID_JSON\","
"\"message\":\"Failed to parse JSON request\"}}";
send_json_to_client(conn, err_json);
return;
}
/* Dispatch the tool. Pass the WebSocket connection so async tools
* (snapshot, eval) can send their response directly when JS completes. */
cJSON *response = agent_tools_dispatch(request, conn);
cJSON_Delete(request);
if (response == NULL) {
/* NULL means the tool is sending its response asynchronously
* (e.g. snapshot, eval). Don't send anything here. */
return;
}
/* Send the response. */
char *response_str = cJSON_PrintUnformatted(response);
if (response_str) {
send_json_to_client(conn, response_str);
free(response_str);
}
cJSON_Delete(response);
}
static void on_ws_closed(SoupWebsocketConnection *conn, gpointer user_data) {
(void)user_data;
untrack_client(conn);
}
static void on_ws_error(SoupWebsocketConnection *conn, gpointer user_data) {
(void)user_data;
g_printerr("[agent] WebSocket error on client\n");
untrack_client(conn);
}
/* ── WebSocket handler (called by SoupServer on new connection) ───── */
static void on_websocket_handler(SoupServer *server,
SoupServerMessage *msg,
const char *path,
SoupWebsocketConnection *conn,
gpointer user_data) {
(void)server;
(void)path;
(void)msg;
(void)user_data;
track_client(conn);
}
/* ── HTTP handler for status (GET /) ──────────────────────────────── */
static void on_http_handler(SoupServer *server,
SoupServerMessage *msg,
const char *path,
GHashTable *query,
gpointer user_data) {
(void)server;
(void)query;
(void)user_data;
if (g_strcmp0(path, "/") != 0) {
soup_server_message_set_status(msg, 404, NULL);
return;
}
/* Build a status JSON response. */
cJSON *status = cJSON_CreateObject();
cJSON_AddStringToObject(status, "name", "sovereign_browser agent server");
cJSON_AddBoolToObject(status, "running", g_running);
cJSON_AddNumberToObject(status, "port", g_port);
cJSON_AddNumberToObject(status, "clients", g_clients ? g_clients->len : 0);
/* Check login state via agent_tools (delegates to agent_login). */
extern gboolean agent_is_logged_in(void);
cJSON_AddBoolToObject(status, "logged_in", agent_is_logged_in());
char *json_str = cJSON_PrintUnformatted(status);
soup_server_message_set_status(msg, 200, NULL);
soup_server_message_set_response(msg, "application/json",
SOUP_MEMORY_TAKE, json_str, strlen(json_str));
cJSON_Delete(status);
}
/* ── Public API ───────────────────────────────────────────────────── */
int agent_server_start(int port) {
if (g_running) {
g_print("[agent] Server already running on port %d\n", g_port);
return 0;
}
GError *error = NULL;
g_server = soup_server_new("server-header", "sovereign_browser-agent", NULL);
if (g_server == NULL) {
g_printerr("[agent] Failed to create SoupServer\n");
return -1;
}
/* Add HTTP handler for status at /. */
soup_server_add_handler(g_server, "/", on_http_handler, NULL, NULL);
/* Add WebSocket handler at /agent. */
soup_server_add_websocket_handler(g_server, "/agent", NULL, NULL,
on_websocket_handler, NULL, NULL);
/* Add MCP handler at /mcp. */
agent_mcp_register(g_server);
/* 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);
g_object_unref(g_server);
g_server = NULL;
return -1;
}
/* Get the actual bound port. */
GSList *uris = soup_server_get_uris(g_server);
if (uris != NULL) {
GUri *uri = uris->data;
g_port = g_uri_get_port(uri);
g_slist_free(uris);
} else {
g_port = 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;
}
void agent_server_stop(void) {
if (!g_running) return;
/* Close all client connections and release our references. */
if (g_clients != NULL) {
for (guint i = 0; i < g_clients->len; i++) {
SoupWebsocketConnection *conn = g_ptr_array_index(g_clients, i);
soup_websocket_connection_close(conn, SOUP_WEBSOCKET_CLOSE_GOING_AWAY,
"server shutting down");
g_object_unref(conn);
}
g_ptr_array_free(g_clients, TRUE);
g_clients = NULL;
}
if (g_server != NULL) {
g_object_unref(g_server);
g_server = NULL;
}
g_running = FALSE;
g_port = 0;
remove_discovery_file();
g_print("[agent] Server stopped\n");
}
int agent_server_get_port(void) {
return g_port;
}
gboolean agent_server_is_running(void) {
return g_running;
}
int agent_server_get_client_count(void) {
return g_clients ? g_clients->len : 0;
}
void agent_server_emit_event(const char *event_name, cJSON *data) {
if (!g_running || g_clients == NULL || g_clients->len == 0) {
if (data) cJSON_Delete(data);
return;
}
cJSON *event = cJSON_CreateObject();
cJSON_AddStringToObject(event, "type", "event");
cJSON_AddStringToObject(event, "event", event_name);
if (data) {
cJSON_AddItemToObject(event, "data", data);
}
char *json_str = cJSON_PrintUnformatted(event);
if (json_str) {
send_json_to_all(json_str);
free(json_str);
}
cJSON_Delete(event);
}
void agent_server_set_login_callback(agent_login_callback_t cb) {
g_login_cb = cb;
}
/* Called by agent_login.c when login succeeds — notifies main.c. */
void agent_server_notify_login(void) {
if (g_login_cb) {
g_login_cb();
}
}