307 lines
11 KiB
C
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();
|
|
}
|
|
}
|