AkurAI Build
Menu

popagent

public

Latest change 8828a8233b015e0c6e7896c3e9ba8b060bd3ce5c - Add BifrOSt Navigator agent tool by Ólafur Búi Ólafsson

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");
}

export function callBifrOSt(
  operation: (typeof operations)[number],
  args: JsonObject,
  signal: AbortSignal,
  path = socketPath(),
): Promise<BifrOStResult> {
  const { promise, resolve, reject } = Promise.withResolvers<BifrOStResult>();
  let settled = false;
  let input = "";
  const socket = createConnection({ path });
  const finish = (error?: Error, result?: BifrOStResult) => {
    if (settled) return;
    settled = true;
    signal.removeEventListener("abort", abort);
    socket.destroy();
    if (error) reject(error);
    else resolve(result ?? { content: [] });
  };
  const abort = () => finish(new Error("BifrOSt request aborted"));
  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: "tools/call",
      params: { name: operation, arguments: args },
    };
    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"));
      if (!Array.isArray(response.result?.content)) throw new Error("BifrOSt response content is missing");
      finish(undefined, response.result as BifrOStResult);
    } catch (error) {
      finish(error as Error);
    }
  });
  return promise;
}

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();