Menu
popagent
publicLatest change 248b7c0673ea9d544f9e4738ee4d3e8ec2925924 - Add native BifrOSt Navigator browser provider with per-browser status LEDs by AkurAI Build
import { createConnection } from "node:net";
import { join } from "node:path";
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { agentSettings, type AgentSettingsStore } from "../agent-settings";
const operations = [
"browser_current_page", "browser_identity", "browser_get_setting", "browser_set_setting",
"browser_tabs", "browser_extract_text", "browser_navigate", "browser_open_tab",
"browser_activate_tab", "browser_close_tab", "browser_reload", "browser_back", "browser_forward",
"browser_elements", "browser_snapshot", "browser_console_logs", "browser_network_events",
"browser_screenshot", "browser_print_pdf", "browser_click", "browser_fill", "browser_select",
"browser_type", "browser_press", "browser_wait_for", "browser_hover", "browser_scroll",
"browser_performance", "browser_benchmark", "browser_page_status", "browser_wait_for_page",
"browser_events", "browser_find_text", "browser_downloads", "browser_download",
"browser_cancel_download", "browser_dialog", "browser_handle_dialog", "browser_permission",
"browser_handle_permission", "browser_upload_file", "browser_clear_site_data", "browser_sessions",
"browser_save_session", "browser_restore_session", "assistant_post_message", "assistant_messages",
"assistant_status",
] as const;
const interactiveOnly = new Set<(typeof operations)[number]>([
"browser_set_setting", "browser_navigate", "browser_open_tab", "browser_activate_tab", "browser_close_tab",
"browser_click", "browser_fill", "browser_select", "browser_type", "browser_press",
"browser_hover", "browser_scroll", "browser_download", "browser_cancel_download",
"browser_handle_dialog", "browser_handle_permission", "browser_upload_file",
"browser_reload", "browser_back", "browser_forward", "browser_clear_site_data",
"browser_save_session", "browser_restore_session", "assistant_post_message",
]);
type JsonObject = Record<string, unknown>;
type BifrOStResult = { content: Array<Record<string, unknown>>; structuredContent?: JsonObject };
type BifrOStCaller = (
operation: (typeof operations)[number],
args: JsonObject,
signal: AbortSignal,
) => Promise<BifrOStResult>;
function socketPath(): string {
return process.env.BIFROST_MCP_SOCKET
?? join(process.env.XDG_RUNTIME_DIR ?? `/run/user/${process.getuid?.() ?? 0}`, "bifrost-navigator.sock");
}
function requestBifrOSt(
method: string,
params: JsonObject,
signal: AbortSignal,
path: string,
): Promise<unknown> {
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
let settled = false;
let input = "";
const socket = createConnection({ path });
const finish = (error?: Error, result?: unknown) => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", abort);
socket.destroy();
if (error) reject(error);
else resolve(result);
};
const abort = () => finish(new Error("BifrOSt request aborted"));
if (signal.aborted) {
abort();
return promise;
}
signal.addEventListener("abort", abort, { once: true });
socket.setEncoding("utf8");
socket.setTimeout(15_000, () => finish(new Error("BifrOSt request timed out")));
socket.once("error", finish);
socket.once("connect", () => {
const request: JsonObject = { jsonrpc: "2.0", id: 1, method, params };
if (process.env.BIFROST_MCP_TOKEN) request.auth = process.env.BIFROST_MCP_TOKEN;
socket.write(`${JSON.stringify(request)}\n`);
});
socket.on("data", (chunk) => {
input += chunk;
if (input.length > 24 * 1024 * 1024) return finish(new Error("BifrOSt response exceeded 24 MiB"));
const newline = input.indexOf("\n");
if (newline < 0) return;
try {
const response = JSON.parse(input.slice(0, newline));
if (response?.jsonrpc !== "2.0" || response?.id !== 1) throw new Error("BifrOSt response ID mismatch");
if (response.error) throw new Error(String(response.error.message ?? "BifrOSt request failed"));
finish(undefined, response.result);
} catch (error) {
finish(error as Error);
}
});
return promise;
}
/** Reachability probe for the local BifrOSt MCP socket; resolves when the browser answers `ping`. */
export async function pingBifrOSt(signal: AbortSignal, path = socketPath()): Promise<void> {
await requestBifrOSt("ping", {}, signal, path);
}
/** Names of the tools BifrOSt currently exposes to this bridge (after its `--allow-mcp-capability` gating). */
export async function listBifrOStTools(signal: AbortSignal, path = socketPath()): Promise<string[]> {
const result = await requestBifrOSt("tools/list", {}, signal, path) as { tools?: Array<{ name?: unknown }> } | undefined;
return (result?.tools ?? []).map((tool) => tool.name).filter((name): name is string => typeof name === "string");
}
export async function callBifrOSt(
operation: (typeof operations)[number],
args: JsonObject,
signal: AbortSignal,
path = socketPath(),
): Promise<BifrOStResult> {
const result = await requestBifrOSt("tools/call", { name: operation, arguments: args }, signal, path) as
{ content?: unknown; structuredContent?: JsonObject } | undefined;
if (!Array.isArray(result?.content)) throw new Error("BifrOSt response content is missing");
return result as BifrOStResult;
}
export function createBifrOStTool(
roles: Pick<AgentSettingsStore, "get"> = agentSettings,
call: BifrOStCaller = callBifrOSt,
) {
return createTool({
id: "bifrost-navigator",
description: "Operate the user's live local BifrOSt Navigator through its protected Unix-socket bridge.",
inputSchema: z.object({
operation: z.enum(operations),
arguments: z.record(z.string(), z.unknown()).default({}),
}).strict(),
outputSchema: z.object({
content: z.array(z.record(z.string(), z.unknown())),
structuredContent: z.record(z.string(), z.unknown()).optional(),
}),
execute: async ({ operation, arguments: args }, context) => {
context.abortSignal?.throwIfAborted();
const agentId = context.agent?.agentId;
if (!agentId) throw new Error("BifrOSt requires an active agent role");
const role = await roles.get(agentId);
if (!role || role.browserAccess === "none") throw new Error("Agent role has no BifrOSt access");
if (role.browserAccess === "read-only" && interactiveOnly.has(operation)) {
throw new Error(`Agent role has read-only BifrOSt access: ${operation} is interactive`);
}
return call(operation, args, context.abortSignal ?? AbortSignal.timeout(15_000));
},
});
}
export const bifrostNavigator = createBifrOStTool();