Menu
popagent
publicLatest change 48f38756b02cd3f67b173ba9e5d7a5e122c03126 - Route read-only agents through fast model by Ólafur Búi Ólafsson
import { AgentBrowser } from "@mastra/agent-browser";
import type { Agent } from "@mastra/core/agent";
import { beforeAll, describe, expect, test } from "bun:test";
import { assertBoundSecretOrigin, BrowserRuntime, BrowserSettingsStore } from "./browser-settings";
import type { AgentSettings, BrowserSettings } from "./api-types";
const store = new BrowserSettingsStore();
beforeAll(() => store.init());
describe("BrowserSettingsStore", () => {
test("persists the complete browser runtime configuration", async () => {
const original = await store.get();
const { updatedAt: _updatedAt, ...originalInput } = original;
const input = {
...originalInput,
enabled: !original.enabled,
scope: original.scope === "thread" ? "shared" as const : "thread" as const,
viewportWidth: original.viewportWidth === 1280 ? 1440 : 1280,
viewportHeight: original.viewportHeight === 720 ? 900 : 720,
timeoutMs: original.timeoutMs === 30_000 ? 45_000 : 30_000,
maxSessions: original.maxSessions === 4 ? 3 : 4,
idleTimeoutMs: original.idleTimeoutMs === 900_000 ? 600_000 : 900_000,
screencastEnabled: !original.screencastEnabled,
screenshotsEnabled: !original.screenshotsEnabled,
multiTabEnabled: !original.multiTabEnabled,
formsEnabled: !original.formsEnabled,
dialogsEnabled: !original.dialogsEnabled,
dragEnabled: !original.dragEnabled,
evaluateEnabled: !original.evaluateEnabled,
recordingEnabled: !original.recordingEnabled,
recordingRetentionDays: original.recordingRetentionDays === 7 ? 14 : 7,
recordingMaxFiles: original.recordingMaxFiles === 25 ? 20 : 25,
allowHosts: ["docs.example.com"],
denyHosts: ["blocked.example.com"],
};
try {
expect(await store.update(input)).toMatchObject(input);
expect(await new BrowserSettingsStore().get()).toMatchObject(input);
} finally {
await store.update(originalInput);
}
});
});
describe("BrowserRuntime", () => {
const runtimeSettings = (enabled: boolean): BrowserSettings => ({
enabled,
scope: "thread",
viewportWidth: 1280,
viewportHeight: 720,
timeoutMs: 30_000,
maxSessions: 4,
idleTimeoutMs: 900_000,
screencastEnabled: true,
screenshotsEnabled: true,
multiTabEnabled: true,
formsEnabled: true,
dialogsEnabled: true,
dragEnabled: true,
evaluateEnabled: false,
recordingEnabled: false,
recordingRetentionDays: 7,
recordingMaxFiles: 25,
allowHosts: [],
denyHosts: [],
updatedAt: "",
});
test("requires the page and target element to match one exact origin", () => {
expect(assertBoundSecretOrigin(
"https://example.com/login?next=%2Faccount",
"https://example.com",
"https://example.com",
)).toBe("https://example.com");
expect(() => assertBoundSecretOrigin(
"https://attacker.example/login",
"https://attacker.example",
"https://example.com",
)).toThrow("current page");
expect(() => assertBoundSecretOrigin(
"https://example.com/login",
"https://identity.example",
"https://example.com",
)).toThrow("target element");
});
test("restricts read-only specialists to non-mutating browser tools", async () => {
const applied = new Map<string, AgentBrowser | undefined>();
const calls: string[] = [];
const target = (id: string) => ({
setBrowser: (browser?: AgentBrowser) => {
calls.push(id);
applied.set(id, browser);
},
});
const specialists = {
researcher: target("researcher"),
implementer: target("implementer"),
reviewer: target("reviewer"),
};
const profiles: AgentSettings[] = [
["popagent", "interactive"],
["researcher", "read-only"],
["implementer", "interactive"],
["reviewer", "read-only"],
].map(([id, browserAccess]) => ({
id: id!,
name: id!,
description: `${id} role`,
instructions: `${id} instructions`,
model: null,
workspaceAccess: id === "popagent" || id === "implementer" ? "read-write" : "read-only",
browserAccess: browserAccess as AgentSettings["browserAccess"],
delegationEnabled: id !== "popagent",
tools: ["webSearch"],
sourceUrls: [],
createdAt: "",
updatedAt: "",
}));
const supervisor = {
id: "popagent",
listAgents: async () => specialists,
...target("supervisor"),
} as unknown as Agent;
const runtime = new BrowserRuntime(
{} as BrowserSettingsStore,
{ list: async () => profiles },
);
await runtime.apply(supervisor, runtimeSettings(true));
await runtime.apply(supervisor, runtimeSettings(true));
expect(calls).toEqual(["supervisor", "researcher", "implementer", "reviewer"]);
expect(applied.get("supervisor")).toBe(applied.get("implementer"));
expect(applied.get("researcher")).toBe(applied.get("reviewer"));
expect(applied.get("researcher")).not.toBe(applied.get("supervisor"));
expect(Object.keys(applied.get("supervisor")!.getTools())).toContain("browser_click");
expect(Object.keys(applied.get("researcher")!.getTools())).not.toEqual(expect.arrayContaining([
"browser_click",
"browser_type",
"browser_press",
"browser_select",
"browser_dialog",
"browser_drag",
"browser_evaluate",
]));
await runtime.apply(supervisor, runtimeSettings(false));
expect([...applied.values()].every((browser) => browser === undefined)).toBe(true);
});
});