claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
324 lines • 18.7 kB
JavaScript
/**
* Browser Session Lifecycle MCP Tools (ADR-0001 ruflo-browser §7).
*
* Five lifecycle tools that wrap the 23 raw `browser_*` interaction tools
* with RVF cognitive containers, ruvector trajectory recording, AgentDB
* indexing, and AIDefence gates. Implements the contract from
* `plugins/ruflo-browser/docs/adrs/0001-browser-skills-architecture.md`.
*
* Design notes:
* - These tools orchestrate at the *primitive* level — they shell out to
* the existing `agent-browser` CLI (for browser actions), `ruvector` CLI
* (for trajectory hooks + RVF), and the bridged `memory` namespace (for
* AgentDB index). They do not inline a replay engine; replay
* enumerates trajectory steps and returns them for the caller to dispatch.
* - Pinned to ruvector@0.2.25 to match `ruflo-ruvector` ADR-0001.
* - Best-effort: missing dependencies (no `ruvector`, no `agent-browser`,
* no AgentDB controller) degrade gracefully with a structured error
* rather than a process crash.
*/
import { validateIdentifier, validateText } from './validate-input.js';
const RUVECTOR_PIN = 'ruvector@0.2.25';
const RVF_DIR_DEFAULT = '.ruflo/browser-sessions';
async function shell(cmd, args, opts = {}) {
const { execFile } = await import('node:child_process');
const { promisify } = await import('node:util');
const run = promisify(execFile);
try {
const { stdout, stderr } = await run(cmd, args, {
timeout: opts.timeout ?? 30000,
encoding: 'utf-8',
});
return { success: true, stdout, stderr };
}
catch (error) {
const err = error;
return {
success: false,
error: err.code === 'ENOENT' ? `command not found: ${cmd}` : err.message,
stdout: err.stdout,
stderr: err.stderr,
};
}
}
async function ensureSessionsDir() {
const { mkdir } = await import('node:fs/promises');
const path = await import('node:path');
const dir = path.resolve(process.cwd(), RVF_DIR_DEFAULT);
await mkdir(dir, { recursive: true });
return dir;
}
function makeSessionId(taskSlug) {
const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
const slug = taskSlug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 32) || 'session';
return `${stamp}-${slug}`;
}
function ok(payload) {
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...payload }, null, 2) }] };
}
function fail(error, extra = {}) {
return {
content: [{ type: 'text', text: JSON.stringify({ success: false, error, ...extra }, null, 2) }],
isError: true,
};
}
export const browserSessionTools = [
// ==========================================================================
// browser_session_record — open a recorded session
// ==========================================================================
{
name: 'browser_session_record',
description: 'Open a named, traced browser session: allocate an RVF cognitive container, begin a ruvector trajectory, then open the URL via agent-browser. Returns the session id and rvf path. Use when native WebFetch is wrong because you need real browser automation — JS-heavy SPA scraping, login flows with cookie reuse, replay against DOM-drifted versions, AIDefence PII gating before content reaches Claude. For static HTML pages, native WebFetch is faster and free.',
category: 'browser-session',
tags: ['session', 'rvf', 'trajectory', 'lifecycle'],
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'Target URL to open' },
task: { type: 'string', description: 'Human-readable task description (recorded in trajectory)' },
session: { type: 'string', description: 'Optional explicit session id; otherwise auto-generated' },
rvf_dir: { type: 'string', description: 'Override the default .ruflo/browser-sessions directory' },
},
required: ['url', 'task'],
},
handler: async (input) => {
const vUrl = validateText(input.url, 'url');
if (!vUrl.valid)
return fail(vUrl.error || 'invalid url');
const vTask = validateText(input.task, 'task');
if (!vTask.valid)
return fail(vTask.error || 'invalid task');
const path = await import('node:path');
const explicitSession = input.session;
if (explicitSession) {
const v = validateIdentifier(explicitSession, 'session');
if (!v.valid)
return fail(v.error || 'invalid session');
}
const sessionId = explicitSession ?? makeSessionId(input.task);
const dir = input.rvf_dir ?? (await ensureSessionsDir());
const rvfPath = path.join(dir, `${sessionId}.rvf`);
// 1. RVF allocate.
// Issue #2015: ruvector@0.2.25's `rvf create` accepts only
// `-d/--dimension <n>` (required) and `-m/--metric <metric>`.
// The wrapper previously passed `--kind browser-session` and
// omitted `--dimension`, so commander hit the required-option
// check first and the wrapper returned `rvf create failed` for
// every call. The second round of the fix strips the bogus
// `--kind` flag — when round 1 only added `--dimension`, the
// next call surfaced `error: unknown option '--kind'`.
//
// 384 matches the MiniLM-L6 default used elsewhere in the
// toolchain (ONNX embedder + AgentDB vector indexes).
const rvf = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'create', rvfPath, '--dimension', '384'], { timeout: 60000 });
if (!rvf.success)
return fail('rvf create failed', { detail: rvf.error, stderr: rvf.stderr, sessionId, rvfPath });
// 2. trajectory-begin
const tb = await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-begin', '--session-id', sessionId, '--task', input.task]);
if (!tb.success)
return fail('trajectory-begin failed', { detail: tb.error, stderr: tb.stderr, sessionId, rvfPath });
// 3. browser_open via agent-browser
const bo = await shell('agent-browser', ['--session', sessionId, '--json', 'open', input.url], { timeout: 30000 });
if (!bo.success) {
const npxBo = await shell('npx', ['--yes', 'agent-browser', '--session', sessionId, '--json', 'open', input.url], { timeout: 60000 });
if (!npxBo.success) {
return fail('browser open failed', { detail: npxBo.error, stderr: npxBo.stderr, sessionId, rvfPath });
}
}
// 4. log the open as the first trajectory step
await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-step',
'--session-id', sessionId,
'--action', 'browser_open',
'--args', JSON.stringify({ url: input.url }),
'--result', 'ok']);
return ok({
sessionId,
rvfPath,
url: input.url,
task: input.task,
ruvectorPin: RUVECTOR_PIN,
});
},
},
// ==========================================================================
// browser_session_end — commit a recorded session
// ==========================================================================
{
name: 'browser_session_end',
description: 'End a recorded browser session: trajectory-end with verdict, rvf compact, AIDefence pre-store gate (best-effort), and AgentDB index in the browser-sessions namespace. Use when native WebFetch is wrong because you need real browser automation — JS-heavy SPA scraping, login flows with cookie reuse, replay against DOM-drifted versions, AIDefence PII gating before content reaches Claude. For static HTML pages, native WebFetch is faster and free.',
category: 'browser-session',
tags: ['session', 'rvf', 'trajectory', 'lifecycle', 'agentdb'],
inputSchema: {
type: 'object',
properties: {
session: { type: 'string', description: 'Session id (returned from browser_session_record)' },
rvf_path: { type: 'string', description: 'Path to the .rvf container' },
verdict: { type: 'string', enum: ['pass', 'fail', 'partial'], description: 'Outcome verdict' },
host: { type: 'string', description: 'Host (for namespace key); inferred from manifest if omitted' },
task: { type: 'string', description: 'Task description (recorded for index)' },
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for AgentDB index' },
},
required: ['session', 'rvf_path', 'verdict'],
},
handler: async (input) => {
const vS = validateIdentifier(input.session, 'session');
if (!vS.valid)
return fail(vS.error || 'invalid session');
const verdict = input.verdict;
if (!['pass', 'fail', 'partial'].includes(verdict))
return fail(`invalid verdict: ${verdict}`);
// 1. trajectory-end
const te = await shell('npx', ['-y', RUVECTOR_PIN, 'hooks', 'trajectory-end',
'--session-id', input.session,
'--verdict', verdict]);
if (!te.success)
return fail('trajectory-end failed', { detail: te.error, stderr: te.stderr });
// 2. rvf compact
const compact = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'compact', input.rvf_path]);
if (!compact.success)
return fail('rvf compact failed', { detail: compact.error, stderr: compact.stderr });
// 3. AgentDB index — best-effort via memory store (claude-flow bridges)
const indexValue = JSON.stringify({
rvf_id: input.session,
rvf_path: input.rvf_path,
host: input.host ?? null,
task: input.task ?? null,
verdict,
tags: input.tags ?? [],
ended_at: new Date().toISOString(),
});
const idx = await shell('npx', ['-y', '@claude-flow/cli@latest', 'memory', 'store',
'--namespace', 'browser-sessions',
'--key', input.session,
'--value', indexValue], { timeout: 60000 });
// Index failure is non-fatal — the RVF container is the source of truth.
return ok({
sessionId: input.session,
rvfPath: input.rvf_path,
verdict,
indexed: idx.success,
indexError: idx.success ? undefined : (idx.stderr || idx.error),
});
},
},
// ==========================================================================
// browser_session_replay — load a trajectory for caller-level dispatch
// ==========================================================================
{
name: 'browser_session_replay',
description: 'Load a recorded session trajectory and return its steps so the caller can dispatch them through the 23 browser_* tools. Does NOT itself drive the browser — replay execution is caller-orchestrated to keep this tool a primitive (ADR-0001 §7). Use when native WebFetch is wrong because you need real browser automation — JS-heavy SPA scraping, login flows with cookie reuse, replay against DOM-drifted versions, AIDefence PII gating before content reaches Claude. For static HTML pages, native WebFetch is faster and free.',
category: 'browser-session',
tags: ['session', 'replay', 'trajectory', 'lifecycle'],
inputSchema: {
type: 'object',
properties: {
session: { type: 'string', description: 'Source session id to replay' },
rvf_path: { type: 'string', description: 'Path to source .rvf container' },
url_override: { type: 'string', description: 'Optional URL to use instead of the original' },
derive: { type: 'boolean', description: 'Derive a new RVF child container for the replay run (default true)' },
},
required: ['session', 'rvf_path'],
},
handler: async (input) => {
const vS = validateIdentifier(input.session, 'session');
if (!vS.valid)
return fail(vS.error || 'invalid session');
// 1. Verify RVF container exists
const status = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'status', input.rvf_path]);
if (!status.success)
return fail('rvf status failed', { detail: status.error, stderr: status.stderr });
// 2. Derive child container if requested
let replayId = null;
let replayPath = null;
const derive = input.derive !== false;
if (derive) {
const path = await import('node:path');
const dir = path.dirname(input.rvf_path);
replayId = `${input.session}-replay-${Date.now()}`;
replayPath = path.join(dir, `${replayId}.rvf`);
const dr = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'derive', input.rvf_path, replayPath]);
if (!dr.success)
return fail('rvf derive failed', { detail: dr.error, stderr: dr.stderr });
}
// 3. Surface the trajectory steps from the segments listing — the caller is
// expected to read trajectory.ndjson from the RVF container and dispatch.
const segments = await shell('npx', ['-y', RUVECTOR_PIN, 'rvf', 'segments', input.rvf_path]);
return ok({
sourceSession: input.session,
sourceRvfPath: input.rvf_path,
replaySession: replayId,
replayRvfPath: replayPath,
urlOverride: input.url_override ?? null,
rvfStatus: status.stdout?.slice(0, 4000) ?? null,
rvfSegments: segments.stdout?.slice(0, 4000) ?? null,
nextStep: 'Caller MUST: (a) read trajectory.ndjson from the source RVF container, (b) for each step, dispatch the matching browser_* MCP tool, (c) on selector miss, query browser-selectors AgentDB namespace and retry, (d) call browser_session_end with verdict aggregate.',
});
},
},
// ==========================================================================
// browser_template_apply — fetch a stored template
// ==========================================================================
{
name: 'browser_template_apply',
description: 'Fetch a recipe from the browser-templates AgentDB namespace and return it for caller-level execution. Use when native WebFetch is wrong because you need real browser automation — JS-heavy SPA scraping, login flows with cookie reuse, replay against DOM-drifted versions, AIDefence PII gating before content reaches Claude. For static HTML pages, native WebFetch is faster and free.',
category: 'browser-session',
tags: ['template', 'agentdb', 'extract'],
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Template name (key in browser-templates namespace)' },
},
required: ['name'],
},
handler: async (input) => {
const vN = validateText(input.name, 'name');
if (!vN.valid)
return fail(vN.error || 'invalid name');
const r = await shell('npx', ['-y', '@claude-flow/cli@latest', 'memory', 'retrieve',
'--namespace', 'browser-templates',
'--key', input.name], { timeout: 60000 });
if (!r.success)
return fail('template fetch failed', { detail: r.error, stderr: r.stderr });
return ok({
templateName: input.name,
recipe: r.stdout,
nextStep: 'Caller dispatches the recipe via browser_* tools; persist updated selectors to browser-selectors on success.',
});
},
},
// ==========================================================================
// browser_cookie_use — fetch a vaulted cookie handle
// ==========================================================================
{
name: 'browser_cookie_use',
description: 'Fetch a vault handle for a host from the browser-cookies AgentDB namespace. Raw cookie values are NEVER returned — only the opaque handle plus expiry / AIDefence verdict. Use when native WebFetch is wrong because you need real browser automation — JS-heavy SPA scraping, login flows with cookie reuse, replay against DOM-drifted versions, AIDefence PII gating before content reaches Claude. For static HTML pages, native WebFetch is faster and free.',
category: 'browser-session',
tags: ['cookie', 'agentdb', 'aidefence', 'auth'],
inputSchema: {
type: 'object',
properties: {
host: { type: 'string', description: 'Host (e.g. "example.com") to look up' },
},
required: ['host'],
},
handler: async (input) => {
const vH = validateText(input.host, 'host');
if (!vH.valid)
return fail(vH.error || 'invalid host');
const r = await shell('npx', ['-y', '@claude-flow/cli@latest', 'memory', 'retrieve',
'--namespace', 'browser-cookies',
'--key', input.host], { timeout: 60000 });
if (!r.success)
return fail('cookie lookup failed', { detail: r.error, stderr: r.stderr });
// The contract: the value blob includes a vault_handle, expiry, aidefence_verdict.
// Raw values do not enter this namespace (browser-login is responsible).
return ok({
host: input.host,
vault: r.stdout,
nextStep: 'Caller mounts the handle via the browser runner; the raw cookie is materialized only inside the browser process, never returned to the model.',
});
},
},
];
export default browserSessionTools;
//# sourceMappingURL=browser-session-tools.js.map