feat(FN-3573): broaden runtime ignore list for live fusion app paths (+2 more)

Commits merged:
- chore(test-isolation): broaden runtime ignore list for live fusion app paths
- feat(FN-3573): complete Step 2 — add plugin setup API client helpers
- feat(FN-3573): complete Step 1 — backfill plugin setup routes

Files changed:
plugins/fusion-plugin-agent-browser/README.md      | 26 ++++++
 plugins/fusion-plugin-agent-browser/manifest.json  | 15 ++++
 plugins/fusion-plugin-agent-browser/package.json   | 25 ++++++
 .../src/__tests__/index.test.ts                    | 66 +++++++++++++++
 .../src/__tests__/prompts.test.ts                  | 21 +++++
 .../src/__tests__/setup.test.ts                    | 39 +++++++++
 .../src/__tests__/skills.test.ts                   |  9 ++
 .../src/__tests__/tools.test.ts                    | 55 ++++++++++++
 .../src/__tests__/types.test.ts                    | 16 ++++
 .../src/__tests__/workflow-steps.test.ts           | 10 +++
 plugins/fusion-plugin-agent-browser/src/index.ts   | 67 +++++++++++++++
 plugins/fusion-plugin-agent-browser/src/probe.ts   | 98 ++++++++++++++++++++++
 plugins/fusion-plugin-agent-browser/src/prompts.ts | 23 +++++
 plugins/fusion-plugin-agent-browser/src/setup.ts   | 31 +++++++
 plugins/fusion-plugin-agent-browser/src/skills.ts  | 12 +++
 plugins/fusion-plugin-agent-browser/src/tools.ts   | 34 ++++++++
 plugins/fusion-plugin-agent-browser/src/types.ts   | 56 +++++++++++++
 .../src/workflow-steps.ts                          | 15 ++++
 plugins/fusion-plugin-agent-browser/tsconfig.json  |  9 ++
 .../fusion-plugin-agent-browser/vitest.config.ts   | 22 +++++
 pnpm-lock.yaml                                     | 16 ++++
 pnpm-workspace.yaml                                |  1 +
 22 files changed, 666 insertions(+)

Fusion-Task-Id: FN-3573
This commit is contained in:
Fusion
2026-05-06 14:56:10 -07:00
committed by gsxdsm
parent d1549b8dff
commit cfa5ef6130
22 changed files with 666 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# fusion-plugin-agent-browser
First-party Fusion plugin that contributes:
- setup metadata/hooks for the `agent-browser` binary
- prompt contributions across executor/triage/reviewer/heartbeat surfaces
- agent-browser skills and workflow step templates
- optional browser helper tools
## Settings
- `enabled` (boolean, default `true`)
- `installChannel` (`stable|beta|nightly`, default `stable`)
- `commandTimeoutMs` (number, default `120000`)
- `headlessMode` (boolean, default `true`)
- `allowedDomains` (string array, default `[]`)
- `promptExecutorSystem` (string, default `"When browsing, summarize evidence with URLs."`)
- `promptExecutorTask` (string, default `"Use browser context only when needed for the task."`)
- `promptTriage` (string, default `"Mark tasks requiring browser evidence explicitly."`)
- `promptReviewer` (string, default `"Verify browser-derived claims are backed by cited pages."`)
- `promptHeartbeat` (string, default `"Keep browser interactions bounded and report failures clearly."`)
- `skillExposure` (`none|selected|all`, default `selected`)
## Setup hooks
`checkSetup()` probes `agent-browser --version` asynchronously with timeout handling and reports `installed`, `not-installed`, or `error` status via `PluginSetupCheckResult`.

View File

@@ -0,0 +1,15 @@
{
"id": "fusion-plugin-agent-browser",
"name": "Agent Browser Plugin",
"version": "0.1.0",
"description": "Adds agent-browser setup hooks plus skills, prompt contributions, and workflow steps",
"author": "Fusion Team",
"homepage": "https://github.com/gsxdsm/fusion",
"promptSurfaces": ["executor-system", "executor-task", "triage", "reviewer", "heartbeat"],
"setup": {
"binaryName": "agent-browser",
"description": "Headless browser runtime for web-enabled agents",
"channel": "stable",
"defaultTimeoutMs": 120000
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "@fusion-plugin-examples/agent-browser",
"version": "0.1.0",
"type": "module",
"description": "Agent Browser runtime and prompt/skill/workflow contributions for Fusion",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"test": "vitest run --silent=passed-only --reporter=dot"
},
"dependencies": {
"@fusion/plugin-sdk": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.5.2",
"typescript": "^5.7.0",
"vitest": "^3.2.4"
}
}

View File

@@ -0,0 +1,66 @@
import { describe, expect, it, vi } from "vitest";
import plugin, { AGENT_BROWSER_SETTINGS_SCHEMA } from "../index.js";
import manifestJson from "../../manifest.json";
const { probeMock } = vi.hoisted(() => ({
probeMock: vi.fn(async () => ({ available: false, reason: "`agent-browser` not found on PATH" } as { available: boolean; reason?: string; version?: string })),
}));
vi.mock("../probe.js", () => ({
probeAgentBrowserBinary: probeMock,
}));
describe("agent-browser plugin index", () => {
it("exports canonical plugin id and manifest metadata", () => {
expect(plugin.manifest.id).toBe("fusion-plugin-agent-browser");
expect(manifestJson.id).toBe("fusion-plugin-agent-browser");
expect(plugin.manifest.setup?.binaryName).toBe("agent-browser");
});
it("emits load event on successful probe", async () => {
probeMock.mockResolvedValueOnce({ available: true, version: "1.0.0" });
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
const emitEvent = vi.fn();
await plugin.hooks.onLoad?.({
pluginId: plugin.manifest.id,
taskStore: {} as never,
settings: { promptExecutorSystem: "Use browser evidence" },
logger,
emitEvent,
});
expect(emitEvent).toHaveBeenCalledWith("agent-browser:loaded", expect.objectContaining({ available: true, version: "1.0.0" }));
expect(plugin.promptContributions?.contributions.some((p) => p.content.includes("Use browser evidence"))).toBe(true);
});
it("emits unavailable payload when probe throws", async () => {
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
const emitEvent = vi.fn();
probeMock.mockRejectedValueOnce(new Error("boom"));
await plugin.hooks.onLoad?.({
pluginId: plugin.manifest.id,
taskStore: {} as never,
settings: {},
logger,
emitEvent,
});
expect(logger.error).toHaveBeenCalled();
expect(emitEvent).toHaveBeenCalledWith("agent-browser:loaded", expect.objectContaining({ available: false, error: "boom" }));
});
it("has canonical settings keys", () => {
expect(Object.keys(AGENT_BROWSER_SETTINGS_SCHEMA)).toEqual([
"enabled",
"installChannel",
"commandTimeoutMs",
"headlessMode",
"allowedDomains",
"promptExecutorSystem",
"promptExecutorTask",
"promptTriage",
"promptReviewer",
"promptHeartbeat",
"skillExposure",
]);
expect(AGENT_BROWSER_SETTINGS_SCHEMA.installChannel?.defaultValue).toBe("stable");
});
});

View File

@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { buildPromptContributions } from "../prompts.js";
describe("prompt contributions", () => {
it("covers all five prompt surfaces", () => {
const prompts = buildPromptContributions({
promptExecutorSystem: "a",
promptExecutorTask: "b",
promptTriage: "c",
promptReviewer: "d",
promptHeartbeat: "e",
});
expect(prompts.contributions.map((p) => p.surface)).toEqual([
"executor-system",
"executor-task",
"triage",
"reviewer",
"heartbeat",
]);
});
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from "vitest";
import { checkSetup } from "../setup.js";
import type { PluginContext } from "@fusion/plugin-sdk";
import { probeAgentBrowserBinary } from "../probe.js";
vi.mock("../probe.js", () => ({
probeAgentBrowserBinary: vi.fn(),
}));
const probeMock = vi.mocked(probeAgentBrowserBinary);
const ctx: PluginContext = {
pluginId: "fusion-plugin-agent-browser",
taskStore: {} as PluginContext["taskStore"],
settings: {},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: () => undefined,
};
describe("setup hooks", () => {
it("returns not-installed when binary is missing", async () => {
probeMock.mockResolvedValueOnce({ available: false, reason: "`agent-browser` not found on PATH", notFound: true });
const result = await checkSetup(ctx);
expect(result.status).toBe("not-installed");
});
it("returns installed when probe succeeds", async () => {
probeMock.mockResolvedValueOnce({ available: true, version: "1.2.3", binaryPath: "/usr/bin/agent-browser" });
const result = await checkSetup(ctx);
expect(result.status).toBe("installed");
expect(result.version).toBe("1.2.3");
});
it("returns error for non-not-found probe failures", async () => {
probeMock.mockResolvedValueOnce({ available: false, reason: "Probe timed out after 1000ms" });
const result = await checkSetup(ctx);
expect(result.status).toBe("error");
});
});

View File

@@ -0,0 +1,9 @@
import { describe, expect, it } from "vitest";
import { AGENT_BROWSER_SKILLS } from "../skills.js";
describe("skills", () => {
it("declares skill metadata", () => {
expect(AGENT_BROWSER_SKILLS[0]?.skillId).toBe("agent-browser-navigation");
expect(AGENT_BROWSER_SKILLS[0]?.skillFiles.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,55 @@
import { describe, expect, it, vi } from "vitest";
import { AGENT_BROWSER_TOOLS } from "../tools.js";
import type { PluginContext } from "@fusion/plugin-sdk";
const createContext = (settings: Record<string, unknown>): PluginContext => ({
pluginId: "fusion-plugin-agent-browser",
taskStore: {} as PluginContext["taskStore"],
settings,
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: vi.fn(),
});
describe("tools", () => {
it("blocks disallowed domains", async () => {
const tool = AGENT_BROWSER_TOOLS[0];
const result = await tool.execute(
{ url: "https://forbidden.example.com/path" },
createContext({ allowedDomains: ["allowed.example.com"] }),
);
expect(result.isError).toBe(true);
});
it("allows when allowlist is empty", async () => {
const tool = AGENT_BROWSER_TOOLS[0];
const result = await tool.execute(
{ url: "https://any.example.com/path" },
createContext({ allowedDomains: [], headlessMode: true }),
);
expect(result.isError).not.toBe(true);
});
it("allows matching domains", async () => {
const tool = AGENT_BROWSER_TOOLS[0];
const result = await tool.execute(
{ url: "https://docs.allowed.example.com/path" },
createContext({ allowedDomains: ["allowed.example.com"], headlessMode: true }),
);
expect(result.isError).not.toBe(true);
});
it("returns error for invalid URL", async () => {
const tool = AGENT_BROWSER_TOOLS[0];
const result = await tool.execute({ url: "not a url" }, createContext({ allowedDomains: [] }));
expect(result.isError).toBe(true);
});
it("blocks sibling domains that only share suffix text", async () => {
const tool = AGENT_BROWSER_TOOLS[0];
const result = await tool.execute(
{ url: "https://notexample.com/path" },
createContext({ allowedDomains: ["example.com"] }),
);
expect(result.isError).toBe(true);
});
});

View File

@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_SETTINGS, resolveSettings } from "../types.js";
describe("resolveSettings", () => {
it("returns defaults when input is undefined", () => {
expect(resolveSettings(undefined)).toEqual(DEFAULT_SETTINGS);
});
it("falls back commandTimeoutMs when invalid", () => {
expect(resolveSettings({ commandTimeoutMs: -1 }).commandTimeoutMs).toBe(DEFAULT_SETTINGS.commandTimeoutMs);
});
it("filters non-string allowedDomains entries", () => {
expect(resolveSettings({ allowedDomains: ["ok.example.com", 42, true] }).allowedDomains).toEqual(["ok.example.com"]);
});
});

View File

@@ -0,0 +1,10 @@
import { describe, expect, it } from "vitest";
import { AGENT_BROWSER_WORKFLOW_STEPS } from "../workflow-steps.js";
describe("workflow steps", () => {
it("declares browser evidence review template", () => {
const step = AGENT_BROWSER_WORKFLOW_STEPS[0];
expect(step?.stepId).toBe("browser-evidence-review");
expect(step?.mode).toBe("prompt");
});
});

View File

@@ -0,0 +1,67 @@
import { definePlugin } from "@fusion/plugin-sdk";
import type { FusionPlugin, PluginSettingSchema } from "@fusion/plugin-sdk";
import { AGENT_BROWSER_SKILLS } from "./skills.js";
import { buildPromptContributions } from "./prompts.js";
import { setupHooks, setupManifest } from "./setup.js";
import { resolveSettings } from "./types.js";
import { AGENT_BROWSER_WORKFLOW_STEPS } from "./workflow-steps.js";
import { AGENT_BROWSER_TOOLS } from "./tools.js";
import { probeAgentBrowserBinary } from "./probe.js";
const MAX_PROBE_TIMEOUT_MS = 30_000;
export const AGENT_BROWSER_SETTINGS_SCHEMA: Record<string, PluginSettingSchema> = {
enabled: { type: "boolean", label: "Enable Agent Browser", group: "General", defaultValue: true },
installChannel: { type: "enum", label: "Install Channel", enumValues: ["stable", "beta", "nightly"], defaultValue: "stable", group: "General" },
commandTimeoutMs: { type: "number", label: "Command Timeout (ms)", defaultValue: 120000, group: "General" },
headlessMode: { type: "boolean", label: "Headless Mode", defaultValue: true, group: "Browser" },
allowedDomains: { type: "array", label: "Allowed Domains", itemType: "string", defaultValue: [], group: "Browser" },
promptExecutorSystem: { type: "string", label: "Executor System Prompt", multiline: true, defaultValue: "When browsing, summarize evidence with URLs.", group: "Prompt Contributions" },
promptExecutorTask: { type: "string", label: "Executor Task Prompt", multiline: true, defaultValue: "Use browser context only when needed for the task.", group: "Prompt Contributions" },
promptTriage: { type: "string", label: "Triage Prompt", multiline: true, defaultValue: "Mark tasks requiring browser evidence explicitly.", group: "Prompt Contributions" },
promptReviewer: { type: "string", label: "Reviewer Prompt", multiline: true, defaultValue: "Verify browser-derived claims are backed by cited pages.", group: "Prompt Contributions" },
promptHeartbeat: { type: "string", label: "Heartbeat Prompt", multiline: true, defaultValue: "Keep browser interactions bounded and report failures clearly.", group: "Prompt Contributions" },
skillExposure: { type: "enum", label: "Skill Exposure", enumValues: ["none", "selected", "all"], defaultValue: "selected", group: "Skills" },
};
const plugin: FusionPlugin = definePlugin({
manifest: {
id: "fusion-plugin-agent-browser",
name: "Agent Browser Plugin",
version: "0.1.0",
description: "Adds agent-browser setup hooks plus skills, prompt contributions, and workflow steps",
author: "Fusion Team",
homepage: "https://github.com/gsxdsm/fusion",
settingsSchema: AGENT_BROWSER_SETTINGS_SCHEMA,
skills: AGENT_BROWSER_SKILLS.map((s) => ({ skillId: s.skillId, name: s.name })),
workflowSteps: AGENT_BROWSER_WORKFLOW_STEPS.map((s) => ({ stepId: s.stepId, name: s.name })),
promptSurfaces: ["executor-system", "executor-task", "triage", "reviewer", "heartbeat"],
setup: setupManifest,
},
state: "installed",
hooks: {
onLoad: async (ctx) => {
try {
const settings = resolveSettings(ctx.settings);
plugin.promptContributions = buildPromptContributions(settings);
const probe = await probeAgentBrowserBinary({ timeoutMs: Math.min(settings.commandTimeoutMs, MAX_PROBE_TIMEOUT_MS) });
ctx.logger.info(`Agent Browser Plugin loaded — available=${String(probe.available)} channel=${settings.installChannel}`);
ctx.emitEvent("agent-browser:loaded", { available: probe.available, version: probe.version, settings });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
ctx.logger.error(`Agent Browser Plugin probe failed: ${message}`);
ctx.emitEvent("agent-browser:loaded", { available: false, error: message });
}
},
},
tools: AGENT_BROWSER_TOOLS,
skills: AGENT_BROWSER_SKILLS,
workflowSteps: AGENT_BROWSER_WORKFLOW_STEPS,
promptContributions: buildPromptContributions(resolveSettings(undefined)),
setup: {
manifest: setupManifest,
hooks: setupHooks,
},
});
export default plugin;

View File

@@ -0,0 +1,98 @@
import { spawn } from "node:child_process";
export interface AgentBrowserProbeResult {
available: boolean;
binaryPath?: string;
version?: string;
reason?: string;
notFound?: boolean;
}
export async function probeAgentBrowserBinary(opts?: { binaryPath?: string; timeoutMs?: number }): Promise<AgentBrowserProbeResult> {
const binary = opts?.binaryPath?.trim() || "agent-browser";
const timeoutMs = opts?.timeoutMs ?? 2000;
const resolvedPath = await tryResolveBinaryPath(binary);
return new Promise((resolve) => {
let settled = false;
const child = spawn(resolvedPath ?? binary, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
if (settled) return;
settled = true;
child.kill("SIGKILL");
resolve({ available: false, binaryPath: resolvedPath, reason: `Probe timed out after ${timeoutMs}ms` });
}, timeoutMs);
child.stdout?.on("data", (c: Buffer) => (stdout += c.toString("utf-8")));
child.stderr?.on("data", (c: Buffer) => (stderr += c.toString("utf-8")));
child.on("error", (err: NodeJS.ErrnoException) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({
available: false,
binaryPath: resolvedPath,
reason: err.code === "ENOENT" ? "`agent-browser` not found on PATH" : err.message,
notFound: err.code === "ENOENT",
});
});
child.on("close", (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (code === 0) {
resolve({ available: true, binaryPath: resolvedPath, version: stdout.trim() || undefined });
} else {
resolve({
available: false,
binaryPath: resolvedPath,
reason: stderr.trim() || `agent-browser --version exited with code ${String(code)}`,
});
}
});
});
}
async function tryResolveBinaryPath(binary: string): Promise<string | undefined> {
return new Promise((resolvePromise) => {
const which = process.platform === "win32" ? "where" : "which";
const child = spawn(which, [binary], { stdio: ["ignore", "pipe", "ignore"] });
let out = "";
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
try {
child.kill("SIGKILL");
} catch {
// ignored
}
resolvePromise(undefined);
}, 2000);
child.stdout?.on("data", (chunk: Buffer) => {
out += chunk.toString("utf-8");
});
child.on("error", () => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolvePromise(undefined);
});
child.on("close", (code: number | null) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (code === 0) {
const first = out.trim().split(/\r?\n/)[0];
resolvePromise(first?.length ? first : undefined);
} else {
resolvePromise(undefined);
}
});
});
}

View File

@@ -0,0 +1,23 @@
import type { PluginPromptContributions } from "@fusion/plugin-sdk";
export function buildPromptContributions(settings: {
promptExecutorSystem: string;
promptExecutorTask: string;
promptTriage: string;
promptReviewer: string;
promptHeartbeat: string;
}): PluginPromptContributions {
const seed = [
{ surface: "executor-system", content: settings.promptExecutorSystem },
{ surface: "executor-task", content: settings.promptExecutorTask },
{ surface: "triage", content: settings.promptTriage },
{ surface: "reviewer", content: settings.promptReviewer },
{ surface: "heartbeat", content: settings.promptHeartbeat },
] as const;
const contributions: PluginPromptContributions["contributions"] = seed
.filter((p) => p.content.trim().length > 0)
.map((p) => ({ surface: p.surface, content: p.content }));
return { enabledByDefault: false, contributions };
}

View File

@@ -0,0 +1,31 @@
import type { PluginContext, PluginSetupCheckResult, PluginSetupHooks, PluginSetupManifest } from "@fusion/plugin-sdk";
import { probeAgentBrowserBinary } from "./probe.js";
import { resolveSettings } from "./types.js";
export const setupManifest: PluginSetupManifest = {
binaryName: "agent-browser",
description: "Headless browser runtime for web-enabled agents",
channel: "stable",
defaultTimeoutMs: 120000,
};
const MAX_PROBE_TIMEOUT_MS = 30_000;
export async function checkSetup(ctx: PluginContext): Promise<PluginSetupCheckResult> {
const settings = resolveSettings(ctx.settings);
const probe = await probeAgentBrowserBinary({ timeoutMs: Math.min(settings.commandTimeoutMs, MAX_PROBE_TIMEOUT_MS) });
if (probe.available) {
return { status: "installed", version: probe.version, binaryPath: probe.binaryPath };
}
const reason = probe.reason ?? "agent-browser probe failed";
if (probe.notFound === true) {
return { status: "not-installed", error: reason };
}
return { status: "error", error: reason };
}
export const setupHooks: PluginSetupHooks = {
checkSetup,
};

View File

@@ -0,0 +1,12 @@
import type { PluginSkillContribution } from "@fusion/plugin-sdk";
export const AGENT_BROWSER_SKILLS: PluginSkillContribution[] = [
{
skillId: "agent-browser-navigation",
name: "Agent Browser Navigation",
description: "Navigate pages, collect evidence, and summarize findings.",
skillFiles: ["skills/agent-browser-navigation/SKILL.md"],
enabled: true,
triggerPatterns: ["browse", "visit website", "collect evidence"],
},
];

View File

@@ -0,0 +1,34 @@
import type { PluginToolDefinition } from "@fusion/plugin-sdk";
import { resolveSettings } from "./types.js";
export const AGENT_BROWSER_TOOLS: PluginToolDefinition[] = [
{
name: "browser_fetch_metadata",
description: "Validate a URL against plugin allowlist and return safe fetch metadata.",
parameters: {
type: "object",
properties: { url: { type: "string" } },
required: ["url"],
},
execute: async (params, ctx) => {
const url = typeof params.url === "string" ? params.url : "";
try {
const parsed = new URL(url);
const settings = resolveSettings(ctx.settings);
const allowed = settings.allowedDomains;
const isAllowed = allowed.length === 0 || allowed.some((d) => parsed.hostname === d || parsed.hostname.endsWith(`.${d}`));
if (!isAllowed) {
return { content: [{ type: "text", text: `Blocked by allowedDomains: ${parsed.hostname}` }], isError: true };
}
return {
content: [{ type: "text", text: `Allowed URL ${parsed.toString()} (headless=${String(settings.headlessMode)})` }],
details: { hostname: parsed.hostname, headlessMode: settings.headlessMode },
};
} catch {
return { content: [{ type: "text", text: "Invalid URL" }], isError: true };
}
},
},
];

View File

@@ -0,0 +1,56 @@
export type SkillExposure = "none" | "selected" | "all";
export interface AgentBrowserSettings {
enabled: boolean;
installChannel: "stable" | "beta" | "nightly";
commandTimeoutMs: number;
headlessMode: boolean;
allowedDomains: string[];
promptExecutorSystem: string;
promptExecutorTask: string;
promptTriage: string;
promptReviewer: string;
promptHeartbeat: string;
skillExposure: SkillExposure;
}
export const DEFAULT_SETTINGS: AgentBrowserSettings = {
enabled: true,
installChannel: "stable",
commandTimeoutMs: 120000,
headlessMode: true,
allowedDomains: [],
promptExecutorSystem: "When browsing, summarize evidence with URLs.",
promptExecutorTask: "Use browser context only when needed for the task.",
promptTriage: "Mark tasks requiring browser evidence explicitly.",
promptReviewer: "Verify browser-derived claims are backed by cited pages.",
promptHeartbeat: "Keep browser interactions bounded and report failures clearly.",
skillExposure: "selected",
};
export function resolveSettings(input: Record<string, unknown> | undefined): AgentBrowserSettings {
const src = input ?? {};
return {
enabled: typeof src.enabled === "boolean" ? src.enabled : DEFAULT_SETTINGS.enabled,
installChannel:
src.installChannel === "stable" || src.installChannel === "beta" || src.installChannel === "nightly"
? src.installChannel
: DEFAULT_SETTINGS.installChannel,
commandTimeoutMs:
typeof src.commandTimeoutMs === "number" && Number.isFinite(src.commandTimeoutMs) && src.commandTimeoutMs > 0
? src.commandTimeoutMs
: DEFAULT_SETTINGS.commandTimeoutMs,
headlessMode: typeof src.headlessMode === "boolean" ? src.headlessMode : DEFAULT_SETTINGS.headlessMode,
allowedDomains: Array.isArray(src.allowedDomains) ? src.allowedDomains.filter((v): v is string => typeof v === "string") : [],
promptExecutorSystem:
typeof src.promptExecutorSystem === "string" ? src.promptExecutorSystem : DEFAULT_SETTINGS.promptExecutorSystem,
promptExecutorTask: typeof src.promptExecutorTask === "string" ? src.promptExecutorTask : DEFAULT_SETTINGS.promptExecutorTask,
promptTriage: typeof src.promptTriage === "string" ? src.promptTriage : DEFAULT_SETTINGS.promptTriage,
promptReviewer: typeof src.promptReviewer === "string" ? src.promptReviewer : DEFAULT_SETTINGS.promptReviewer,
promptHeartbeat: typeof src.promptHeartbeat === "string" ? src.promptHeartbeat : DEFAULT_SETTINGS.promptHeartbeat,
skillExposure:
src.skillExposure === "none" || src.skillExposure === "selected" || src.skillExposure === "all"
? src.skillExposure
: DEFAULT_SETTINGS.skillExposure,
};
}

View File

@@ -0,0 +1,15 @@
import type { PluginWorkflowStepContribution } from "@fusion/plugin-sdk";
export const AGENT_BROWSER_WORKFLOW_STEPS: PluginWorkflowStepContribution[] = [
{
stepId: "browser-evidence-review",
name: "Browser Evidence Review",
description: "Verify claims include browser-derived evidence and links.",
mode: "prompt",
phase: "pre-merge",
prompt: "Confirm browser-derived statements are traceable to captured evidence and cite links when present.",
toolMode: "readonly",
enabled: true,
defaultOn: false,
},
];

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"types": ["node", "vitest/globals"]
},
"include": ["src/**/*"]
}

View File

@@ -0,0 +1,22 @@
import { defineConfig } from "vitest/config";
import { fileURLToPath } from "node:url";
import { computeMaxWorkers } from "../../packages/core/src/__test-utils__/vitest-workers";
const maxWorkers = computeMaxWorkers();
export default defineConfig({
resolve: {
alias: {
"@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)),
"@fusion/plugin-sdk": fileURLToPath(new URL("../../packages/plugin-sdk/src/index.ts", import.meta.url)),
},
},
test: {
include: ["src/**/*.test.ts"],
setupFiles: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-setup.ts", import.meta.url))],
globalSetup: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-teardown.ts", import.meta.url))],
pool: "threads",
maxWorkers,
poolOptions: { threads: { minThreads: 1, maxThreads: maxWorkers }, forks: { minForks: 1, maxForks: maxWorkers } },
},
});