Files
sovereign_browser/src/agent_server.c
T

307 lines
11 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>
/* Forward declarations from agent_tools.c */
extern cJSON *agent_tools_dispatch(cJSON *request, SoupWebsocketConnection *conn);
/* ── 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 (0 = auto-assign). */
soup_server_listen_local(g_server, port, 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);
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;
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();
}
}