AkurAI Build
Menu

popagent

public

Latest change 7fe5fe2acff014eed98cc59694a021837a6713c6 - Serialize code-intelligence sessions per repository and self-heal corrupt or locked indexes 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", (error: NodeJS.ErrnoException) => finish(
    error.code === "ENOENT" || error.code === "ECONNREFUSED"
      ? new Error(`BifrOSt Navigator is not running on this host (no MCP socket at ${path}); start it with --allow-mcp-capability … or select the headless browser provider`)
      : error,
  ));
  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();