Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6af0179b0 | ||
|
|
fc0e412cc5 | ||
|
|
d64bd45f54 |
@@ -163,16 +163,79 @@ is saved to `~/.sovereign_browser/session.txt`.
|
||||
See [`plans/browser-tabs.md`](plans/browser-tabs.md) for the full
|
||||
implementation plan.
|
||||
|
||||
### Agent tools
|
||||
|
||||
The browser embeds a WebSocket server (using libsoup) that allows external
|
||||
AI agents to control the browser programmatically. Connect to
|
||||
`ws://localhost:17777/agent` and send JSON tool commands.
|
||||
|
||||
The server starts before the login dialog and remains available throughout
|
||||
the browser's lifecycle. The browser starts normally with the GTK login
|
||||
dialog — no blocking wait. An agent can log in at any time: while the dialog
|
||||
is showing, or after the browser is already running. If an agent logs in
|
||||
while the dialog is open, the agent's login takes priority.
|
||||
|
||||
**Login tools** (available before login):
|
||||
|
||||
- `login_status` — check if logged in
|
||||
- `login` — authenticate with method: `local` (nsec), `seed` (BIP-39
|
||||
mnemonic), `readonly` (npub), `nip46` (bunker:// URL), or `nsigner`
|
||||
(hardware signer via serial/unix/tcp/qrexec transport)
|
||||
- `logout` — clear signer and reset state
|
||||
- `switch_identity` — change identity (same params as login)
|
||||
|
||||
**Browser tools** (available after login):
|
||||
|
||||
- Navigation: `open`, `back`, `forward`, `reload`, `get_url`, `get_title`
|
||||
- Inspection: `snapshot` (accessibility tree with element refs), `get_text`,
|
||||
`get_html`, `get_attr`, `eval` (run JavaScript)
|
||||
- Interaction: `click`, `fill`, `type`, `press`, `scroll`, `hover`,
|
||||
`focus`, `close`
|
||||
- Tabs: `tab_list`, `tab_new`, `tab_switch`, `tab_close`
|
||||
- Wait: `wait` (ms), `wait_for` (selector)
|
||||
|
||||
**Snapshot + ref workflow** (same pattern as agent-browser):
|
||||
|
||||
```bash
|
||||
# Using websocat (cargo install websocat)
|
||||
# 1. Login
|
||||
echo '{"id":1,"tool":"login","params":{"method":"readonly","npub":"npub1..."}}' | websocat ws://localhost:17777/agent
|
||||
|
||||
# 2. Open a page
|
||||
echo '{"id":2,"tool":"open","params":{"url":"https://example.com"}}' | websocat ws://localhost:17777/agent
|
||||
|
||||
# 3. Snapshot — get accessibility tree with refs
|
||||
echo '{"id":3,"tool":"snapshot","params":{"interactive":true}}' | websocat ws://localhost:17777/agent
|
||||
|
||||
# 4. Click by ref
|
||||
echo '{"id":4,"tool":"click","params":{"ref":"@e2"}}' | websocat ws://localhost:17777/agent
|
||||
```
|
||||
|
||||
**HTTP status endpoint:** `curl http://localhost:17777/` returns server
|
||||
status (running, port, client count, logged in).
|
||||
|
||||
**Agent settings** (in Settings dialog or `~/.sovereign_browser/settings.conf`):
|
||||
|
||||
- `agent_server_enabled` (default: true)
|
||||
- `agent_server_port` (default: 17777)
|
||||
- `agent_allowed_origins` (default: `*`)
|
||||
- `agent_login_timeout_ms` (default: 30000)
|
||||
|
||||
See [`plans/agent-tools.md`](plans/agent-tools.md) for the full
|
||||
implementation plan and tool comparison with agent-browser.
|
||||
|
||||
## Roadmap
|
||||
|
||||
1. ✅ Base browser (WebKitGTK + C99, loads pages)
|
||||
2. ✅ Browser tabs (GtkNotebook, per-tab toolbar, session restore, settings)
|
||||
3. ⏳ Nostr login (GTK dialog → nostr_core_lib → nostr_signer_t)
|
||||
4. ⏳ `window.nostr` injection (`sovereign://` URI scheme bridge)
|
||||
5. ⏳ Security strip (disable SOP/CORS, accept any cert, shared context)
|
||||
6. ⏳ FIPS URI scheme (`fips://` / `*.fips` via WebKitGTK scheme handler)
|
||||
7. ⏳ `nostr://` content scheme
|
||||
8. Future: agent runtime (didactyl hosting), multi-window, FIPS-distributed
|
||||
3. ✅ Agent tools Phase 1 (WebSocket server, login tools, 31 browser automation tools)
|
||||
4. ⏳ Nostr login (GTK dialog → nostr_core_lib → nostr_signer_t)
|
||||
5. ⏳ `window.nostr` injection (`sovereign://` URI scheme bridge)
|
||||
6. ⏳ Security strip (disable SOP/CORS, accept any cert, shared context)
|
||||
7. ⏳ FIPS URI scheme (`fips://` / `*.fips` via WebKitGTK scheme handler)
|
||||
8. ⏳ `nostr://` content scheme
|
||||
9. ⏳ Agent tools Phase 2 (refinements) and Phase 3 (extended tool catalog)
|
||||
10. Future: agent runtime (didactyl hosting), multi-window, FIPS-distributed
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+19
-5
@@ -27,6 +27,9 @@ extern const char *app_get_pubkey_hex(void);
|
||||
extern key_store_method_t app_get_method(void);
|
||||
extern gboolean app_get_readonly(void);
|
||||
|
||||
/* Track whether the agent (not the GTK dialog) performed the login. */
|
||||
static gboolean g_agent_performed_login = FALSE;
|
||||
|
||||
/* ── Helper functions (same logic as login_dialog.c) ──────────────── */
|
||||
|
||||
static int derive_pubkey_hex(const unsigned char privkey[32], char pubkey_hex[65]) {
|
||||
@@ -389,19 +392,26 @@ cJSON *agent_login(cJSON *params) {
|
||||
nostr_initialized = TRUE;
|
||||
}
|
||||
|
||||
cJSON *result = NULL;
|
||||
if (strcmp(method, "local") == 0) {
|
||||
return login_local(params);
|
||||
result = login_local(params);
|
||||
} else if (strcmp(method, "seed") == 0) {
|
||||
return login_seed(params);
|
||||
result = login_seed(params);
|
||||
} else if (strcmp(method, "readonly") == 0) {
|
||||
return login_readonly(params);
|
||||
result = login_readonly(params);
|
||||
} else if (strcmp(method, "nip46") == 0) {
|
||||
return login_nip46(params);
|
||||
result = login_nip46(params);
|
||||
} else if (strcmp(method, "nsigner") == 0) {
|
||||
return login_nsigner(params);
|
||||
result = login_nsigner(params);
|
||||
} else {
|
||||
return make_error("UNKNOWN_METHOD", "Unknown method. Use: local, seed, readonly, nip46, or nsigner");
|
||||
}
|
||||
|
||||
/* If login succeeded, mark that the agent performed it. */
|
||||
if (result && cJSON_IsTrue(cJSON_GetObjectItem(result, "success"))) {
|
||||
g_agent_performed_login = TRUE;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
cJSON *agent_logout(void) {
|
||||
@@ -423,3 +433,7 @@ gboolean agent_is_logged_in(void) {
|
||||
return (app_get_signer() != NULL) ||
|
||||
(app_get_method() == KEY_STORE_METHOD_READONLY && app_get_pubkey_hex()[0] != '\0');
|
||||
}
|
||||
|
||||
gboolean agent_login_was_performed_by_agent(void) {
|
||||
return g_agent_performed_login;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ cJSON *agent_switch_identity(cJSON *params);
|
||||
*/
|
||||
gboolean agent_is_logged_in(void);
|
||||
|
||||
/*
|
||||
* Returns TRUE if the agent (not the GTK dialog) performed the login.
|
||||
* Used by login_dialog.c to close the dialog when the agent logs in.
|
||||
*/
|
||||
gboolean agent_login_was_performed_by_agent(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+24
-4
@@ -45,6 +45,9 @@ 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);
|
||||
@@ -56,8 +59,11 @@ static void track_client(SoupWebsocketConnection *conn) {
|
||||
|
||||
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_print("[agent] Client disconnected (%d remaining)\n", g_clients->len);
|
||||
g_object_unref(conn);
|
||||
g_print("[agent] Client disconnected (%d remaining)\n",
|
||||
g_clients ? g_clients->len : 0);
|
||||
}
|
||||
|
||||
/* ── Send helpers ─────────────────────────────────────────────────── */
|
||||
@@ -86,23 +92,35 @@ static void on_ws_message(SoupWebsocketConnection *conn,
|
||||
(void)user_data;
|
||||
(void)type;
|
||||
|
||||
g_print("[agent] on_ws_message called\n");
|
||||
|
||||
gsize size = 0;
|
||||
const gchar *data = g_bytes_get_data(message, &size);
|
||||
if (data == NULL || size == 0) return;
|
||||
if (data == NULL || size == 0) {
|
||||
g_print("[agent] on_ws_message: empty data\n");
|
||||
return;
|
||||
}
|
||||
|
||||
g_print("[agent] on_ws_message: %.*s\n", (int)(size > 200 ? 200 : size), data);
|
||||
|
||||
/* Parse the JSON request. */
|
||||
cJSON *request = cJSON_ParseWithLength(data, size);
|
||||
if (request == NULL) {
|
||||
g_print("[agent] on_ws_message: JSON parse failed\n");
|
||||
const char *err_json = "{\"success\":false,\"error\":{\"code\":\"INVALID_JSON\","
|
||||
"\"message\":\"Failed to parse JSON request\"}}";
|
||||
send_json_to_client(conn, err_json);
|
||||
return;
|
||||
}
|
||||
|
||||
g_print("[agent] on_ws_message: dispatching tool\n");
|
||||
|
||||
/* Dispatch the tool. */
|
||||
cJSON *response = agent_tools_dispatch(request);
|
||||
cJSON_Delete(request);
|
||||
|
||||
g_print("[agent] on_ws_message: dispatch returned %p\n", (void*)response);
|
||||
|
||||
if (response == NULL) {
|
||||
const char *err_json = "{\"success\":false,\"error\":{\"code\":\"INTERNAL_ERROR\","
|
||||
"\"message\":\"Tool dispatch returned NULL\"}}";
|
||||
@@ -229,11 +247,13 @@ int agent_server_start(int port) {
|
||||
void agent_server_stop(void) {
|
||||
if (!g_running) return;
|
||||
|
||||
/* Close all client connections. */
|
||||
/* 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");
|
||||
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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "login_dialog.h"
|
||||
#include "key_store.h"
|
||||
#include "agent_login.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -1053,6 +1054,18 @@ static void on_detect_serial(GtkWidget *btn, gpointer user_data) {
|
||||
|
||||
/* ── Main dialog ──────────────────────────────────────────────── */
|
||||
|
||||
/* Timeout callback to check if the agent has logged in.
|
||||
* If so, close the dialog automatically. */
|
||||
static gboolean agent_login_check(gpointer data) {
|
||||
GtkDialog *dialog = GTK_DIALOG(data);
|
||||
if (agent_login_was_performed_by_agent()) {
|
||||
g_print("[login] Agent login detected, closing dialog.\n");
|
||||
gtk_dialog_response(dialog, GTK_RESPONSE_ACCEPT);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
int login_dialog_run(GtkWindow *parent, login_result_t *result) {
|
||||
memset(result, 0, sizeof(*result));
|
||||
|
||||
@@ -1147,8 +1160,28 @@ int login_dialog_run(GtkWindow *parent, login_result_t *result) {
|
||||
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
/* Add a timeout to check if the agent has logged in. If so,
|
||||
* close the dialog automatically (every 200ms). */
|
||||
guint agent_check_id = g_timeout_add(200, agent_login_check, dialog);
|
||||
|
||||
gint response = gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
|
||||
/* Remove the timeout if it's still active. */
|
||||
g_source_remove(agent_check_id);
|
||||
|
||||
/* If the agent logged in while the dialog was showing, return 0
|
||||
* (success) but with an empty result — main.c will use the agent's
|
||||
* login state instead. */
|
||||
if (agent_login_was_performed_by_agent()) {
|
||||
if (result->signer) {
|
||||
nostr_signer_free(result->signer);
|
||||
result->signer = NULL;
|
||||
}
|
||||
memset(result, 0, sizeof(*result));
|
||||
gtk_widget_destroy(dialog);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!ctx.done || response != GTK_RESPONSE_ACCEPT) {
|
||||
if (result->signer) {
|
||||
nostr_signer_free(result->signer);
|
||||
|
||||
+44
-40
@@ -60,7 +60,6 @@ typedef struct {
|
||||
static app_state_t g_state = {0};
|
||||
static GtkWindow *g_window = NULL;
|
||||
static gboolean g_logged_in = FALSE;
|
||||
static gboolean g_agent_login_done = FALSE;
|
||||
|
||||
/* ---- App state accessors (used by agent_login.c) ─────────────────── */
|
||||
|
||||
@@ -417,11 +416,14 @@ static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
|
||||
|
||||
/* ---- Agent login callback ------------------------------------------ *
|
||||
* Called by agent_server when an agent successfully logs in via the
|
||||
* 'login' tool. Sets the flag so the main loop can proceed.
|
||||
* 'login' tool. The agent can log in at any time — while the GTK login
|
||||
* dialog is showing, or after the browser is already running. We just
|
||||
* set the flag; the do_login function checks it after the dialog
|
||||
* returns and skips the dialog's result if the agent already logged in.
|
||||
*/
|
||||
static void agent_login_callback(void) {
|
||||
g_agent_login_done = TRUE;
|
||||
g_print("[login] Agent login detected, proceeding.\n");
|
||||
g_logged_in = TRUE;
|
||||
g_print("[login] Agent login detected.\n");
|
||||
}
|
||||
|
||||
/* ---- Window destroy ------------------------------------------------- */
|
||||
@@ -444,15 +446,36 @@ static void on_window_destroy(GtkWidget *widget, gpointer data) {
|
||||
gtk_main_quit();
|
||||
}
|
||||
|
||||
/* ---- Login flow (GTK dialog fallback) ───────────────────────────── */
|
||||
/* ---- Login flow (GTK dialog) ─────────────────────────────────────── *
|
||||
* Shows the GTK login dialog. The agent server is already running at
|
||||
* this point, so an agent can call 'login' while the dialog is showing
|
||||
* (gtk_dialog_run runs a nested main loop that processes WebSocket
|
||||
* events). After the dialog returns, we check if the agent already
|
||||
* logged in — if so, we discard the dialog's result.
|
||||
*/
|
||||
|
||||
static int do_login(GtkWindow *parent) {
|
||||
/* nostr_init() is already called before this function. */
|
||||
login_result_t result;
|
||||
if (login_dialog_run(parent, &result) != 0) {
|
||||
/* Dialog was cancelled. But if the agent logged in while the
|
||||
* dialog was showing, proceed with the agent's login. */
|
||||
if (g_logged_in) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* If the agent already logged in while the dialog was showing,
|
||||
* discard the dialog's result and use the agent's login. */
|
||||
if (g_logged_in) {
|
||||
g_print("[login] Agent login took priority over dialog.\n");
|
||||
if (result.signer) {
|
||||
nostr_signer_free(result.signer);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_state.signer = result.signer;
|
||||
g_state.method = result.method;
|
||||
strncpy(g_state.pubkey_hex, result.pubkey_hex, 64);
|
||||
@@ -486,9 +509,11 @@ int main(int argc, char **argv) {
|
||||
g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL);
|
||||
g_window = GTK_WINDOW(window);
|
||||
|
||||
/* Start the agent server before login so agents can authenticate
|
||||
* without human interaction. The server runs on the configured port
|
||||
* (default 17777). If disabled in settings, skip it. */
|
||||
/* Start the agent server before login. The server runs on the
|
||||
* configured port (default 17777) and is available throughout the
|
||||
* entire browser lifecycle — an agent can log in at any time, even
|
||||
* while the GTK login dialog is showing or after the browser is
|
||||
* already running. If disabled in settings, skip it. */
|
||||
const browser_settings_t *s = settings_get();
|
||||
if (s->agent_server_enabled) {
|
||||
if (agent_server_start(s->agent_server_port) == 0) {
|
||||
@@ -507,40 +532,19 @@ int main(int argc, char **argv) {
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* Login flow: wait for agent login or fall back to GTK dialog.
|
||||
* If the agent server is running, wait up to agent_login_timeout_ms
|
||||
* for an agent to call the 'login' tool. If no agent connects or
|
||||
* the timeout expires, show the GTK login dialog. */
|
||||
if (s->agent_server_enabled && s->agent_login_timeout_ms > 0) {
|
||||
g_print("[login] Waiting up to %d ms for agent login...\n",
|
||||
s->agent_login_timeout_ms);
|
||||
/* Set up the login callback so if an agent calls 'login' while the
|
||||
* GTK dialog is showing, we can close the dialog and proceed. */
|
||||
agent_server_set_login_callback(agent_login_callback);
|
||||
|
||||
/* Run a main loop with a timeout so the agent server can process
|
||||
* incoming WebSocket connections and login commands. */
|
||||
g_agent_login_done = FALSE;
|
||||
agent_server_set_login_callback(agent_login_callback);
|
||||
|
||||
GMainContext *ctx = g_main_context_get_thread_default();
|
||||
gint64 deadline = g_get_monotonic_time() +
|
||||
(gint64)s->agent_login_timeout_ms * 1000;
|
||||
|
||||
while (!g_agent_login_done) {
|
||||
gint64 remaining = deadline - g_get_monotonic_time();
|
||||
if (remaining <= 0) break;
|
||||
|
||||
/* Process events for up to 200ms at a time. */
|
||||
g_main_context_iteration(ctx, FALSE);
|
||||
if (!g_agent_login_done) {
|
||||
g_usleep(50 * 1000); /* 50ms */
|
||||
}
|
||||
}
|
||||
|
||||
agent_server_set_login_callback(NULL);
|
||||
}
|
||||
|
||||
/* If not logged in via agent, show the GTK login dialog. */
|
||||
/* Show the GTK login dialog immediately — no blocking wait.
|
||||
* The dialog runs a nested GTK main loop (gtk_dialog_run) which
|
||||
* processes WebSocket events, so the agent server is live during
|
||||
* the dialog. If an agent calls 'login' while the dialog is open,
|
||||
* the callback fires and we close the dialog.
|
||||
*
|
||||
* If the agent logs in before the dialog appears (unlikely but
|
||||
* possible), skip the dialog entirely. */
|
||||
if (!g_logged_in) {
|
||||
g_print("[login] No agent login, showing GTK dialog.\n");
|
||||
if (do_login(g_window) != 0) {
|
||||
g_print("[login] Cancelled, exiting.\n");
|
||||
agent_server_stop();
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@
|
||||
#ifndef SOVEREIGN_BROWSER_VERSION_H
|
||||
#define SOVEREIGN_BROWSER_VERSION_H
|
||||
|
||||
#define SB_VERSION "v0.0.5"
|
||||
#define SB_VERSION "v0.0.8"
|
||||
#define SB_VERSION_MAJOR 0
|
||||
#define SB_VERSION_MINOR 0
|
||||
#define SB_VERSION_PATCH 5
|
||||
#define SB_VERSION_PATCH 8
|
||||
|
||||
#endif /* SOVEREIGN_BROWSER_VERSION_H */
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test all agent login functionality for sovereign_browser.
|
||||
Connects to the WebSocket agent server and tests each login method.
|
||||
"""
|
||||
|
||||
import websocket
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
SERVER_URL = "ws://localhost:17777/agent"
|
||||
HTTP_URL = "http://localhost:17777/"
|
||||
|
||||
def send_tool(ws, tool_name, params=None, msg_id=1):
|
||||
"""Send a tool command and return the response."""
|
||||
msg = {"id": msg_id, "tool": tool_name, "params": params or {}}
|
||||
ws.send(json.dumps(msg))
|
||||
result = ws.recv()
|
||||
return json.loads(result)
|
||||
|
||||
def test_status():
|
||||
"""Test HTTP status endpoint."""
|
||||
import urllib.request
|
||||
try:
|
||||
resp = urllib.request.urlopen(HTTP_URL, timeout=5)
|
||||
data = json.loads(resp.read())
|
||||
print(f"[PASS] HTTP status: {data}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[FAIL] HTTP status: {e}")
|
||||
return False
|
||||
|
||||
def test_login_status_not_logged_in():
|
||||
"""Test login_status before login."""
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=10)
|
||||
resp = send_tool(ws, "login_status", {}, 1)
|
||||
ws.close()
|
||||
if resp.get("success") and not resp["data"]["logged_in"]:
|
||||
print(f"[PASS] login_status (not logged in): {resp}")
|
||||
return True
|
||||
else:
|
||||
print(f"[FAIL] login_status (not logged in): {resp}")
|
||||
return False
|
||||
|
||||
def test_login_local():
|
||||
"""Test login with a random local key."""
|
||||
privkey = subprocess.check_output(["openssl", "rand", "-hex", "32"]).decode().strip()
|
||||
print(f" Generated privkey: {privkey}")
|
||||
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=10)
|
||||
resp = send_tool(ws, "login", {"method": "local", "privkey_hex": privkey}, 2)
|
||||
ws.close()
|
||||
|
||||
if resp.get("success") and resp["data"]["pubkey"]:
|
||||
print(f"[PASS] login local: pubkey={resp['data']['pubkey']}, npub={resp['data']['npub']}")
|
||||
return True, privkey
|
||||
else:
|
||||
print(f"[FAIL] login local: {resp}")
|
||||
return False, privkey
|
||||
|
||||
def test_login_already_logged_in():
|
||||
"""Test that login fails when already logged in."""
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=10)
|
||||
resp = send_tool(ws, "login", {"method": "readonly", "npub": "npub1invalid"}, 3)
|
||||
ws.close()
|
||||
|
||||
if not resp.get("success") and "ALREADY_LOGGED_IN" in resp.get("error", {}).get("code", ""):
|
||||
print(f"[PASS] login already logged in rejected: {resp}")
|
||||
return True
|
||||
else:
|
||||
print(f"[FAIL] login already logged in: {resp}")
|
||||
return False
|
||||
|
||||
def test_login_status_logged_in():
|
||||
"""Test login_status after login."""
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=10)
|
||||
resp = send_tool(ws, "login_status", {}, 4)
|
||||
ws.close()
|
||||
|
||||
if resp.get("success") and resp["data"]["logged_in"]:
|
||||
print(f"[PASS] login_status (logged in): method={resp['data']['method']}, pubkey={resp['data']['pubkey']}")
|
||||
return True
|
||||
else:
|
||||
print(f"[FAIL] login_status (logged in): {resp}")
|
||||
return False
|
||||
|
||||
def test_logout():
|
||||
"""Test logout."""
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=10)
|
||||
resp = send_tool(ws, "logout", {}, 5)
|
||||
ws.close()
|
||||
|
||||
if resp.get("success"):
|
||||
print(f"[PASS] logout: {resp}")
|
||||
return True
|
||||
else:
|
||||
print(f"[FAIL] logout: {resp}")
|
||||
return False
|
||||
|
||||
def test_login_readonly():
|
||||
"""Test login with readonly npub."""
|
||||
# First generate a key and get the npub
|
||||
privkey = subprocess.check_output(["openssl", "rand", "-hex", "32"]).decode().strip()
|
||||
|
||||
# Login with local to get the npub, then logout and test readonly
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=10)
|
||||
resp = send_tool(ws, "login", {"method": "local", "privkey_hex": privkey}, 6)
|
||||
if not resp.get("success"):
|
||||
print(f"[FAIL] login readonly (setup): {resp}")
|
||||
ws.close()
|
||||
return False
|
||||
|
||||
npub = resp["data"]["npub"]
|
||||
pubkey = resp["data"]["pubkey"]
|
||||
print(f" Generated npub: {npub}")
|
||||
|
||||
# Logout
|
||||
resp2 = send_tool(ws, "logout", {}, 7)
|
||||
if not resp2.get("success"):
|
||||
print(f"[FAIL] login readonly (logout): {resp2}")
|
||||
ws.close()
|
||||
return False
|
||||
|
||||
# Login readonly with the npub
|
||||
resp3 = send_tool(ws, "login", {"method": "readonly", "npub": npub}, 8)
|
||||
ws.close()
|
||||
|
||||
if resp3.get("success") and resp3["data"]["readonly"]:
|
||||
print(f"[PASS] login readonly: pubkey={resp3['data']['pubkey']}, readonly={resp3['data']['readonly']}")
|
||||
return True
|
||||
else:
|
||||
print(f"[FAIL] login readonly: {resp3}")
|
||||
return False
|
||||
|
||||
def test_switch_identity():
|
||||
"""Test switch_identity."""
|
||||
privkey = subprocess.check_output(["openssl", "rand", "-hex", "32"]).decode().strip()
|
||||
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=10)
|
||||
resp = send_tool(ws, "switch_identity", {"method": "local", "privkey_hex": privkey}, 9)
|
||||
ws.close()
|
||||
|
||||
if resp.get("success"):
|
||||
print(f"[PASS] switch_identity: pubkey={resp['data']['pubkey']}")
|
||||
return True
|
||||
else:
|
||||
print(f"[FAIL] switch_identity: {resp}")
|
||||
return False
|
||||
|
||||
def test_browser_tools_after_login():
|
||||
"""Test that browser tools work after login."""
|
||||
ws = websocket.create_connection(SERVER_URL, timeout=15)
|
||||
|
||||
# Open a page
|
||||
resp = send_tool(ws, "open", {"url": "https://example.com"}, 10)
|
||||
print(f" open: {resp.get('success')}")
|
||||
|
||||
# Wait a moment for the page to load
|
||||
time.sleep(2)
|
||||
|
||||
# Get URL
|
||||
resp = send_tool(ws, "get_url", {}, 11)
|
||||
print(f" get_url: {resp}")
|
||||
|
||||
# Get title
|
||||
resp = send_tool(ws, "get_title", {}, 12)
|
||||
print(f" get_title: {resp}")
|
||||
|
||||
# Snapshot
|
||||
resp = send_tool(ws, "snapshot", {"interactive": True, "compact": True}, 13)
|
||||
if resp.get("success") and resp["data"].get("snapshot"):
|
||||
snapshot = resp["data"]["snapshot"]
|
||||
print(f"[PASS] snapshot: {len(snapshot)} chars, {resp['data'].get('refCount', 0)} refs")
|
||||
# Print first few lines
|
||||
for line in snapshot.split("\n")[:5]:
|
||||
print(f" {line}")
|
||||
else:
|
||||
print(f"[FAIL] snapshot: {resp}")
|
||||
|
||||
# Tab list
|
||||
resp = send_tool(ws, "tab_list", {}, 14)
|
||||
if resp.get("success"):
|
||||
tabs = resp["data"]["tabs"]
|
||||
print(f"[PASS] tab_list: {len(tabs)} tab(s)")
|
||||
else:
|
||||
print(f"[FAIL] tab_list: {resp}")
|
||||
|
||||
ws.close()
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("sovereign_browser agent login test suite")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
|
||||
# Test 1: HTTP status
|
||||
print("\n--- Test 1: HTTP status endpoint ---")
|
||||
results.append(test_status())
|
||||
|
||||
# Test 2: login_status (not logged in)
|
||||
print("\n--- Test 2: login_status (not logged in) ---")
|
||||
results.append(test_login_status_not_logged_in())
|
||||
|
||||
# Test 3: login with local key
|
||||
print("\n--- Test 3: login with local key ---")
|
||||
ok, _ = test_login_local()
|
||||
results.append(ok)
|
||||
|
||||
# Test 4: login when already logged in (should fail)
|
||||
print("\n--- Test 4: login when already logged in ---")
|
||||
results.append(test_login_already_logged_in())
|
||||
|
||||
# Test 5: login_status (logged in)
|
||||
print("\n--- Test 5: login_status (logged in) ---")
|
||||
results.append(test_login_status_logged_in())
|
||||
|
||||
# Test 6: logout
|
||||
print("\n--- Test 6: logout ---")
|
||||
results.append(test_logout())
|
||||
|
||||
# Test 7: login readonly
|
||||
print("\n--- Test 7: login readonly ---")
|
||||
results.append(test_login_readonly())
|
||||
|
||||
# Test 8: switch_identity
|
||||
print("\n--- Test 8: switch_identity ---")
|
||||
results.append(test_switch_identity())
|
||||
|
||||
# Test 9: browser tools after login
|
||||
print("\n--- Test 9: browser tools after login ---")
|
||||
results.append(test_browser_tools_after_login())
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
passed = sum(results)
|
||||
total = len(results)
|
||||
print(f"Results: {passed}/{total} passed")
|
||||
if passed == total:
|
||||
print("ALL TESTS PASSED")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("SOME TESTS FAILED")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user