feat: migrate from MCP tools to CLI based tools (#252)

* feat: migrate from MCP tools to CLI tools

* fix: restore browser action emoji formatters for CLI output

Adapt formatBrowserAction for playwright-cli commands, replacing the old
mcp__playwright__browser_* tool name matching removed during migration.
This commit is contained in:
ezl-keygraph
2026-03-22 13:12:24 +05:30
committed by GitHub
parent 01dc49bbd6
commit c408eabc62
57 changed files with 724 additions and 1274 deletions
+8 -93
View File
@@ -7,13 +7,11 @@
// Production Claude agent execution with retry, git checkpoints, and audit logging
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createShannonHelperServer } from '@shannon/mcp-server';
import { fs, path } from 'zx';
import type { AuditSession } from '../audit/index.js';
import { isRetryableError, PentestError } from '../services/error-handling.js';
import { AGENT_VALIDATORS, AGENTS, MCP_AGENT_MAPPING } from '../session-manager.js';
import { AGENT_VALIDATORS } from '../session-manager.js';
import type { ActivityLogger } from '../types/activity-logger.js';
import type { AgentName } from '../types/index.js';
import { isSpendingCapBehavior } from '../utils/billing-detection.js';
import { formatTimestamp } from '../utils/formatting.js';
import { Timer } from '../utils/metrics.js';
@@ -43,89 +41,6 @@ export interface ClaudePromptResult {
retryable?: boolean | undefined;
}
interface StdioMcpServer {
type: 'stdio';
command: string;
args: string[];
env: Record<string, string>;
}
type McpServer = ReturnType<typeof createShannonHelperServer> | StdioMcpServer;
// Configures MCP servers for agent execution, with Docker-specific Chromium handling
function buildMcpServers(
sourceDir: string,
agentName: string | null,
logger: ActivityLogger,
): Record<string, McpServer> {
// 1. Create the shannon-helper server (always present)
const shannonHelperServer = createShannonHelperServer(sourceDir);
const mcpServers: Record<string, McpServer> = {
'shannon-helper': shannonHelperServer,
};
// 2. Look up the agent's Playwright MCP mapping
if (agentName) {
const promptTemplate = AGENTS[agentName as AgentName].promptTemplate;
const playwrightMcpName = MCP_AGENT_MAPPING[promptTemplate as keyof typeof MCP_AGENT_MAPPING] || null;
if (playwrightMcpName) {
logger.info(`Assigned ${agentName} -> ${playwrightMcpName}`);
const userDataDir = `/tmp/${playwrightMcpName}`;
// 3. Configure Playwright MCP args with Docker/local browser handling
const isDocker = process.env.SHANNON_DOCKER === 'true';
const mcpArgs: string[] = ['@playwright/mcp@0.0.68', '--isolated', '--user-data-dir', userDataDir];
if (isDocker) {
mcpArgs.push('--executable-path', '/usr/bin/chromium-browser');
mcpArgs.push('--browser', 'chromium');
}
// NOTE: Explicit allowlist — the Playwright MCP subprocess must not inherit
// secrets (API keys, AWS tokens) from the parent process.
const MCP_ENV_ALLOWLIST = [
'PATH',
'HOME',
'NODE_PATH',
'DISPLAY',
'PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH',
] as const;
const envVars: Record<string, string> = {
PLAYWRIGHT_HEADLESS: 'true',
...(isDocker && { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' }),
};
for (const key of MCP_ENV_ALLOWLIST) {
const val = process.env[key];
if (val) {
envVars[key] = val;
}
}
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('XDG_') && value !== undefined) {
envVars[key] = value;
}
}
mcpServers[playwrightMcpName] = {
type: 'stdio' as const,
command: 'npx',
args: mcpArgs,
env: envVars,
};
}
}
// 4. Return configured servers
return mcpServers;
}
function outputLines(lines: string[]): void {
for (const line of lines) {
console.log(line);
@@ -213,7 +128,7 @@ export async function runClaudePrompt(
sourceDir: string,
context: string = '',
description: string = 'Claude analysis',
agentName: string | null = null,
_agentName: string | null = null,
auditSession: AuditSession | null = null,
logger: ActivityLogger,
modelTier: ModelTier = 'medium',
@@ -232,10 +147,7 @@ export async function runClaudePrompt(
logger.info(`Running Claude Code: ${description}...`);
// 3. Configure MCP servers
const mcpServers = buildMcpServers(sourceDir, agentName, logger);
// 4. Build env vars to pass to SDK subprocesses
// 3. Build env vars to pass to SDK subprocesses
const sdkEnv: Record<string, string> = {
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || '64000',
};
@@ -254,6 +166,9 @@ export async function runClaudePrompt(
'ANTHROPIC_SMALL_MODEL',
'ANTHROPIC_MEDIUM_MODEL',
'ANTHROPIC_LARGE_MODEL',
'HOME',
'PATH',
'PLAYWRIGHT_MCP_EXECUTABLE_PATH',
];
for (const name of passthroughVars) {
const val = process.env[name];
@@ -262,14 +177,14 @@ export async function runClaudePrompt(
}
}
// 5. Configure SDK options
// 4. Configure SDK options
const options = {
model: resolveModel(modelTier),
maxTurns: 10_000,
cwd: sourceDir,
permissionMode: 'bypassPermissions' as const,
allowDangerouslySkipPermissions: true,
mcpServers,
settingSources: ['user'] as ('user' | 'project' | 'local')[],
env: sdkEnv,
};
-4
View File
@@ -308,10 +308,6 @@ export async function dispatchMessage(
const actualModel = getActualModelName(initMsg.model);
if (!execContext.useCleanOutput) {
logger.info(`Model: ${actualModel}, Permission: ${initMsg.permissionMode}`);
if (initMsg.mcp_servers && initMsg.mcp_servers.length > 0) {
const mcpStatus = initMsg.mcp_servers.map((s) => `${s.name}(${s.status})`).join(', ');
logger.info(`MCP: ${mcpStatus}`);
}
}
// Return actual model for tracking in audit logs
return { type: 'continue', model: actualModel };
+83 -103
View File
@@ -16,6 +16,7 @@ interface ToolCallInput {
text?: string;
action?: string;
description?: string;
command?: string;
todos?: Array<{
status: string;
content: string;
@@ -76,6 +77,80 @@ function extractDomain(url: string): string {
}
}
/**
* Format playwright-cli commands into clean progress indicators
*/
function formatBrowserAction(command: string): string | null {
// Extract subcommand after optional session flag (e.g., "playwright-cli -s=session1 navigate https://example.com")
const match = command.match(/playwright-cli\s+(?:-s=\S+\s+)?(\S+)(?:\s+(.*))?/);
if (!match) return null;
const subcommand = match[1];
const args = match[2] || '';
switch (subcommand) {
case 'open':
case 'goto': {
const domain = args.trim() ? extractDomain(args.trim()) : '';
return domain ? `🌐 Navigating to ${domain}` : '🌐 Opening browser';
}
case 'go-back':
return '⬅️ Going back';
case 'go-forward':
return '➡️ Going forward';
case 'reload':
return '🔄 Reloading page';
case 'click':
case 'dblclick':
return `🖱️ Clicking ${(args || 'element').slice(0, 25)}`;
case 'hover':
return `👆 Hovering over ${(args || 'element').slice(0, 20)}`;
case 'type':
return `⌨️ Typing ${(args || 'text').slice(0, 20)}`;
case 'press':
case 'keydown':
case 'keyup':
return `⌨️ Pressing ${args || 'key'}`;
case 'fill':
return `📝 Filling ${(args || 'field').slice(0, 25)}`;
case 'select':
return '📋 Selecting dropdown option';
case 'check':
case 'uncheck':
return `☑️ ${subcommand === 'check' ? 'Checking' : 'Unchecking'} ${(args || 'element').slice(0, 20)}`;
case 'upload':
return '📁 Uploading file';
case 'drag':
return '🖱️ Dragging element';
case 'snapshot':
return '📸 Taking page snapshot';
case 'screenshot':
return '📸 Taking screenshot';
case 'eval':
case 'run-code':
return '🔍 Running JavaScript analysis';
case 'console':
return '📜 Checking console logs';
case 'network':
return '🌐 Analyzing network traffic';
case 'tab-list':
case 'tab-new':
case 'tab-close':
case 'tab-select':
return `🗂️ ${subcommand.replace('tab-', '')} browser tab`;
case 'dialog-accept':
return '💬 Accepting dialog';
case 'dialog-dismiss':
return '💬 Dismissing dialog';
case 'pdf':
return '📄 Saving page as PDF';
case 'resize':
return `🖥️ Resizing browser ${args || ''}`.trim();
default:
return `🌐 Browser: ${subcommand}`;
}
}
/**
* Summarize TodoWrite updates into clean progress indicators
*/
@@ -103,104 +178,6 @@ function summarizeTodoUpdate(input: ToolCallInput | undefined): string | null {
return null;
}
/**
* Format browser tool calls into clean progress indicators
*/
function formatBrowserAction(toolCall: ToolCall): string {
const toolName = toolCall.name;
const input = toolCall.input || {};
// Core Browser Operations
if (toolName === 'mcp__playwright__browser_navigate') {
const url = input.url || '';
const domain = extractDomain(url);
return `🌐 Navigating to ${domain}`;
}
if (toolName === 'mcp__playwright__browser_navigate_back') {
return `⬅️ Going back`;
}
// Page Interaction
if (toolName === 'mcp__playwright__browser_click') {
const element = input.element || 'element';
return `🖱️ Clicking ${element.slice(0, 25)}`;
}
if (toolName === 'mcp__playwright__browser_hover') {
const element = input.element || 'element';
return `👆 Hovering over ${element.slice(0, 20)}`;
}
if (toolName === 'mcp__playwright__browser_type') {
const element = input.element || 'field';
return `⌨️ Typing in ${element.slice(0, 20)}`;
}
if (toolName === 'mcp__playwright__browser_press_key') {
const key = input.key || 'key';
return `⌨️ Pressing ${key}`;
}
// Form Handling
if (toolName === 'mcp__playwright__browser_fill_form') {
const fieldCount = input.fields?.length || 0;
return `📝 Filling ${fieldCount} form fields`;
}
if (toolName === 'mcp__playwright__browser_select_option') {
return `📋 Selecting dropdown option`;
}
if (toolName === 'mcp__playwright__browser_file_upload') {
return `📁 Uploading file`;
}
// Page Analysis
if (toolName === 'mcp__playwright__browser_snapshot') {
return `📸 Taking page snapshot`;
}
if (toolName === 'mcp__playwright__browser_take_screenshot') {
return `📸 Taking screenshot`;
}
if (toolName === 'mcp__playwright__browser_evaluate') {
return `🔍 Running JavaScript analysis`;
}
// Waiting & Monitoring
if (toolName === 'mcp__playwright__browser_wait_for') {
if (input.text) {
return `⏳ Waiting for "${input.text.slice(0, 20)}"`;
}
return `⏳ Waiting for page response`;
}
if (toolName === 'mcp__playwright__browser_console_messages') {
return `📜 Checking console logs`;
}
if (toolName === 'mcp__playwright__browser_network_requests') {
return `🌐 Analyzing network traffic`;
}
// Tab Management
if (toolName === 'mcp__playwright__browser_tabs') {
const action = input.action || 'managing';
return `🗂️ ${action} browser tab`;
}
// Dialog Handling
if (toolName === 'mcp__playwright__browser_handle_dialog') {
return `💬 Handling browser dialog`;
}
// Fallback for any missed tools
const actionType = toolName.split('_').pop();
return `🌐 Browser: ${actionType}`;
}
/**
* Filter out JSON tool calls from content, with special handling for Task calls
*/
@@ -241,11 +218,14 @@ export function filterJsonToolCalls(content: string | null | undefined): string
continue;
}
// Special handling for browser tool calls
if (toolCall.name.startsWith('mcp__playwright__browser_')) {
const browserAction = formatBrowserAction(toolCall);
if (browserAction) {
processedLines.push(browserAction);
// Special handling for browser tool calls (playwright-cli via Bash)
if (toolCall.name === 'Bash') {
const command = toolCall.input?.command || '';
if (command.includes('playwright-cli')) {
const browserAction = formatBrowserAction(command);
if (browserAction) {
processedLines.push(browserAction);
}
}
}
} catch {
-1
View File
@@ -92,7 +92,6 @@ export interface SystemInitMessage {
subtype: 'init';
model?: string;
permissionMode?: string;
mcp_servers?: Array<{ name: string; status: string }>;
}
export interface UserMessage {
-16
View File
@@ -260,22 +260,6 @@ export class WorkflowLogger {
return String(p.url);
}
break;
case 'mcp__playwright__browser_navigate':
if (p.url) {
return String(p.url);
}
break;
case 'mcp__playwright__browser_click':
if (p.selector) {
return this.truncate(String(p.selector), 60);
}
break;
case 'mcp__playwright__browser_type':
if (p.selector) {
const text = p.text ? `: "${this.truncate(String(p.text), 30)}"` : '';
return `${this.truncate(String(p.selector), 40)}${text}`;
}
break;
}
// Default: show first string-valued param truncated
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env node
// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
/**
* generate-totp CLI
*
* Generates 6-digit TOTP codes for authentication.
* Replaces the MCP generate_totp tool.
* Based on RFC 6238 (TOTP) and RFC 4226 (HOTP).
*
* Usage:
* generate-totp --secret JBSWY3DPEHPK3PXP
*/
import { createHmac } from 'node:crypto';
// === Base32 Decoding ===
function base32Decode(encoded: string): Buffer {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const cleanInput = encoded.toUpperCase().replace(/[^A-Z2-7]/g, '');
if (cleanInput.length === 0) {
throw new Error('TOTP secret is empty after cleaning');
}
const output: number[] = [];
let bits = 0;
let value = 0;
for (const char of cleanInput) {
const index = alphabet.indexOf(char);
if (index === -1) {
throw new Error(`Invalid base32 character: ${char}`);
}
value = (value << 5) | index;
bits += 5;
if (bits >= 8) {
output.push((value >>> (bits - 8)) & 255);
bits -= 8;
}
}
return Buffer.from(output);
}
// === TOTP Generation (RFC 6238) ===
function generateHOTP(secret: string, counter: number, digits: number = 6): string {
const key = base32Decode(secret);
// Convert counter to 8-byte buffer (big-endian)
const counterBuffer = Buffer.alloc(8);
counterBuffer.writeBigUInt64BE(BigInt(counter));
// Generate HMAC-SHA1
const hmac = createHmac('sha1', key);
hmac.update(counterBuffer);
const hash = hmac.digest();
// Dynamic truncation (SHA-1 always produces 20 bytes)
const lastByte = hash[hash.length - 1] ?? 0;
const offset = lastByte & 0x0f;
const code =
(((hash[offset] ?? 0) & 0x7f) << 24) |
(((hash[offset + 1] ?? 0) & 0xff) << 16) |
(((hash[offset + 2] ?? 0) & 0xff) << 8) |
((hash[offset + 3] ?? 0) & 0xff);
return (code % 10 ** digits).toString().padStart(digits, '0');
}
function generateTOTP(secret: string, timeStep: number = 30, digits: number = 6): string {
const counter = Math.floor(Date.now() / 1000 / timeStep);
return generateHOTP(secret, counter, digits);
}
// === Argument Parsing ===
function parseSecret(argv: string[]): string {
for (let i = 2; i < argv.length; i++) {
const next = argv[i + 1];
if (argv[i] === '--secret' && next) {
return next;
}
}
return '';
}
// === Main ===
function main(): void {
const secret = parseSecret(process.argv);
if (!secret) {
console.log(JSON.stringify({ status: 'error', message: 'Missing required --secret argument', retryable: false }));
process.exit(1);
}
const base32Regex = /^[A-Z2-7]+$/i;
if (!base32Regex.test(secret)) {
console.log(
JSON.stringify({
status: 'error',
message: 'Secret must be base32-encoded (characters A-Z and 2-7)',
retryable: false,
}),
);
process.exit(1);
}
try {
const totpCode = generateTOTP(secret);
const expiresIn = 30 - (Math.floor(Date.now() / 1000) % 30);
console.log(
JSON.stringify({
status: 'success',
totpCode,
expiresIn,
}),
);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
console.log(JSON.stringify({ status: 'error', message: `TOTP generation failed: ${msg}`, retryable: false }));
process.exit(1);
}
}
main();
+191
View File
@@ -0,0 +1,191 @@
#!/usr/bin/env node
// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
/**
* save-deliverable CLI
*
* Standalone script to save deliverable files with validation.
* Replaces the MCP save_deliverable tool.
*
* Usage:
* node save-deliverable.js --type INJECTION_QUEUE --content '{"vulnerabilities": [...]}'
* node save-deliverable.js --type INJECTION_ANALYSIS --file-path deliverables/injection_analysis_deliverable.md
*/
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { DELIVERABLE_FILENAMES, type DeliverableType, isQueueType } from '../types/deliverables.js';
// === Argument Parsing ===
interface ParsedArgs {
type: string;
content?: string;
filePath?: string;
}
function parseArgs(argv: string[]): ParsedArgs {
const args: ParsedArgs = { type: '' };
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
const next = argv[i + 1];
if (arg === '--type' && next) {
args.type = next;
i++;
} else if (arg === '--content' && next) {
args.content = next;
i++;
} else if (arg === '--file-path' && next) {
args.filePath = next;
i++;
}
}
return args;
}
// === Queue Validation ===
interface ValidationResult {
valid: boolean;
message?: string;
}
function validateQueueJson(content: string): ValidationResult {
try {
const parsed = JSON.parse(content) as unknown;
if (typeof parsed !== 'object' || parsed === null) {
return {
valid: false,
message: `Invalid queue structure: Expected an object. Got: ${typeof parsed}`,
};
}
const obj = parsed as Record<string, unknown>;
if (!('vulnerabilities' in obj)) {
return {
valid: false,
message: `Invalid queue structure: Missing 'vulnerabilities' property. Expected: {"vulnerabilities": [...]}`,
};
}
if (!Array.isArray(obj.vulnerabilities)) {
return {
valid: false,
message: `Invalid queue structure: 'vulnerabilities' must be an array. Expected: {"vulnerabilities": [...]}`,
};
}
return { valid: true };
} catch (error) {
return {
valid: false,
message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
// === File Operations ===
function saveDeliverableFile(targetDir: string, filename: string, content: string): string {
const deliverablesDir = join(targetDir, 'deliverables');
const filepath = join(deliverablesDir, filename);
try {
mkdirSync(deliverablesDir, { recursive: true });
} catch {
throw new Error(`Cannot create deliverables directory at ${deliverablesDir}`);
}
writeFileSync(filepath, content, 'utf8');
return filepath;
}
// === Main ===
function main(): void {
const args = parseArgs(process.argv);
// 1. Validate --type
if (!args.type) {
console.log(JSON.stringify({ status: 'error', message: 'Missing required --type argument', retryable: false }));
process.exit(1);
}
const deliverableType = args.type as DeliverableType;
const filename = DELIVERABLE_FILENAMES[deliverableType];
if (!filename) {
console.log(
JSON.stringify({ status: 'error', message: `Unknown deliverable type: ${args.type}`, retryable: false }),
);
process.exit(1);
}
// 2. Resolve content from --content or --file-path
let content: string;
if (args.content) {
content = args.content;
} else if (args.filePath) {
// Path traversal protection: must resolve inside cwd
const cwd = process.cwd();
const resolved = resolve(cwd, args.filePath);
if (!resolved.startsWith(`${cwd}/`) && resolved !== cwd) {
console.log(
JSON.stringify({ status: 'error', message: `Path traversal detected: ${args.filePath}`, retryable: false }),
);
process.exit(1);
}
try {
content = readFileSync(resolved, 'utf8');
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
console.log(JSON.stringify({ status: 'error', message: `Failed to read file: ${msg}`, retryable: true }));
process.exit(1);
}
} else {
console.log(
JSON.stringify({
status: 'error',
message: 'Either --content or --file-path is required',
retryable: false,
}),
);
process.exit(1);
}
// 3. Validate queue types
let validated = false;
if (isQueueType(args.type)) {
const validation = validateQueueJson(content);
if (!validation.valid) {
console.log(JSON.stringify({ status: 'error', message: validation.message, retryable: true }));
process.exit(1);
}
validated = true;
}
// 4. Save the file
try {
const targetDir = process.cwd();
const filepath = saveDeliverableFile(targetDir, filename, content);
console.log(JSON.stringify({ status: 'success', filepath, validated }));
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
console.log(JSON.stringify({ status: 'error', message: `Failed to save: ${msg}`, retryable: true }));
process.exit(1);
}
}
main();
@@ -63,7 +63,6 @@ const RETRYABLE_PATTERNS = [
'service unavailable',
'bad gateway',
// Claude API errors
'mcp server',
'model unavailable',
'service temporarily unavailable',
'api error',
+10 -10
View File
@@ -6,7 +6,7 @@
import { fs, path } from 'zx';
import { PROMPTS_DIR } from '../paths.js';
import { MCP_AGENT_MAPPING } from '../session-manager.js';
import { PLAYWRIGHT_SESSION_MAPPING } from '../session-manager.js';
import type { ActivityLogger } from '../types/activity-logger.js';
import type { Authentication, DistributedConfig } from '../types/config.js';
import { handlePromptError, PentestError } from './error-handling.js';
@@ -14,7 +14,7 @@ import { handlePromptError, PentestError } from './error-handling.js';
interface PromptVariables {
webUrl: string;
repoPath: string;
MCP_SERVER?: string;
PLAYWRIGHT_SESSION?: string;
}
interface IncludeReplacement {
@@ -166,7 +166,7 @@ async function interpolateVariables(
let result = template
.replace(/{{WEB_URL}}/g, variables.webUrl)
.replace(/{{REPO_PATH}}/g, variables.repoPath)
.replace(/{{MCP_SERVER}}/g, variables.MCP_SERVER || 'playwright-agent1')
.replace(/{{PLAYWRIGHT_SESSION}}/g, variables.PLAYWRIGHT_SESSION || 'agent1')
.replace(/{{AUTH_CONTEXT}}/g, buildAuthContext(config));
if (config) {
@@ -236,16 +236,16 @@ export async function loadPrompt(
throw new PentestError(`Prompt file not found: ${promptPath}`, 'prompt', false, { promptName, promptPath });
}
// 2. Assign MCP server based on agent name
// 2. Assign Playwright session based on agent name
const enhancedVariables: PromptVariables = { ...variables };
const mcpServer = MCP_AGENT_MAPPING[promptName as keyof typeof MCP_AGENT_MAPPING];
if (mcpServer) {
enhancedVariables.MCP_SERVER = mcpServer;
logger.info(`Assigned ${promptName} -> ${enhancedVariables.MCP_SERVER}`);
const session = PLAYWRIGHT_SESSION_MAPPING[promptName as keyof typeof PLAYWRIGHT_SESSION_MAPPING];
if (session) {
enhancedVariables.PLAYWRIGHT_SESSION = session;
logger.info(`Assigned ${promptName} -> ${enhancedVariables.PLAYWRIGHT_SESSION}`);
} else {
enhancedVariables.MCP_SERVER = 'playwright-agent1';
logger.warn(`Unknown agent ${promptName}, using fallback -> ${enhancedVariables.MCP_SERVER}`);
enhancedVariables.PLAYWRIGHT_SESSION = 'agent1';
logger.warn(`Unknown agent ${promptName}, using fallback -> ${enhancedVariables.PLAYWRIGHT_SESSION}`);
}
// 3. Read template file
+19 -24
View File
@@ -7,10 +7,9 @@
import { fs, path } from 'zx';
import { validateQueueAndDeliverable } from './services/queue-validation.js';
import type { ActivityLogger } from './types/activity-logger.js';
import type { AgentDefinition, AgentName, AgentValidator, PlaywrightAgent, VulnType } from './types/index.js';
import type { AgentDefinition, AgentName, AgentValidator, PlaywrightSession, VulnType } from './types/index.js';
// Agent definitions according to PRD
// NOTE: deliverableFilename values must match mcp-server/src/types/deliverables.ts:DELIVERABLE_FILENAMES
export const AGENTS: Readonly<Record<AgentName, AgentDefinition>> = Object.freeze({
'pre-recon': {
name: 'pre-recon',
@@ -149,35 +148,31 @@ function createExploitValidator(vulnType: VulnType): AgentValidator {
};
}
// MCP agent mapping - assigns each agent to a specific Playwright instance to prevent conflicts
// Playwright session mapping - assigns each agent to a specific session for browser isolation
// Keys are promptTemplate values from AGENTS registry
export const MCP_AGENT_MAPPING: Record<string, PlaywrightAgent> = Object.freeze({
// Phase 1: Pre-reconnaissance (actual prompt name is 'pre-recon-code')
// NOTE: Pre-recon is pure code analysis and doesn't use browser automation,
// but assigning MCP server anyway for consistency and future extensibility
'pre-recon-code': 'playwright-agent1',
export const PLAYWRIGHT_SESSION_MAPPING: Record<string, PlaywrightSession> = Object.freeze({
// Phase 1: Pre-reconnaissance
'pre-recon-code': 'agent1',
// Phase 2: Reconnaissance (actual prompt name is 'recon')
recon: 'playwright-agent2',
// Phase 2: Reconnaissance
recon: 'agent2',
// Phase 3: Vulnerability Analysis (5 parallel agents)
'vuln-injection': 'playwright-agent1',
'vuln-xss': 'playwright-agent2',
'vuln-auth': 'playwright-agent3',
'vuln-ssrf': 'playwright-agent4',
'vuln-authz': 'playwright-agent5',
'vuln-injection': 'agent1',
'vuln-xss': 'agent2',
'vuln-auth': 'agent3',
'vuln-ssrf': 'agent4',
'vuln-authz': 'agent5',
// Phase 4: Exploitation (5 parallel agents - same as vuln counterparts)
'exploit-injection': 'playwright-agent1',
'exploit-xss': 'playwright-agent2',
'exploit-auth': 'playwright-agent3',
'exploit-ssrf': 'playwright-agent4',
'exploit-authz': 'playwright-agent5',
'exploit-injection': 'agent1',
'exploit-xss': 'agent2',
'exploit-auth': 'agent3',
'exploit-ssrf': 'agent4',
'exploit-authz': 'agent5',
// Phase 5: Reporting (actual prompt name is 'report-executive')
// NOTE: Report generation is typically text-based and doesn't use browser automation,
// but assigning MCP server anyway for potential screenshot inclusion or future needs
'report-executive': 'playwright-agent3',
// Phase 5: Reporting
'report-executive': 'agent3',
});
// Direct agent-to-validator mapping - much simpler than pattern matching
+1 -6
View File
@@ -34,12 +34,7 @@ export const ALL_AGENTS = [
*/
export type AgentName = (typeof ALL_AGENTS)[number];
export type PlaywrightAgent =
| 'playwright-agent1'
| 'playwright-agent2'
| 'playwright-agent3'
| 'playwright-agent4'
| 'playwright-agent5';
export type PlaywrightSession = 'agent1' | 'agent2' | 'agent3' | 'agent4' | 'agent5';
import type { ActivityLogger } from './activity-logger.js';
+94
View File
@@ -0,0 +1,94 @@
// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
/**
* Deliverable Type Definitions
*
* Maps deliverable types to their filenames and defines validation requirements.
*/
export enum DeliverableType {
// Pre-recon agent
CODE_ANALYSIS = 'CODE_ANALYSIS',
// Recon agent
RECON = 'RECON',
// Vulnerability analysis agents
INJECTION_ANALYSIS = 'INJECTION_ANALYSIS',
INJECTION_QUEUE = 'INJECTION_QUEUE',
XSS_ANALYSIS = 'XSS_ANALYSIS',
XSS_QUEUE = 'XSS_QUEUE',
AUTH_ANALYSIS = 'AUTH_ANALYSIS',
AUTH_QUEUE = 'AUTH_QUEUE',
AUTHZ_ANALYSIS = 'AUTHZ_ANALYSIS',
AUTHZ_QUEUE = 'AUTHZ_QUEUE',
SSRF_ANALYSIS = 'SSRF_ANALYSIS',
SSRF_QUEUE = 'SSRF_QUEUE',
// Exploitation agents
INJECTION_EVIDENCE = 'INJECTION_EVIDENCE',
XSS_EVIDENCE = 'XSS_EVIDENCE',
AUTH_EVIDENCE = 'AUTH_EVIDENCE',
AUTHZ_EVIDENCE = 'AUTHZ_EVIDENCE',
SSRF_EVIDENCE = 'SSRF_EVIDENCE',
}
/**
* Hard-coded filename mappings from agent prompts
*/
export const DELIVERABLE_FILENAMES: Record<DeliverableType, string> = {
[DeliverableType.CODE_ANALYSIS]: 'code_analysis_deliverable.md',
[DeliverableType.RECON]: 'recon_deliverable.md',
[DeliverableType.INJECTION_ANALYSIS]: 'injection_analysis_deliverable.md',
[DeliverableType.INJECTION_QUEUE]: 'injection_exploitation_queue.json',
[DeliverableType.XSS_ANALYSIS]: 'xss_analysis_deliverable.md',
[DeliverableType.XSS_QUEUE]: 'xss_exploitation_queue.json',
[DeliverableType.AUTH_ANALYSIS]: 'auth_analysis_deliverable.md',
[DeliverableType.AUTH_QUEUE]: 'auth_exploitation_queue.json',
[DeliverableType.AUTHZ_ANALYSIS]: 'authz_analysis_deliverable.md',
[DeliverableType.AUTHZ_QUEUE]: 'authz_exploitation_queue.json',
[DeliverableType.SSRF_ANALYSIS]: 'ssrf_analysis_deliverable.md',
[DeliverableType.SSRF_QUEUE]: 'ssrf_exploitation_queue.json',
[DeliverableType.INJECTION_EVIDENCE]: 'injection_exploitation_evidence.md',
[DeliverableType.XSS_EVIDENCE]: 'xss_exploitation_evidence.md',
[DeliverableType.AUTH_EVIDENCE]: 'auth_exploitation_evidence.md',
[DeliverableType.AUTHZ_EVIDENCE]: 'authz_exploitation_evidence.md',
[DeliverableType.SSRF_EVIDENCE]: 'ssrf_exploitation_evidence.md',
};
/**
* Queue types that require JSON validation
*/
export const QUEUE_TYPES: DeliverableType[] = [
DeliverableType.INJECTION_QUEUE,
DeliverableType.XSS_QUEUE,
DeliverableType.AUTH_QUEUE,
DeliverableType.AUTHZ_QUEUE,
DeliverableType.SSRF_QUEUE,
];
/**
* Type guard to check if a deliverable type is a queue
*/
export function isQueueType(type: string): boolean {
return QUEUE_TYPES.includes(type as DeliverableType);
}
/**
* Vulnerability queue structure
*/
export interface VulnerabilityQueue {
vulnerabilities: VulnerabilityItem[];
}
export interface VulnerabilityItem {
[key: string]: unknown;
}
+1
View File
@@ -12,6 +12,7 @@ export * from './activity-logger.js';
export * from './agents.js';
export * from './audit.js';
export * from './config.js';
export * from './deliverables.js';
export * from './errors.js';
export * from './metrics.js';
export * from './result.js';