/* * 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 #include #include /* 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(); } }